mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 00:32:31 -04:00
Compare commits
719 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 240de5a5dd | |||
| 3b4f7a3c81 | |||
| 891446d063 | |||
| fa7322365a | |||
| 4fbdd23283 | |||
| c953d17bcc | |||
| 904d41e51a | |||
| a2d71bdd94 | |||
| 11ae1aa6b8 | |||
| 3c8837163d | |||
| b6ca782c99 | |||
| a976629174 | |||
| cd66b62083 | |||
| 4b68de8418 | |||
| 008415f206 | |||
| f0eae39cb2 | |||
| 7fa90f08ae | |||
| 5ec503386c | |||
| 6f9a39525a | |||
| cf611796c6 | |||
| aadda6e34e | |||
| 6c4cb5006a | |||
| 12107f035c | |||
| 832df649c1 | |||
| cc63eca0c8 | |||
| aa94f2b802 | |||
| 3f9a431100 | |||
| 8c860641b9 | |||
| 99914d2204 | |||
| 24b2e02ee5 | |||
| be2fdb6af6 | |||
| 16b296c97e | |||
| 11eee95222 | |||
| 1de4a99ec3 | |||
| 96579b97f6 | |||
| 8cc2f770fa | |||
| a23f707268 | |||
| ed4c2775e4 | |||
| bff2469d9d | |||
| a08ab0c007 | |||
| 28e1f7c562 | |||
| 914f39d784 | |||
| 0ba427a6f4 | |||
| 7fab1b15c8 | |||
| 3856ad03b0 | |||
| d411b7d087 | |||
| 580f7677ad | |||
| 120811e7f7 | |||
| 43458bda46 | |||
| a9ccaa1ae5 | |||
| f6ee100bae | |||
| f5720fecd6 | |||
| 0b2e054839 | |||
| 6f01928512 | |||
| 6115a462c7 | |||
| 5f9cba0f19 | |||
| 05b3938556 | |||
| 62b4553f7d | |||
| ad20323b52 | |||
| 721c100bb0 | |||
| 6720bdfb55 | |||
| 0c626fbc2e | |||
| af82141808 | |||
| d11b648137 | |||
| 14a8ffedd8 | |||
| b5906135c7 | |||
| 4bad5c79be | |||
| 81430e4aff | |||
| c238b72d5d | |||
| a2ed91bc45 | |||
| 15fecbc161 | |||
| c32a0f5f71 | |||
| 0c3d90ed21 | |||
| fb31669261 | |||
| 917d9bc9da | |||
| fd6e4516dc | |||
| 86205efcfe | |||
| 701e77514f | |||
| 018105eec9 | |||
| bf6ec2bbfd | |||
| 13d0454f71 | |||
| db2741c6e0 | |||
| 605787f671 | |||
| 657780bcdf | |||
| 9d767e768a | |||
| 15268e8cdb | |||
| 04789a2446 | |||
| e28ee90c2a | |||
| 3841517ce1 | |||
| bea48b80ce | |||
| 9f525af210 | |||
| c00b3a520c | |||
| f6e6a6be04 | |||
| bc5df3b383 | |||
| 1a0292b830 | |||
| e6a3e5e1f3 | |||
| 397d67876c | |||
| 47b78714b8 | |||
| fda7350a43 | |||
| 80dfb8b2a7 | |||
| 98f160e39c | |||
| 4f8020a94c | |||
| b295aab2d8 | |||
| 448edcca8e | |||
| 72d0debde6 | |||
| 9037d3ab85 | |||
| 8a511989a0 | |||
| 44e3a97a67 | |||
| c0190a3460 | |||
| 396d8e989f | |||
| 33b00dc8b1 | |||
| eb9857137a | |||
| c1d6c928e3 | |||
| 118f666706 | |||
| e9641c5c7e | |||
| 495656f72b | |||
| c70d4a4cf6 | |||
| 39c5d6b964 | |||
| 0c69e9ed7f | |||
| 0a95b5d359 | |||
| 6246d4c3ca | |||
| 4de9d64c0c | |||
| 1867ded14c | |||
| 22db8bcf3d | |||
| 59e7a8864a | |||
| 7d737427a9 | |||
| eac939e9a7 | |||
| 2ea544e9a0 | |||
| 87b645386f | |||
| e3ba9ffff2 | |||
| e0efb027da | |||
| 9e4a29191c | |||
| fa10b0275f | |||
| 4f8ff09551 | |||
| f2491580e0 | |||
| 8369a12115 | |||
| 97e1f14dd3 | |||
| 930ca1cc1b | |||
| 23627bbf54 | |||
| 2fc615b405 | |||
| a36c7c7e87 | |||
| fdec3c68f0 | |||
| 0ecc5c46bf | |||
| a947f70c56 | |||
| c259381541 | |||
| 7f546e529e | |||
| a7aeb979be | |||
| 771dcf3d40 | |||
| f3a4f46d78 | |||
| 78455c7cb9 | |||
| 01f2b85826 | |||
| 7fe9e13fbf | |||
| f92a3aa0e5 | |||
| 917534e35e | |||
| 8ab447e615 | |||
| 0d8384a9b4 | |||
| e14328b71b | |||
| f5aaa471de | |||
| 0b83014ff8 | |||
| 0684cf8611 | |||
| 1570bc5d03 | |||
| 8811853f6d | |||
| b7028b139f | |||
| 620f9687c8 | |||
| 2c43616781 | |||
| d1171af679 | |||
| 598de9e6d9 | |||
| 393bc2992e | |||
| 33f2b16a1b | |||
| f03ad80701 | |||
| a68b01080c | |||
| e0f1a02c37 | |||
| 2358102c07 | |||
| 1533652b78 | |||
| c7562e46a4 | |||
| 8f583dcf36 | |||
| 09188981c4 | |||
| ae5f013a48 | |||
| b7091650f8 | |||
| 3a810c6502 | |||
| 764c9ec956 | |||
| ce0988f48a | |||
| 1c92557c8b | |||
| 8f7a1d6a25 | |||
| 1b085efa47 | |||
| d9e6e7ffa5 | |||
| 05d0b213a9 | |||
| 6f580c6aa3 | |||
| 1d9a094315 | |||
| f6e50890b3 | |||
| 22dfb140d0 | |||
| 15455e5a7e | |||
| f46da403d8 | |||
| 4f5df39bdd | |||
| 1f8d1df4ec | |||
| dd83687447 | |||
| 3ce3f3a96a | |||
| 86060ef9b4 | |||
| d3e3fc533f | |||
| 03b10f9c8e | |||
| f7757da7ed | |||
| 13f9c34d16 | |||
| 13a54dbdda | |||
| 7ed7a95524 | |||
| d47b041923 | |||
| dfbc2e81e3 | |||
| 9edc16e4d6 | |||
| 73273c5bf8 | |||
| 93c5256318 | |||
| 3ccad1814e | |||
| 35269572d7 | |||
| a457b35750 | |||
| 5e5f9b0563 | |||
| 16722e4d99 | |||
| 89c20f9a55 | |||
| d3b731e925 | |||
| 3e0695ee31 | |||
| 9239f3cbcc | |||
| b7a7fd4651 | |||
| 06b067b02c | |||
| dfb5aa6dc6 | |||
| f56696f478 | |||
| fcbb90a9af | |||
| be84b74d01 | |||
| bb5b01c911 | |||
| 3ca6bc4a66 | |||
| 053373a385 | |||
| e263566673 | |||
| 6965075825 | |||
| e54dfa49c3 | |||
| accaa378f0 | |||
| 60a0208e8d | |||
| 2aaaa368bb | |||
| 4829cc6aaf | |||
| 553acf93e2 | |||
| f058419042 | |||
| 13268db536 | |||
| 1f7b5abc80 | |||
| c667f81866 | |||
| b321c00a8f | |||
| 9160789b42 | |||
| df7cdc3fae | |||
| 86fd2f22fb | |||
| 148a6f4430 | |||
| b05006663f | |||
| 5f1f8e4ee6 | |||
| ef48e17e79 | |||
| fe03c1aefa | |||
| 078770a5a6 | |||
| 294f6957f0 | |||
| fe664c00ff | |||
| 518edd3cd4 | |||
| b019501b8b | |||
| 2922d09bef | |||
| 97487e6f0d | |||
| 694d2c9b2e | |||
| a674c0051a | |||
| 98de336a21 | |||
| 9fe2ef417c | |||
| 88edca65d3 | |||
| 64c18a7c6c | |||
| d2fc045219 | |||
| 917a604094 | |||
| b33b24fc9e | |||
| 4d9ee000c8 | |||
| 2966db7b78 | |||
| 38e65e28d4 | |||
| 73b61af58d | |||
| 858e96f21c | |||
| f379bf3421 | |||
| 1896b420d8 | |||
| 1580169e2b | |||
| 95514da91b | |||
| 18ff8748e7 | |||
| 2ed1dd6afc | |||
| 8039a7127f | |||
| a8dfa9f0b7 | |||
| 33aeb1cb5c | |||
| 8bdd13b594 | |||
| 52316952a5 | |||
| 7c868afd32 | |||
| 4df8028bc3 | |||
| f1eaae9b0d | |||
| 385ea53309 | |||
| 2716e272c1 | |||
| ca34a3e1aa | |||
| 3ee6d30659 | |||
| ef40659c70 | |||
| 6e2de19d9f | |||
| 3afb1ae380 | |||
| 37c852c382 | |||
| 3d01f46efa | |||
| 3a6496c268 | |||
| 64c9f20919 | |||
| d10d8c23c4 | |||
| 3cd36fd47d | |||
| aaec7e469c | |||
| 6f78cc49d1 | |||
| 13dfffd203 | |||
| 5552dcbbc7 | |||
| 37b291f82c | |||
| d3f338ddab | |||
| 3b66865da5 | |||
| 637b0b47ee | |||
| a6521357e5 | |||
| 269a8b5fce | |||
| 1201492222 | |||
| faa5248d1f | |||
| 986d4ffe3d | |||
| a03eba6fbc | |||
| 8db80c4a88 | |||
| 4704a56a17 | |||
| 896dc6bc69 | |||
| 6f4cf7eec7 | |||
| be96cc0e65 | |||
| ef585ed810 | |||
| 4b2e22289d | |||
| f26447e2fb | |||
| 08028714b5 | |||
| 2de4950015 | |||
| d29640699e | |||
| 6a9aea04b1 | |||
| 592d199315 | |||
| 5820356cf6 | |||
| 6b3c2212a1 | |||
| 703cf7bf8b | |||
| 3e00e18adc | |||
| 6c17e4d4c8 | |||
| 388ff6bc0a | |||
| fc2ff9155c | |||
| a50f3a4cfe | |||
| fd3fafa50c | |||
| e20779e405 | |||
| fc6d62286e | |||
| e2997ac974 | |||
| 8f0b44b8a4 | |||
| 50ab4fe11e | |||
| 106d62b067 | |||
| a76222f607 | |||
| e9515425e0 | |||
| c80c34ef45 | |||
| 1ba5512015 | |||
| 55a564df6d | |||
| 8a326d4dc1 | |||
| d35719daed | |||
| c296d7e7e0 | |||
| fc1509eed4 | |||
| 9619fe224c | |||
| c0efec52d9 | |||
| a74320bf4c | |||
| 1125a236ea | |||
| 8658e189e1 | |||
| 9a22cda15d | |||
| 169ab3acda | |||
| 5f39cbef94 | |||
| 63fd264043 | |||
| 345b312e00 | |||
| 5cca9cc18e | |||
| 9ebc11d775 | |||
| 689591ef01 | |||
| 2782553231 | |||
| 4ec5522a33 | |||
| ad2956fd1d | |||
| 34a34c565d | |||
| 74d4fd3c29 | |||
| ac1f3bfaaa | |||
| f7a70266ed | |||
| fc75527eb5 | |||
| e5d04f9a96 | |||
| 91a60a8d25 | |||
| 5c9fc3a473 | |||
| 02ac1f61c4 | |||
| 59a8ada4a8 | |||
| 1889049ef3 | |||
| 68a495f144 | |||
| a2db340378 | |||
| c6a2911725 | |||
| 654f26cb91 | |||
| dd4b3efa47 | |||
| 3a969bc075 | |||
| 425f61142f | |||
| 79072828a5 | |||
| 0548b97701 | |||
| 99625ae3f6 | |||
| c4dfbb9956 | |||
| b0d9c058cc | |||
| cccfe3b4ef | |||
| f71955e89c | |||
| dd44491e13 | |||
| ac865e8910 | |||
| b7167803f2 | |||
| 97710ced7e | |||
| f878247a18 | |||
| 118cf5f240 | |||
| f9cba03d25 | |||
| baf6db5b57 | |||
| e60400a92e | |||
| e377eeff50 | |||
| 84a2f8e89e | |||
| 64be3e410c | |||
| 643dac688c | |||
| 0a624f87ff | |||
| fea8f37f9d | |||
| a808252079 | |||
| 93bcca0ccc | |||
| d39b95600a | |||
| 545fa844bb | |||
| b6e10e3cb2 | |||
| bc56793d3b | |||
| ad973f1d12 | |||
| c06941ed52 | |||
| 54c65cb025 | |||
| 22b835b9f4 | |||
| 46ae4a6652 | |||
| 56453e9664 | |||
| 3b144c21d0 | |||
| 9e156e0940 | |||
| 65191eb5ae | |||
| f6d75bb79a | |||
| f069a575cc | |||
| 32bb6a4cde | |||
| a59bdd08ca | |||
| b324a32b61 | |||
| 10484cfad2 | |||
| 129efde9b0 | |||
| a16a80ca52 | |||
| 6d7462ac99 | |||
| c0c7437fa5 | |||
| 01f3593fd6 | |||
| 4cce8c7b6b | |||
| 0d99751a2f | |||
| 0a31c32fb7 | |||
| 0b4dda0aba | |||
| c7868affe1 | |||
| 74316fe01b | |||
| ef3d63e3e5 | |||
| 4b1b329edb | |||
| e49474a4f5 | |||
| c026e2b734 | |||
| be36fec7ea | |||
| 49e98a1518 | |||
| a7498bee68 | |||
| 280ae833d4 | |||
| 261547b42c | |||
| 53ae9b8521 | |||
| 20fbc7303c | |||
| 6b546389b8 | |||
| ff56151931 | |||
| 981f364845 | |||
| 5e0896305c | |||
| d2fa8600fc | |||
| ebce0b7aec | |||
| b699a17a1b | |||
| b5ec462299 | |||
| 617988844b | |||
| 4e52b3fe8a | |||
| bd67ec99f0 | |||
| a7ed0cf69e | |||
| d48e51cb78 | |||
| d3e5f9d456 | |||
| cbb85532a8 | |||
| 65bc696b0c | |||
| e7f08bff38 | |||
| 16fa3ecb0f | |||
| dd3f460cf8 | |||
| 36d8d2c7de | |||
| c06ff1cb37 | |||
| a48e4ecb5a | |||
| 74940af624 | |||
| 32ec39cdea | |||
| a197c864e8 | |||
| 4991d702fd | |||
| 76a282718d | |||
| c8307409c9 | |||
| 1366a44639 | |||
| ea245b5af5 | |||
| 10d5422c3e | |||
| b63d9fdc68 | |||
| 9b073aad58 | |||
| ae7e098240 | |||
| 6e0317a703 | |||
| 20f76a256e | |||
| 40b52fb02e | |||
| 91150bb770 | |||
| f1dd9f2b79 | |||
| 6aba4a311a | |||
| 56153e0bb3 | |||
| 905eb70773 | |||
| e2544597a1 | |||
| ba1132214e | |||
| b987c7893c | |||
| aebe387f72 | |||
| 0985024670 | |||
| 25a596a98f | |||
| acc67eb3b2 | |||
| 4c700efbbb | |||
| 9ad96b33ff | |||
| 387a083255 | |||
| 95366e41c4 | |||
| a6ec51b349 | |||
| f6a96227c4 | |||
| 56b3ea876b | |||
| 2d9273f915 | |||
| 8bc7b93bc8 | |||
| 4750699ab0 | |||
| a4bf6e586d | |||
| dfa389c9df | |||
| 078c991574 | |||
| bf7b25482e | |||
| 3bc925400b | |||
| 655e61ab32 | |||
| 43b56d621b | |||
| 7b5efb5d75 | |||
| 3390862918 | |||
| 47fc35acc0 | |||
| d3fc9f7a9b | |||
| a63a6ecb04 | |||
| 47e770621c | |||
| 7516b4b533 | |||
| 133ed18374 | |||
| b0ab3d4281 | |||
| f68233a1ba | |||
| f3721c103c | |||
| 3e2b1d145a | |||
| f4b6f15e07 | |||
| 95a6237693 | |||
| 0da76e2b76 | |||
| 8051c73cc3 | |||
| a368230ba5 | |||
| 8a058828a3 | |||
| ee124a6d3c | |||
| 97a631ec4c | |||
| cbdd3a4f8e | |||
| 6b8e40b3fb | |||
| 132f2a9cc3 | |||
| baf269d4e2 | |||
| 20a047f7e1 | |||
| 6ab0d8d8d9 | |||
| 6fde3632ef | |||
| 474f119702 | |||
| 33e1560d53 | |||
| a5eb552215 | |||
| 7fc0940fe6 | |||
| 7323b14580 | |||
| 1845e5cf52 | |||
| 410ece831f | |||
| ebf4279e98 | |||
| b0cf3f0d2d | |||
| 8d3f336971 | |||
| 05ea5c32be | |||
| a3b2a6a296 | |||
| 724829b689 | |||
| 73494ce63a | |||
| 5f860d3a9f | |||
| 6bb84ba19c | |||
| 710f38043e | |||
| 958abcfa4c | |||
| ea24744bbf | |||
| f06b825f44 | |||
| 642aa63a9c | |||
| ae645ef2e9 | |||
| 90efff68e5 | |||
| e38921f4a5 | |||
| 8e7a36de45 | |||
| 86d107f641 | |||
| dfebffb1ee | |||
| 59a5afab29 | |||
| d8fb2ddc2d | |||
| 5e467883b8 | |||
| 9fbac10a4b | |||
| 6d9783a267 | |||
| d5371aff22 | |||
| 5685a16449 | |||
| f58653bc13 | |||
| e0ed709397 | |||
| b3dd604904 | |||
| 8f09ed8f0d | |||
| 49d79d7ebc | |||
| 4c034f6ad1 | |||
| 503c6b392c | |||
| 0146bb4e49 | |||
| 7ee4ea244f | |||
| 705cb98865 | |||
| ff45801cda | |||
| 761a32a080 | |||
| aa7ecb02af | |||
| 5d7db89a90 | |||
| 1bae36ef29 | |||
| 52fd4f89bf | |||
| cad89a07e0 | |||
| b18527285d | |||
| 1deb99c75c | |||
| 0775f9123c | |||
| 5fbd63e35d | |||
| f09fff3d8b | |||
| 0a798aafac | |||
| f8614b877d | |||
| 182e1b4fb2 | |||
| c684de9a88 | |||
| 27785f7993 | |||
| ad4191a07e | |||
| 91da965a39 | |||
| b37da03989 | |||
| 92af3ee4d8 | |||
| 1e8ab1cadf | |||
| 729e4f0239 | |||
| 790c842fad | |||
| f28a159b72 | |||
| f77a7a805a | |||
| 236341f78b | |||
| ac3bbdbd3f | |||
| ce2a9cd8f9 | |||
| 4462e3978b | |||
| 344017dc21 | |||
| a56a833423 | |||
| 6b66b19deb | |||
| 33257de2e8 | |||
| 702dec0647 | |||
| 8d1da68b47 | |||
| 7a7e3d160b | |||
| 5a1243ff42 | |||
| edf9cd34cc | |||
| f415ea263e | |||
| 3ca419e2cf | |||
| 7d15435361 | |||
| e26a855d8b | |||
| c0ce2b1d50 | |||
| 59bf71c293 | |||
| 464ade1da7 | |||
| ce47cf51be | |||
| 6be0386716 | |||
| 398d9a6bb5 | |||
| 956266cd79 | |||
| 6cabc9bfe3 | |||
| da674fd599 | |||
| 4e1229e7c9 | |||
| 5341c85a27 | |||
| fbd6412359 | |||
| 36d2027493 | |||
| a148b92381 | |||
| 36a62f0915 | |||
| d85e90a7b4 | |||
| d5cc10f7aa | |||
| 96bfb9f347 | |||
| 5e48f0a412 | |||
| 18c93756b4 | |||
| cfe52084aa | |||
| 6aa0e30af3 | |||
| 5a41e8bc1a | |||
| 9e4eeb4fb7 | |||
| c62b6b9f1a | |||
| 52584f7f23 | |||
| 2be0dc40f0 | |||
| e3e62a952d | |||
| 6bc3e7536e | |||
| df9d062a8f | |||
| eafbf0b218 | |||
| 73d52490d0 | |||
| 4a095590b1 | |||
| c8514ad7b7 | |||
| e3f2d96a5e | |||
| bcddfb2daa | |||
| 75ccc05d84 | |||
| 0a0d2cc1cf | |||
| 50749b4e84 | |||
| 06873175bf | |||
| f49e0c9b56 | |||
| ccdc28631a | |||
| a2c410b8e1 | |||
| 73794f2a2c | |||
| 4b877eebc4 | |||
| c4842e0fc1 | |||
| ff8c430ff0 | |||
| 1262ae92e9 | |||
| 6083871088 | |||
| ce3580bf91 | |||
| 9720da5bc8 | |||
| 286d8d1e89 | |||
| 977a3c3226 | |||
| 82cbd7a96b | |||
| cdf7cf5c3f | |||
| 579007822f | |||
| e50de809a5 | |||
| c37481cc7b | |||
| 91ff734327 | |||
| 524dcee9f6 | |||
| 0cc48e849c | |||
| 58b2edd229 | |||
| 6271abb22a | |||
| 58053fce48 | |||
| 55bded68c2 | |||
| dc3efc939c | |||
| bdb61f4a1d | |||
| 1183d91c7b | |||
| 463c9d9dd2 | |||
| 1bd9e9e590 | |||
| b650a26727 | |||
| 943ed931db | |||
| 2417d70bcb | |||
| 1a7612071a | |||
| 5072d70f38 | |||
| b210101f45 | |||
| 18edf5864e | |||
| ce7d3db1be | |||
| f32eed1912 | |||
| cdb79a60f2 | |||
| 7419573266 | |||
| d8f92baee2 | |||
| 9e9298ee5d | |||
| dc6c986b3f | |||
| 65cb966d38 | |||
| d264a2cf0a | |||
| 139a3cfb13 | |||
| 04da9c7374 | |||
| 16250da3f0 | |||
| 45a0e4cf49 | |||
| e14a62f188 | |||
| eeb23a2469 | |||
| c6ba43f888 |
@@ -9,6 +9,13 @@
|
||||
|
||||
# go fmt will enforce this, but in case a user has not called "go fmt" allow GIT to catch this:
|
||||
*.go text eol=lf core.whitespace whitespace=indent-with-non-tab,trailing-space,tabwidth=4
|
||||
go.mod text eol=lf
|
||||
go.sum text eol=lf
|
||||
|
||||
*.txt text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.tpl text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.htm text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.html text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.md text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
.git* text eol=auto core.whitespace whitespace=trailing-space
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
Contributing to Caddy
|
||||
=====================
|
||||
|
||||
Welcome! Thank you for choosing to be a part of our community. Caddy wouldn't be great without your involvement!
|
||||
|
||||
For starters, we invite you to join [the Caddy forum](https://caddy.community) where you can hang out with other Caddy users and developers.
|
||||
|
||||
## Common Tasks
|
||||
|
||||
- [Contributing code](#contributing-code)
|
||||
- [Writing a plugin](#writing-a-plugin)
|
||||
- [Asking or answering questions for help using Caddy](#getting-help-using-caddy)
|
||||
- [Reporting a bug](#reporting-bugs)
|
||||
- [Suggesting an enhancement or a new feature](#suggesting-features)
|
||||
- [Improving documentation](#improving-documentation)
|
||||
|
||||
Other menu items:
|
||||
|
||||
- [Values](#values)
|
||||
- [Responsible Disclosure](#responsible-disclosure)
|
||||
- [Thank You](#thank-you)
|
||||
|
||||
|
||||
### Contributing code
|
||||
|
||||
You can have a huge impact on the project by helping with its code. To contribute code to Caddy, open a [pull request](https://github.com/caddyserver/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy/-/search).
|
||||
|
||||
We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions—even if it seems small or insignificant. Please don't take it personally. :blue_heart: If your change is on the right track, we can guide you to make it mergable.
|
||||
|
||||
Here are some of the expectations we have of contributors:
|
||||
|
||||
- **Open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that any changes are in-line with the project's goals and the best interests of its users. We can also discuss the best possible implementation. If there's already an issue about it, comment on the existing issue to claim it.
|
||||
|
||||
- **Keep pull requests small.** Smaller PRs are more likely to be merged because they are easier to review! We might ask you to break up large PRs into smaller ones. [An example of what we want to avoid.](https://twitter.com/iamdevloper/status/397664295875805184)
|
||||
|
||||
- **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other.
|
||||
|
||||
- **Write tests.** Tests are essential! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass.
|
||||
|
||||
- **Benchmarks should be included for optimizations.** Optimizations sometimes make code harder to read or have changes that are less than obvious. They should be proven with benchmarks or profiling.
|
||||
|
||||
- **[Squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) insignificant commits.** Every commit should be significant. Commits which merely rewrite a comment or fix a typo can be combined into another commit that has more substance. Interactive rebase can do this, or a simpler way is `git reset --soft <diverging-commit>` then `git commit -s`.
|
||||
|
||||
- **Own your contributions.** Caddy is a growing project, and it's much better when individual contributors help maintain their change after it is merged.
|
||||
|
||||
- **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious.
|
||||
|
||||
We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base!
|
||||
|
||||
|
||||
#### HOW TO MAKE A PULL REQUEST TO CADDY
|
||||
|
||||
Contributing to Go projects on GitHub is fun and easy. We recommend the following workflow:
|
||||
|
||||
1. [Fork this repo](https://github.com/caddyserver/caddy). This makes a copy of the code you can write to.
|
||||
|
||||
2. If you don't already have this repo (caddyserver/caddy.git) repo on your computer, get it with `go get github.com/caddyserver/caddy/caddy`.
|
||||
|
||||
3. Tell git that it can push the caddyserver/caddy.git repo to your fork by adding a remote: `git remote add myfork https://github.com/you/caddy.git`
|
||||
|
||||
4. Make your changes in the caddyserver/caddy.git repo on your computer.
|
||||
|
||||
5. Push your changes to your fork: `git push myfork`
|
||||
|
||||
6. [Create a pull request](https://github.com/caddyserver/caddy/pull/new/master) to merge your changes into caddyserver/caddy @ master. (Click "compare across forks" and change the head fork.)
|
||||
|
||||
This workflow is nice because you don't have to change import paths. You can get fancier by using different branches if you want.
|
||||
|
||||
|
||||
### Writing a plugin
|
||||
|
||||
Caddy can do more with plugins! Anyone can write a plugin. Plugins are Go libraries that get compiled into Caddy, extending its feature set. They can add directives to the Caddyfile, change how the Caddyfile is loaded, and even implement new server types (e.g. HTTP, DNS). When it's ready, you can submit your plugin to the Caddy website so others can download it.
|
||||
|
||||
[Learn how to write and submit a plugin](https://github.com/caddyserver/caddy/wiki) on the wiki. You should also share and discuss your plugin idea [on the forums](https://caddy.community) to have people test it out. We don't use the Caddy issue tracker for plugins.
|
||||
|
||||
|
||||
### Getting help using Caddy
|
||||
|
||||
If you have a question about using Caddy, [ask on our forum](https://caddy.community)! There will be more people there who can help you than just the Caddy developers who follow our issue tracker. Issues are not the place for usage questions.
|
||||
|
||||
Many people on the forums could benefit from your experience and expertise, too. Once you've been helped, consider giving back by answering other people's questions and participating in other discussions.
|
||||
|
||||
|
||||
### Reporting bugs
|
||||
|
||||
Like every software, Caddy has its flaws. If you find one, [search the issues](https://github.com/caddyserver/caddy/issues) to see if it has already been reported. If not, [open a new issue](https://github.com/caddyserver/caddy/issues/new) and describe the bug, and somebody will look into it! (This repository is only for Caddy and its standard modules.)
|
||||
|
||||
**You can help stop bugs in their tracks!** Speed up the patch by identifying the bug in the code. This can sometimes be done by adding `fmt.Println()` statements (or similar) in relevant code paths to narrow down where the problem may be. It's a good way to [introduce yourself to the Go language](https://tour.golang.org), too.
|
||||
|
||||
Please follow the issue template so we have all the needed information. Unredacted—yes, actual values matter. We need to be able to repeat the bug using your instructions. Please simplify the issue as much as possible. 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!
|
||||
|
||||
We suggest reading [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 support when we fix your issues. If we helped you, please consider helping someone else!
|
||||
|
||||
#### Bug reporting requirements
|
||||
|
||||
Maintainers---or more generally, developers---need three things to act on bugs:
|
||||
|
||||
1. To agree or be convinced that it's a bug (reporter's responsibility).
|
||||
- A bug is undesired or surprising behavior which violates documentation or the spec.
|
||||
|
||||
2. To be able to understand what is happening (mostly reporter's responsibility).
|
||||
- If the reporter can provide satisfactory instructions such that a developer can reproduce the bug, the developer will likely be able to understand the bug, write a test case, and implement a fix.
|
||||
- Otherwise, the burden is on the reporter to test possible solutions. This is discouraged because it loosens the feedback loop, slows down debugging efforts, obscures the true nature of the problem from the developers, and is unlikely to result in new test cases.
|
||||
|
||||
3. A solution, or ideas toward a solution (mostly maintainer's responsibility).
|
||||
- Sometimes the best solution is a documentation change.
|
||||
- Usually the developers have the best domain knowledge for inventing a solution, but reporters may have ideas or preferences for how they would like the software to work.
|
||||
- Security, correctness, and project goals/vision all take priority over a user's preferences.
|
||||
- It's simply good business to yield a solution that satisfies the users, and it's even better business to leave them impressed.
|
||||
|
||||
Thus, at the very least, the reporter is expected to:
|
||||
|
||||
1. Convince the reader that it's a bug (if it's not obvious).
|
||||
2. Reduce the problem down to the minimum specific steps required to reproduce it.
|
||||
|
||||
The maintainer is usually able to do the rest; but of course the reporter may invest additional effort to speed up the process.
|
||||
|
||||
|
||||
|
||||
### Suggesting features
|
||||
|
||||
First, [search to see if your feature has already been requested](https://github.com/caddyserver/caddy/issues). If it has, you can add a :+1: reaction to vote for it. If your feature idea is new, open an issue to request the feature. Please describe your idea thoroughly so that we know how to implement it! Really vague requests may not be helpful or actionable and, without clarification, will have to be closed.
|
||||
|
||||
While we really do value your requests and implement many of them, not all features are a good fit for Caddy. Most of those [make good plugins](https://github.com/caddyserver/caddy/wiki), which can be made by anyone! But if a feature is not in the best interest of the Caddy project or its users in general, we may politely decline to implement it into Caddy core.
|
||||
|
||||
|
||||
### Improving documentation
|
||||
|
||||
Caddy's documentation is available at [https://caddyserver.com/v1/docs](https://caddyserver.com/v1/docs). If you would like to make a fix to the docs, please submit an issue here describing the change to make.
|
||||
|
||||
Note that plugin documentation is not hosted by the Caddy website, other than basic usage examples. They are managed by the individual plugin authors, and you will have to contact them to change their documentation.
|
||||
|
||||
|
||||
|
||||
## Collaborator Instructions
|
||||
|
||||
Collaborators have push rights to the repository. We grant this permission after one or more successful, high-quality PRs are merged! We thank them for their help.The expectations we have of collaborators are:
|
||||
|
||||
- **Help review pull requests.** Be meticulous, but also kind. We love our contributors, but we critique the contribution to make it better. Multiple, thorough reviews make for the best contributions! Here are some questions to consider:
|
||||
- Can the change be made more elegant?
|
||||
- Is this a maintenance burden?
|
||||
- What assumptions does the code make?
|
||||
- Is it well-tested?
|
||||
- Is the change a good fit for the project?
|
||||
- Does it actually fix the problem or is it creating a special case instead?
|
||||
- Does the change incur any new dependencies? (Avoid these!)
|
||||
|
||||
- **Answer issues.** If every collaborator helped out with issues, we could count the number of open issues on two hands. This means getting involved in the discussion, investigating the code, and yes, debugging it. It's fun. Really! :smile: Please, please help with open issues. Granted, some issues need to be done before others. And of course some are larger than others: you don't have to do it all yourself. Work with other collaborators as a team!
|
||||
|
||||
- **Do not merge pull requests until they have been approved by one or two other collaborators.** If a project owner approves the PR, it can be merged (as long as the conversation has finished too).
|
||||
|
||||
- **Prefer squashed commits over a messy merge.** If there are many little commits, please [squash the commits](https://stackoverflow.com/a/11732910/1048862) so we don't clutter the commit history.
|
||||
|
||||
- **Don't accept new dependencies lightly.** Dependencies can make the world crash and burn, but they are sometimes necessary. Choose carefully. Extremely small dependencies (a few lines of code) can be inlined. The rest may not be needed. For those that are, Caddy vendors all dependencies with the help of [gvt](https://github.com/FiloSottile/gvt). All external dependencies must be vendored, and _Caddy must not export any types defined by those dependencies_. Check this diligently!
|
||||
|
||||
- **Be extra careful in some areas of the code.** There are some critical areas in the Caddy code base that we review extra meticulously: the `caddy` and `caddytls` packages especially.
|
||||
|
||||
- **Make sure tests test the actual thing.** Double-check that the tests fail without the change, and pass with it. It's important that they assert what they're purported to assert.
|
||||
|
||||
- **Recommended reading**
|
||||
- [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments) for an idea of what we look for in good, clean Go code
|
||||
- [Linus Torvalds describes a good commit message](https://gist.github.com/matthewhudson/1475276)
|
||||
- [Best Practices for Maintainers](https://opensource.guide/best-practices/)
|
||||
- [Shrinking Code Review](https://alexgaynor.net/2015/dec/29/shrinking-code-review/)
|
||||
|
||||
|
||||
|
||||
## Values
|
||||
|
||||
- A person is always more important than code. People don't like being handled "efficiently". But we can still process issues and pull requests efficiently while being kind, patient, and considerate.
|
||||
|
||||
- The ends justify the means, if the means are good. A good tree won't produce bad fruit. But if we cut corners or are hasty in our process, the end result will not be good.
|
||||
|
||||
|
||||
## Responsible Disclosure
|
||||
|
||||
If you've found a security vulnerability, please email me, the author, directly: Matthew dot Holt at Gmail. I'll need enough information to verify the bug and make a patch. It will speed things up if you suggest a working patch. If your report is valid and a patch is released, we will not reveal your identity by default. If you wish to be credited, please give me the name to use. Thanks for responsibly helping Caddy—and thousands of websites—be more secure!
|
||||
|
||||
|
||||
## Thank you
|
||||
|
||||
Thanks for your help! Caddy would not be what it is today without your
|
||||
contributions.
|
||||
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [mholt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
+6
-1
@@ -13,5 +13,10 @@ access.log
|
||||
|
||||
/*.conf
|
||||
Caddyfile
|
||||
!caddyfile/
|
||||
|
||||
og_static/
|
||||
og_static/
|
||||
|
||||
.vscode/
|
||||
|
||||
*.bat
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.7.4
|
||||
- tip
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
fast_finish: true
|
||||
|
||||
before_install:
|
||||
# Decrypts a script that installs an authenticated cookie
|
||||
# for git to use when cloning from googlesource.com.
|
||||
# Bypasses "bandwidth limit exceeded" errors.
|
||||
# See github.com/golang/go/issues/12933
|
||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then openssl aes-256-cbc -K $encrypted_3df18f9af81d_key -iv $encrypted_3df18f9af81d_iv -in dist/gitcookie.sh.enc -out dist/gitcookie.sh -d; fi
|
||||
|
||||
install:
|
||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash dist/gitcookie.sh; fi
|
||||
- go get -t ./...
|
||||
- go get github.com/golang/lint/golint
|
||||
- go get github.com/gordonklaus/ineffassign
|
||||
- go get github.com/client9/misspell/cmd/misspell
|
||||
|
||||
script:
|
||||
- diff <(echo -n) <(gofmt -s -d .)
|
||||
- ineffassign .
|
||||
- misspell -error .
|
||||
- go vet ./...
|
||||
- go test -race ./...
|
||||
|
||||
after_script:
|
||||
- golint ./...
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
## Contributing to Caddy
|
||||
|
||||
Welcome! Our community focuses on helping others and making Caddy the best it
|
||||
can be. We gladly accept contributions and encourage you to get involved!
|
||||
|
||||
|
||||
### Join us in the forum
|
||||
|
||||
The [Caddy forum](https://forum.caddyserver.com) is the place for all discussion
|
||||
that doesn't belong in issues or pull requests. Feel free to participate with us!
|
||||
|
||||
If you want to file a bug report or make an improvement to Caddy, however, you
|
||||
should submit an issue or pull request.
|
||||
|
||||
|
||||
### Bug reports
|
||||
|
||||
Please [search this repository](https://github.com/mholt/caddy/search?q=&type=Issues&utf8=%E2%9C%93)
|
||||
with a variety of keywords to ensure your bug is not already reported.
|
||||
|
||||
If unique, [open an issue](https://github.com/mholt/caddy/issues) and answer the
|
||||
questions so we can understand and reproduce the problematic behavior.
|
||||
|
||||
The burden is on you to convince us that it is actually a bug in Caddy. This is
|
||||
easiest to do when you write clear, concise instructions so we can reproduce
|
||||
the behavior (even if it seems obvious). The more detailed and specific you are,
|
||||
the faster we will be able to help you. Check out
|
||||
[How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
|
||||
|
||||
Please be kind. :smile: Remember that Caddy comes at no cost to you, and you're
|
||||
getting free help. If we helped you, please consider
|
||||
[donating](https://caddyserver.com/donate) - it keeps us motivated!
|
||||
|
||||
|
||||
### Minor improvements and new tests
|
||||
|
||||
Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time for
|
||||
minor changes or new tests. Make sure to write tests to assert your change is
|
||||
working properly and is thoroughly covered. We'll ask most pull requests to be
|
||||
[squashed](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html),
|
||||
especially with small commits.
|
||||
|
||||
Your pull request may be thoroughly reviewed. This is because if we accept the
|
||||
PR, we also assume responsibility for it, although we would prefer you to
|
||||
help maintain your code after it gets merged.
|
||||
|
||||
|
||||
### Proposals, suggestions, ideas, new features
|
||||
|
||||
First, please [search](https://github.com/mholt/caddy/search?q=&type=Issues&utf8=%E2%9C%93)
|
||||
with a variety of keywords to ensure your suggestion/proposal is new.
|
||||
|
||||
If so, you may open either an issue or a pull request for discussion and
|
||||
feedback.
|
||||
|
||||
The advantage of issues is that you don't have to spend time implementing your
|
||||
idea, but you should still describe it thoroughly as if someone reading it would
|
||||
implement the whole thing starting from scratch.
|
||||
|
||||
The advantage of pull requests is that we can immediately see the impact the
|
||||
change will have on the project, what the code will look like, and how to
|
||||
improve it. The disadvantage of pull requests is that they are unlikely to get
|
||||
accepted without significant changes first, or it may be rejected entirely.
|
||||
Don't worry, that won't happen without an open discussion first.
|
||||
|
||||
If you are going to spend significant time writing code for a new pull request,
|
||||
best to open an issue to "claim" it and get feedback before you invest a lot of
|
||||
time. Not all pull requests are merged, and that's okay,
|
||||
[Read why.](https://github.com/turbolinks/turbolinks/pull/124#issuecomment-239826060)
|
||||
|
||||
Remember: pull requests should always be thoroughly documented both via godoc
|
||||
and with at least a rough draft of documentation that might go on the website
|
||||
for users to read.
|
||||
|
||||
|
||||
### Collaborator status
|
||||
|
||||
If your pull request is merged, congratulations! You're technically a
|
||||
collaborator. We may also grant you "Collaborator status" which means you can
|
||||
push to the repository and merge other pull requests. We hope that you will
|
||||
stay involved by reviewing pull requests, submitting more of your own, and
|
||||
resolving issues as you are able to. Thanks for making Caddy amazing!
|
||||
|
||||
We ask that collaborators will conduct thorough code reviews and be nice to
|
||||
new contributors. Before merging a PR, it's best to get the approval of
|
||||
at least one or two other collaborators and/or the project owner. We prefer
|
||||
squashed commits instead of many little, semantically-unimportant commits. Also,
|
||||
CI and other post-commit hooks must pass before being merged except in certain
|
||||
unusual circumstances.
|
||||
|
||||
Collaborator status may be removed for inactive users from time to time as
|
||||
we see fit; this is not an insult, just a basic security precaution in case
|
||||
the account becomes inactive or abandoned. Privileges can always be restored
|
||||
later.
|
||||
|
||||
**Reviewing pull requests:** Please help submit and review pull requests as
|
||||
you are able! We would ask that every pull request be reviewed by at least
|
||||
one collaborator who did not open the pull request before merging. This will
|
||||
help ensure high code quality as new collaborators are added to the project.
|
||||
|
||||
Read [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments)
|
||||
on the Go wiki for an idea of what we look for in good, clean Go code.
|
||||
|
||||
|
||||
|
||||
### Vulnerabilities
|
||||
|
||||
If you've found a vulnerability that is serious, please email me: Matthew dot
|
||||
Holt at Gmail. If it's not a big deal, a pull request will probably be faster.
|
||||
|
||||
|
||||
## Thank you
|
||||
|
||||
Thanks for your help! Caddy would not be what it is today without your
|
||||
contributions.
|
||||
@@ -1,24 +0,0 @@
|
||||
(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,68 +1,151 @@
|
||||
<a href="https://caddyserver.com"><img src="https://caddyserver.com/resources/images/caddy-lower.png" alt="Caddy" width="350"></a>
|
||||
<p align="center">
|
||||
<a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></a>
|
||||
</p>
|
||||
<h3 align="center">Every Site on HTTPS <!-- Serve Confidently --></h3>
|
||||
<p align="center">Caddy is a general-purpose HTTP/2 web server that serves HTTPS by default.</p>
|
||||
<p align="center">
|
||||
<a href="https://dev.azure.com/mholt-dev/Caddy/_build?definitionId=5"><img src="https://img.shields.io/azure-devops/build/mholt-dev/afec6074-9842-457f-98cf-69df6adbbf2e/5/master.svg?label=cross-platform%20tests"></a>
|
||||
<a href="https://godoc.org/github.com/caddyserver/caddy"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
|
||||
<a href="https://goreportcard.com/report/caddyserver/caddy"><img src="https://goreportcard.com/badge/github.com/caddyserver/caddy"></a>
|
||||
<br>
|
||||
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
|
||||
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
|
||||
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://caddyserver.com/download">Download</a> ·
|
||||
<a href="https://caddyserver.com/docs">Documentation</a> ·
|
||||
<a href="https://caddy.community">Community</a>
|
||||
</p>
|
||||
|
||||
[](https://forum.caddyserver.com) [](https://twitter.com/caddyserver) [](https://godoc.org/github.com/mholt/caddy) [](https://travis-ci.org/mholt/caddy) [](https://ci.appveyor.com/project/mholt/caddy)
|
||||
[](https://goreportcard.com/report/mholt/caddy)
|
||||
---
|
||||
|
||||
Caddy is a **production-ready** open-source web server that is fast, easy to use, and makes you more productive.
|
||||
|
||||
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.
|
||||
Available for Windows, Mac, Linux, BSD, Solaris, and [Android](https://github.com/caddyserver/caddy/wiki/Running-Caddy-on-Android).
|
||||
|
||||
[Releases](https://github.com/mholt/caddy/releases) ·
|
||||
[User Guide](https://caddyserver.com/docs) ·
|
||||
[Community](https://forum.caddyserver.com)
|
||||
|
||||
Try browsing [the code on Sourcegraph](https://sourcegraph.com/github.com/mholt/caddy)!
|
||||
<p align="center">
|
||||
<b>Thanks to our special sponsor:</b>
|
||||
<br><br>
|
||||
<a href="https://relicabackup.com"><img src="https://caddyserver.com/resources/images/sponsors/relica.png" width="220" alt="Relica - Cross-platform file backup to the cloud, local disks, or other computers"></a>
|
||||
</p>
|
||||
|
||||
## Menu
|
||||
|
||||
- [Features](#features)
|
||||
- [Install](#install)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Running from Source](#running-from-source)
|
||||
- [Running in Production](#running-in-production)
|
||||
- [Contributing](#contributing)
|
||||
- [Donors](#donors)
|
||||
- [About the Project](#about-the-project)
|
||||
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **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
|
||||
- **Easy configuration** with the Caddyfile
|
||||
- **Automatic HTTPS** on by default (via [Let's Encrypt](https://letsencrypt.org))
|
||||
- **HTTP/2** by default
|
||||
- **Virtual hosting** so multiple sites just work
|
||||
- Experimental **QUIC support** for cutting-edge transmissions
|
||||
- TLS session ticket **key rotation** for more secure connections
|
||||
- **Brilliant extensibility** so Caddy can be customized for your needs
|
||||
- **Extensible with plugins** because a convenient web server is a helpful one
|
||||
- **Runs anywhere** with **no external dependencies** (not even libc)
|
||||
|
||||
[See a more complete list of features built into Caddy.](https://caddyserver.com/#features) On top of all those, Caddy does even more with plugins: choose which plugins you want at [download](https://caddyserver.com/download).
|
||||
|
||||
Altogether, Caddy can do things other web servers simply cannot do. Its features and plugins save you time and mistakes, and will cheer you up. Your Caddy instance takes care of the details for you!
|
||||
|
||||
|
||||
<p align="center">
|
||||
<b>Powered by</b>
|
||||
<br>
|
||||
<a href="https://github.com/mholt/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
|
||||
</p>
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
Caddy binaries have no dependencies and are available for every platform. Get Caddy any of these ways:
|
||||
|
||||
- **[Download page](https://caddyserver.com/download)** (RECOMMENDED) allows you to customize your build in the browser
|
||||
- **[Latest release](https://github.com/caddyserver/caddy/releases/latest)** for pre-built, vanilla binaries
|
||||
- **[AWS Marketplace](https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C)** makes it easy to deploy directly to your cloud environment. <a href="https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C" target="_blank">
|
||||
<img src="https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png" alt="Get Caddy on the AWS Marketplace" height="25"/></a>
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
To build from source you need **[Git](https://git-scm.com/downloads)** and **[Go](https://golang.org/doc/install)** (1.13 or newer).
|
||||
|
||||
**To build Caddy without plugins:**
|
||||
|
||||
- Run `go get github.com/caddyserver/caddy/caddy`
|
||||
|
||||
Caddy will be installed to your `$GOPATH/bin` folder.
|
||||
|
||||
With these instructions, the binary will not have embedded version information (see [golang/go#29228](https://github.com/golang/go/issues/29228)), but it is fine for a quick start.
|
||||
|
||||
**To build Caddy with plugins (and with version information):**
|
||||
|
||||
There is no need to modify the Caddy code to build it with plugins. We will create a simple Go module with our own `main()` that you can use to make custom Caddy builds.
|
||||
- Create a new folder anywhere and within create a Go file (with an extension of `.go`, such as `main.go`) with the contents below, adjusting to import the plugins you want to include:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/caddy/caddymain"
|
||||
|
||||
// plug in plugins here, for example:
|
||||
// _ "import/path/here"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// optional: disable telemetry
|
||||
// caddymain.EnableTelemetry = false
|
||||
caddymain.Run()
|
||||
}
|
||||
```
|
||||
3. `go mod init caddy`
|
||||
4. Run `go get github.com/caddyserver/caddy`
|
||||
5. `go install` will then create your binary at `$GOPATH/bin`, or `go build` will put it in the current directory.
|
||||
|
||||
**To install Caddy's source code for development:**
|
||||
|
||||
- Run `git clone https://github.com/caddyserver/caddy.git` in any folder (doesn't have to be in GOPATH).
|
||||
|
||||
You can make changes to the source code from that clone and checkout any commit or tag you wish to develop on.
|
||||
|
||||
When building from source, telemetry is enabled by default. You can disable it by changing `caddymain.EnableTelemetry = false` in run.go, or use the `-disabled-metrics` flag at runtime to disable only certain metrics.
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
Caddy binaries have no dependencies and are available for every platform.
|
||||
Install Caddy any one of these ways:
|
||||
To serve static files from the current working directory, run:
|
||||
|
||||
- **[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`
|
||||
```
|
||||
caddy
|
||||
```
|
||||
|
||||
Once `caddy` is in your PATH, you can `cd` to your website's folder and run
|
||||
`caddy` to serve it. By default, Caddy serves the current directory at
|
||||
[localhost:2015](http://localhost:2015).
|
||||
Caddy's default port is 2015, so open your browser to [http://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:
|
||||
### Go from 0 to HTTPS in 5 seconds
|
||||
|
||||
If the `caddy` binary has permission to bind to low ports and your domain name's DNS records point to the machine you're on:
|
||||
|
||||
```
|
||||
caddy -host example.com
|
||||
```
|
||||
|
||||
This command serves static files from the current directory over HTTPS. Certificates are automatically obtained and renewed for you! Caddy is also automatically configuring ports 80 and 443 for you, and redirecting HTTP to HTTPS. Cool, huh?
|
||||
|
||||
### Customizing your site
|
||||
|
||||
To customize how your site is served, create a file named Caddyfile by your site and paste this into it:
|
||||
|
||||
```plain
|
||||
localhost
|
||||
|
||||
gzip
|
||||
push
|
||||
browse
|
||||
websocket /echo cat
|
||||
ext .html
|
||||
@@ -71,93 +154,65 @@ proxy /api 127.0.0.1:7005
|
||||
header /api Access-Control-Allow-Origin *
|
||||
```
|
||||
|
||||
When you run `caddy` in that directory, it will automatically find and use
|
||||
that Caddyfile to configure itself.
|
||||
When you run `caddy` in that directory, it will automatically find and use that Caddyfile.
|
||||
|
||||
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.
|
||||
This simple file enables server push (via Link headers), allows directory browsing (for folders without an index file), hosts a WebSocket echo server at /echo, serves clean URLs, logs requests to an access log, proxies all API requests to a backend on port 7005, and adds the coveted `Access-Control-Allow-Origin: *` header for all responses from the API.
|
||||
|
||||
Wow! Caddy can do a lot with just a few lines.
|
||||
|
||||
To host multiple sites and do more with the Caddyfile, please see the
|
||||
[Caddyfile documentation](https://caddyserver.com/docs/caddyfile).
|
||||
### Doing more with Caddy
|
||||
|
||||
Note that production sites are served over
|
||||
[HTTPS by default](https://caddyserver.com/docs/automatic-https).
|
||||
To host multiple sites and do more with the Caddyfile, please see the [Caddyfile tutorial](https://caddyserver.com/tutorial/caddyfile).
|
||||
|
||||
Caddy has a command line interface. Run `caddy -h` to view basic help or see
|
||||
the [CLI documentation](https://caddyserver.com/docs/cli) for details.
|
||||
|
||||
**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.7](https://golang.org/dl/)** or newer.
|
||||
|
||||
1. `go get github.com/mholt/caddy/caddy`
|
||||
2. `cd` into your website's directory
|
||||
3. Run `caddy` (assuming `$GOPATH/bin` is in your `$PATH`)
|
||||
|
||||
Caddy's `main()` is in the caddy subfolder. To recompile Caddy, use
|
||||
`build.bash` found in that folder.
|
||||
Sites with qualifying hostnames are served over [HTTPS by default](https://caddyserver.com/docs/automatic-https).
|
||||
|
||||
Caddy has a nice little command line interface. Run `caddy -h` to view basic help or see the [CLI documentation](https://caddyserver.com/docs/cli) for details.
|
||||
|
||||
|
||||
## 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.
|
||||
Caddy is production-ready if you find it to be a good fit for your site and workflow.
|
||||
|
||||
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.
|
||||
**Running as root:** We advise against this. You can still listen on ports < 1024 on Linux using setcap like so: `sudo setcap cap_net_bind_service=+ep ./caddy`
|
||||
|
||||
The Caddy project does not officially maintain any system-specific integrations nor suggest how to administer your own system. But your download file includes [unofficial resources](https://github.com/caddyserver/caddy/tree/master/dist/init) contributed by the community that you may find helpful for running Caddy in production.
|
||||
|
||||
How you choose to run Caddy is up to you. Many users are satisfied with `nohup caddy &`. Others use `screen`. Users who need Caddy to come back up after reboots either do so in the script that caused the reboot, add a command to an init script, or configure a service with their OS.
|
||||
|
||||
If you have questions or concerns about Caddy' underlying crypto implementations, consult Go's [crypto packages](https://golang.org/pkg/crypto), starting with their documentation, then issues, then the code itself; as Caddy uses mainly those libraries.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
**[Join our community](https://forum.caddyserver.com) where you can chat with
|
||||
other Caddy users and developers!**
|
||||
**[Join our forum](https://caddy.community) where you can chat with other Caddy users and developers!** To get familiar with the code base, try [Caddy code search on Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy/)!
|
||||
|
||||
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).
|
||||
Please see our [contributing guidelines](https://github.com/caddyserver/caddy/blob/master/.github/CONTRIBUTING.md) for instructions. If you want to write a plugin, check out the [developer wiki](https://github.com/caddyserver/caddy/wiki).
|
||||
|
||||
We 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)!
|
||||
We use GitHub issues and pull requests only for discussing bug reports and the development of specific changes. We welcome all other topics on the [forum](https://caddy.community)!
|
||||
|
||||
If you want to contribute to the documentation, please submit pull requests to [caddyserver/caddyserver.com](https://github.com/caddyserver/caddyserver.com).
|
||||
If you want to contribute to the documentation, please [submit an issue](https://github.com/caddyserver/caddy/issues/new) describing the change that should be made.
|
||||
|
||||
### Good First Issue
|
||||
|
||||
If you are looking for somewhere to start and would like to help out by working on an existing issue, take a look at our [`Good First Issue`](https://github.com/caddyserver/caddy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag
|
||||
|
||||
Thanks for making Caddy -- and the Web -- better!
|
||||
|
||||
Special thanks to
|
||||
[](https://www.digitalocean.com)
|
||||
for hosting the Caddy project.
|
||||
|
||||
## Donors
|
||||
|
||||
- [DigitalOcean](https://m.do.co/c/6d7bdafccf96) is hosting the Caddy project.
|
||||
- [DNSimple](https://dnsimple.link/resolving-caddy) provides DNS services for Caddy's sites.
|
||||
- [DNS Spy](https://dnsspy.io) keeps an eye on Caddy's DNS properties.
|
||||
|
||||
We thank them for their services. **If you want to help keep Caddy free, please [become a sponsor](https://github.com/sponsors/mholt)!**
|
||||
|
||||
|
||||
## 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](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.
|
||||
|
||||
**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).
|
||||
**The name "Caddy" is trademarked:** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". See [brand guidelines](https://caddyserver.com/brand). Caddy is a registered trademark of Light Code Labs, LLC.
|
||||
|
||||
*Author on Twitter: [@mholt6](https://twitter.com/mholt6)*
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
version: "{build}"
|
||||
|
||||
os: Windows Server 2012 R2
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\mholt\caddy
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.7.4.windows-amd64.zip
|
||||
- 7z x go1.7.4.windows-amd64.zip -y -oC:\ > NUL
|
||||
- go version
|
||||
- go env
|
||||
- go get -t ./...
|
||||
- go get github.com/golang/lint/golint
|
||||
- go get github.com/gordonklaus/ineffassign
|
||||
- set PATH=%GOPATH%\bin;%PATH%
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- go vet ./...
|
||||
- go test -race ./...
|
||||
- ineffassign .
|
||||
|
||||
after_test:
|
||||
- golint ./...
|
||||
|
||||
deploy: off
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
|
||||
+22
-2
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
@@ -11,9 +25,15 @@ func TestAssetsPath(t *testing.T) {
|
||||
t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
|
||||
}
|
||||
|
||||
os.Setenv("CADDYPATH", "testpath")
|
||||
err := os.Setenv("CADDYPATH", "testpath")
|
||||
if err != nil {
|
||||
t.Error("Could not set CADDYPATH")
|
||||
}
|
||||
if actual, expected := AssetsPath(), "testpath"; actual != expected {
|
||||
t.Errorf("Expected path to be %v, got: %v", expected, actual)
|
||||
}
|
||||
os.Setenv("CADDYPATH", "")
|
||||
err = os.Setenv("CADDYPATH", "")
|
||||
if err != nil {
|
||||
t.Error("Could not set CADDYPATH")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Mutilated beyond recognition from the example at:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/go
|
||||
|
||||
trigger:
|
||||
- master
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
linux:
|
||||
imageName: ubuntu-16.04
|
||||
gorootDir: /usr/local
|
||||
mac:
|
||||
imageName: macos-10.13
|
||||
gorootDir: /usr/local
|
||||
windows:
|
||||
imageName: windows-2019
|
||||
gorootDir: C:\
|
||||
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
|
||||
variables:
|
||||
GOROOT: $(gorootDir)/go
|
||||
GOPATH: $(system.defaultWorkingDirectory)/gopath
|
||||
GOBIN: $(GOPATH)/bin
|
||||
modulePath: '$(GOPATH)/src/github.com/$(build.repository.name)'
|
||||
# TODO: modules should be the default in Go 1.13, so this won't be needed
|
||||
GO111MODULE: on
|
||||
|
||||
steps:
|
||||
- bash: |
|
||||
latestGo=$(curl "https://golang.org/VERSION?m=text")
|
||||
echo "##vso[task.setvariable variable=LATEST_GO]$latestGo"
|
||||
echo "Latest Go version: $latestGo"
|
||||
displayName: "Get latest Go version"
|
||||
|
||||
- bash: |
|
||||
sudo rm -f $(which go)
|
||||
echo '##vso[task.prependpath]$(GOBIN)'
|
||||
echo '##vso[task.prependpath]$(GOROOT)/bin'
|
||||
mkdir -p '$(modulePath)'
|
||||
shopt -s extglob
|
||||
shopt -s dotglob
|
||||
mv !(gopath) '$(modulePath)'
|
||||
displayName: Remove old Go, set GOBIN/GOROOT, and move project into GOPATH
|
||||
|
||||
# Install Go (this varies by platform)
|
||||
|
||||
- bash: |
|
||||
wget "https://dl.google.com/go/$(LATEST_GO).linux-amd64.tar.gz"
|
||||
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).linux-amd64.tar.gz"
|
||||
condition: eq( variables['Agent.OS'], 'Linux' )
|
||||
displayName: Install Go on Linux
|
||||
|
||||
- bash: |
|
||||
wget "https://dl.google.com/go/$(LATEST_GO).darwin-amd64.tar.gz"
|
||||
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).darwin-amd64.tar.gz"
|
||||
condition: eq( variables['Agent.OS'], 'Darwin' )
|
||||
displayName: Install Go on macOS
|
||||
|
||||
- powershell: |
|
||||
Write-Host "Downloading Go... (please be patient, I am very slow)"
|
||||
(New-Object System.Net.WebClient).DownloadFile("https://dl.google.com/go/$(LATEST_GO).windows-amd64.zip", "$(LATEST_GO).windows-amd64.zip")
|
||||
Write-Host "Extracting Go... (I'm slow too)"
|
||||
Expand-Archive "$(LATEST_GO).windows-amd64.zip" -DestinationPath "$(gorootDir)"
|
||||
condition: eq( variables['Agent.OS'], 'Windows_NT' )
|
||||
displayName: Install Go on Windows
|
||||
|
||||
# TODO: When this issue is fixed, replace with installer script:
|
||||
# https://github.com/golangci/golangci-lint/issues/472
|
||||
- script: go get -v github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
displayName: Install golangci-lint
|
||||
|
||||
- bash: |
|
||||
printf "Using go at: $(which go)\n"
|
||||
printf "Go version: $(go version)\n"
|
||||
printf "\n\nGo environment:\n\n"
|
||||
go env
|
||||
printf "\n\nSystem environment:\n\n"
|
||||
env
|
||||
displayName: Print Go version and environment
|
||||
|
||||
- script: |
|
||||
go get -v -t -d ./...
|
||||
golangci-lint run -E gofmt -E goimports -E misspell
|
||||
go test -race ./...
|
||||
workingDirectory: '$(modulePath)'
|
||||
displayName: Run tests
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package caddy implements the Caddy server manager.
|
||||
//
|
||||
// To use this package:
|
||||
@@ -5,6 +19,8 @@
|
||||
// 1. Set the AppName and AppVersion variables.
|
||||
// 2. Call LoadCaddyfile() to get the Caddyfile.
|
||||
// Pass in the name of the server type (like "http").
|
||||
// Make sure the server type's package is imported
|
||||
// (import _ "github.com/caddyserver/caddy/caddyhttp").
|
||||
// 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.
|
||||
@@ -15,6 +31,7 @@ package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -26,7 +43,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
"github.com/caddyserver/caddy/caddyfile"
|
||||
"github.com/caddyserver/caddy/telemetry"
|
||||
)
|
||||
|
||||
// Configurable application parameters
|
||||
@@ -49,7 +67,7 @@ var (
|
||||
// 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
|
||||
isUpgrade = os.Getenv("CADDY__UPGRADE") == "1"
|
||||
|
||||
// started will be set to true when the first
|
||||
// instance is started; it never gets set to
|
||||
@@ -60,8 +78,18 @@ var (
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
OnProcessExit = append(OnProcessExit, func() {
|
||||
if PidFile != "" {
|
||||
os.Remove(PidFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Instance contains the state of servers created as a result of
|
||||
// calling Start and can be used to access or control those servers.
|
||||
// It is literally an instance of a server type. Instance values
|
||||
// should NOT be copied. Use *Instance for safety.
|
||||
type Instance struct {
|
||||
// serverType is the name of the instance's server type
|
||||
serverType string
|
||||
@@ -72,18 +100,33 @@ type Instance struct {
|
||||
// wg is used to wait for all servers to shut down
|
||||
wg *sync.WaitGroup
|
||||
|
||||
// context is the context created for this instance.
|
||||
// context is the context created for this instance,
|
||||
// used to coordinate the setting up of the server type
|
||||
context Context
|
||||
|
||||
// servers is the list of servers with their listeners.
|
||||
// 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
|
||||
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
|
||||
OnRestartFailed []func() error // if restart failed
|
||||
OnShutdown []func() error // stopping, even as part of a restart
|
||||
OnFinalShutdown []func() error // stopping, not as part of a restart
|
||||
|
||||
// storing values on an instance is preferable to
|
||||
// global state because these will get garbage-
|
||||
// collected after in-process reloads when the
|
||||
// old instances are destroyed; use StorageMu
|
||||
// to access this value safely
|
||||
Storage map[interface{}]interface{}
|
||||
StorageMu sync.RWMutex
|
||||
}
|
||||
|
||||
// Instances returns the list of instances.
|
||||
func Instances() []*Instance {
|
||||
return instances
|
||||
}
|
||||
|
||||
// Servers returns the ServerListeners in i.
|
||||
@@ -120,13 +163,13 @@ func (i *Instance) Stop() error {
|
||||
// the rest. All the non-nil errors will be returned.
|
||||
func (i *Instance) ShutdownCallbacks() []error {
|
||||
var errs []error
|
||||
for _, shutdownFunc := range i.onShutdown {
|
||||
for _, shutdownFunc := range i.OnShutdown {
|
||||
err := shutdownFunc()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
for _, finalShutdownFunc := range i.onFinalShutdown {
|
||||
for _, finalShutdownFunc := range i.OnFinalShutdown {
|
||||
err := finalShutdownFunc()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
@@ -144,9 +187,28 @@ func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) {
|
||||
i.wg.Add(1)
|
||||
defer i.wg.Done()
|
||||
|
||||
var err error
|
||||
// if something went wrong on restart then run onRestartFailed callbacks
|
||||
defer func() {
|
||||
r := recover()
|
||||
if err != nil || r != nil {
|
||||
for _, fn := range i.OnRestartFailed {
|
||||
if err := fn(); err != nil {
|
||||
log.Printf("[ERROR] Restart failed callback returned error: %v", err)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Restart failed: %v", err)
|
||||
}
|
||||
if r != nil {
|
||||
log.Printf("[PANIC] Restart: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// run restart callbacks
|
||||
for _, fn := range i.onRestart {
|
||||
err := fn()
|
||||
for _, fn := range i.OnRestart {
|
||||
err = fn()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
@@ -179,22 +241,28 @@ func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) {
|
||||
}
|
||||
|
||||
// create new instance; if the restart fails, it is simply discarded
|
||||
newInst := &Instance{serverType: newCaddyfile.ServerType(), wg: i.wg}
|
||||
newInst := &Instance{serverType: newCaddyfile.ServerType(), wg: i.wg, Storage: make(map[interface{}]interface{})}
|
||||
|
||||
// attempt to start new instance
|
||||
err := startWithListenerFds(newCaddyfile, newInst, restartFds)
|
||||
err = startWithListenerFds(newCaddyfile, newInst, restartFds)
|
||||
if err != nil {
|
||||
return i, err
|
||||
return i, fmt.Errorf("starting with listener file descriptors: %v", err)
|
||||
}
|
||||
|
||||
// success! stop the old instance
|
||||
for _, shutdownFunc := range i.onShutdown {
|
||||
err := shutdownFunc()
|
||||
err = i.Stop()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
for _, shutdownFunc := range i.OnShutdown {
|
||||
err = shutdownFunc()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
i.Stop()
|
||||
|
||||
// Execute instantiation events
|
||||
EmitEvent(InstanceStartupEvent, newInst)
|
||||
|
||||
log.Println("[INFO] Reloading complete")
|
||||
|
||||
@@ -208,42 +276,6 @@ 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 {
|
||||
return lnAddr == addr
|
||||
}
|
||||
if lnAddr == net.JoinHostPort("::", port) {
|
||||
return true
|
||||
}
|
||||
if lnAddr == net.JoinHostPort("0.0.0.0", port) {
|
||||
return true
|
||||
}
|
||||
return hostname != "" && lnAddr == addr
|
||||
}
|
||||
|
||||
// TCPServer is a type that can listen and serve connections.
|
||||
// A TCPServer must associate with exactly zero or one net.Listeners.
|
||||
type TCPServer interface {
|
||||
@@ -322,6 +354,11 @@ type GracefulServer interface {
|
||||
// address; you must store the address the
|
||||
// server is to serve on some other way.
|
||||
Address() string
|
||||
|
||||
// WrapListener wraps a listener with the
|
||||
// listener middlewares configured for this
|
||||
// server, if any.
|
||||
WrapListener(net.Listener) net.Listener
|
||||
}
|
||||
|
||||
// Listener is a net.Listener with an underlying file descriptor.
|
||||
@@ -358,6 +395,16 @@ type AfterStartup interface {
|
||||
// is returned. Consequently, this function never returns a nil
|
||||
// value as long as there are no errors.
|
||||
func LoadCaddyfile(serverType string) (Input, error) {
|
||||
// If we are finishing an upgrade, we must obtain the Caddyfile
|
||||
// from our parent process, regardless of configured loaders.
|
||||
if IsUpgrade() {
|
||||
err := gob.NewDecoder(os.Stdin).Decode(&loadedGob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loadedGob.Caddyfile, nil
|
||||
}
|
||||
|
||||
// Ask plugged-in loaders for a Caddyfile
|
||||
cdyfile, err := loadCaddyfileInput(serverType)
|
||||
if err != nil {
|
||||
@@ -422,17 +469,51 @@ func (i *Instance) Caddyfile() Input {
|
||||
//
|
||||
// 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)
|
||||
inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})}
|
||||
err := startWithListenerFds(cdyfile, inst, nil)
|
||||
if err != nil {
|
||||
return inst, err
|
||||
}
|
||||
signalSuccessToParent()
|
||||
if pidErr := writePidFile(); pidErr != nil {
|
||||
log.Printf("[ERROR] Could not write pidfile: %v", pidErr)
|
||||
}
|
||||
|
||||
// Execute instantiation events
|
||||
EmitEvent(InstanceStartupEvent, inst)
|
||||
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartTriple) error {
|
||||
// save this instance in the list now so that
|
||||
// plugins can access it if need be, for example
|
||||
// the caddytls package, so it can perform cert
|
||||
// renewals while starting up; we just have to
|
||||
// remove the instance from the list later if
|
||||
// it fails
|
||||
instancesMu.Lock()
|
||||
instances = append(instances, inst)
|
||||
instancesMu.Unlock()
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
instancesMu.Lock()
|
||||
for i, otherInst := range instances {
|
||||
if otherInst == inst {
|
||||
instances = append(instances[:i], instances[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
instancesMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
if cdyfile == nil {
|
||||
cdyfile = CaddyfileInput{}
|
||||
}
|
||||
|
||||
err := ValidateAndExecuteDirectives(cdyfile, inst, false)
|
||||
err = ValidateAndExecuteDirectives(cdyfile, inst, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -443,16 +524,17 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
|
||||
}
|
||||
|
||||
// run startup callbacks
|
||||
if restartFds == nil {
|
||||
for _, firstStartupFunc := range inst.onFirstStartup {
|
||||
err := firstStartupFunc()
|
||||
if !IsUpgrade() && restartFds == nil {
|
||||
// first startup means not a restart or upgrade
|
||||
for _, firstStartupFunc := range inst.OnFirstStartup {
|
||||
err = firstStartupFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, startupFunc := range inst.onStartup {
|
||||
err := startupFunc()
|
||||
for _, startupFunc := range inst.OnStartup {
|
||||
err = startupFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -463,10 +545,6 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
|
||||
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 {
|
||||
@@ -477,6 +555,11 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
|
||||
}
|
||||
if !Quiet {
|
||||
for _, srvln := range inst.servers {
|
||||
// only show FD notice if the listener is not nil.
|
||||
// This can happen when only serving UDP or TCP
|
||||
if srvln.listener == nil {
|
||||
continue
|
||||
}
|
||||
if !IsLoopback(srvln.listener.Addr().String()) {
|
||||
checkFdlimit()
|
||||
break
|
||||
@@ -498,10 +581,9 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
|
||||
// callbacks will not be executed between directives, since the purpose
|
||||
// is only to check the input for valid syntax.
|
||||
func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bool) error {
|
||||
|
||||
// If parsing only inst will be nil, create an instance for this function call only.
|
||||
if justValidate {
|
||||
inst = &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup)}
|
||||
inst = &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})}
|
||||
}
|
||||
|
||||
stypeName := cdyfile.ServerType()
|
||||
@@ -518,23 +600,25 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo
|
||||
return err
|
||||
}
|
||||
|
||||
inst.context = stype.NewContext()
|
||||
for _, sb := range sblocks {
|
||||
for dir := range sb.Tokens {
|
||||
telemetry.AppendUnique("directives", dir)
|
||||
}
|
||||
}
|
||||
|
||||
inst.context = stype.NewContext(inst)
|
||||
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
|
||||
return fmt.Errorf("error inspecting server blocks: %v", err)
|
||||
}
|
||||
|
||||
err = executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
telemetry.Set("num_server_blocks", len(sblocks))
|
||||
|
||||
return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate)
|
||||
}
|
||||
|
||||
func executeDirectives(inst *Instance, filename string,
|
||||
@@ -607,6 +691,11 @@ func executeDirectives(inst *Instance, filename string,
|
||||
func startServers(serverList []Server, inst *Instance, restartFds map[string]restartTriple) error {
|
||||
errChan := make(chan error, len(serverList))
|
||||
|
||||
// used for signaling to error logging goroutine to terminate
|
||||
stopChan := make(chan struct{})
|
||||
// used to track termination of servers
|
||||
stopWg := &sync.WaitGroup{}
|
||||
|
||||
for _, s := range serverList {
|
||||
var (
|
||||
ln net.Listener
|
||||
@@ -614,6 +703,37 @@ func startServers(serverList []Server, inst *Instance, restartFds map[string]res
|
||||
err error
|
||||
)
|
||||
|
||||
// if performing an upgrade, obtain listener file descriptors
|
||||
// from parent process
|
||||
if IsUpgrade() {
|
||||
if gs, ok := s.(GracefulServer); ok {
|
||||
addr := gs.Address()
|
||||
if fdIndex, ok := loadedGob.ListenerFds["tcp"+addr]; ok {
|
||||
file := os.NewFile(fdIndex, "")
|
||||
ln, err = net.FileListener(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making listener from file: %v", err)
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing copy of listener file: %v", err)
|
||||
}
|
||||
}
|
||||
if fdIndex, ok := loadedGob.ListenerFds["udp"+addr]; ok {
|
||||
file := os.NewFile(fdIndex, "")
|
||||
pc, err = net.FilePacketConn(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making packet connection from file: %v", err)
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing copy of packet connection file: %v", err)
|
||||
}
|
||||
}
|
||||
ln = gs.WrapListener(ln)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -623,71 +743,97 @@ func startServers(serverList []Server, inst *Instance, restartFds map[string]res
|
||||
if old.listener != nil {
|
||||
file, err := old.listener.File()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("getting old listener file: %v", err)
|
||||
}
|
||||
ln, err = net.FileListener(file)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("getting file listener: %v", err)
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing copy of listener file: %v", err)
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
// packetconn
|
||||
if old.packet != nil {
|
||||
file, err := old.packet.File()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("getting old packet file: %v", err)
|
||||
}
|
||||
pc, err = net.FilePacketConn(file)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("getting file packet connection: %v", err)
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("close copy of packet file: %v", err)
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
ln = gs.WrapListener(ln)
|
||||
}
|
||||
}
|
||||
|
||||
if ln == nil {
|
||||
ln, err = s.Listen()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("Listen: %v", err)
|
||||
}
|
||||
}
|
||||
if pc == nil {
|
||||
pc, err = s.ListenPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("ListenPacket: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
inst.servers = append(inst.servers, ServerListener{server: s, listener: ln, packet: pc})
|
||||
}
|
||||
|
||||
for _, s := range inst.servers {
|
||||
inst.wg.Add(2)
|
||||
go func(s Server, ln net.Listener, pc net.PacketConn, inst *Instance) {
|
||||
defer inst.wg.Done()
|
||||
stopWg.Add(2)
|
||||
func(s Server, ln net.Listener, pc net.PacketConn, inst *Instance) {
|
||||
go func() {
|
||||
defer func() {
|
||||
inst.wg.Done()
|
||||
stopWg.Done()
|
||||
}()
|
||||
errChan <- s.Serve(ln)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
errChan <- s.Serve(ln)
|
||||
defer inst.wg.Done()
|
||||
defer func() {
|
||||
inst.wg.Done()
|
||||
stopWg.Done()
|
||||
}()
|
||||
errChan <- s.ServePacket(pc)
|
||||
}()
|
||||
errChan <- s.ServePacket(pc)
|
||||
}(s, ln, pc, inst)
|
||||
|
||||
inst.servers = append(inst.servers, ServerListener{server: s, listener: ln, packet: pc})
|
||||
}(s.server, s.listener, s.packet, inst)
|
||||
}
|
||||
|
||||
// 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
|
||||
for {
|
||||
select {
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
// this error is normal when closing the listener; see https://github.com/golang/go/issues/4373
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "use of closed network connection") {
|
||||
// this error is normal when closing the listener
|
||||
continue
|
||||
}
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
stopWg.Wait()
|
||||
stopChan <- struct{}{}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -737,10 +883,15 @@ func Stop() error {
|
||||
for {
|
||||
instancesMu.Lock()
|
||||
if len(instances) == 0 {
|
||||
instancesMu.Unlock()
|
||||
break
|
||||
}
|
||||
inst := instances[0]
|
||||
instancesMu.Unlock()
|
||||
// Increase the instance waitgroup so that the last wait() call in
|
||||
// caddymain/run.go blocks until this server instance has shut down
|
||||
inst.wg.Add(1)
|
||||
defer inst.wg.Done()
|
||||
if err := inst.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", inst.serverType, err)
|
||||
}
|
||||
@@ -752,7 +903,7 @@ func Stop() error {
|
||||
// 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)
|
||||
host, _, err := net.SplitHostPort(strings.ToLower(addr))
|
||||
if err != nil {
|
||||
host = addr // happens if the addr is just a hostname
|
||||
}
|
||||
@@ -761,23 +912,36 @@ func IsLoopback(addr string) bool {
|
||||
strings.HasPrefix(host, "127.")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// IsInternal returns true if the IP of addr
|
||||
// belongs to a private network IP range. addr must only
|
||||
// be an IP or an IP:port combination.
|
||||
// Loopback addresses are considered false.
|
||||
func IsInternal(addr string) bool {
|
||||
privateNetworks := []string{
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"fc00::/7",
|
||||
}
|
||||
|
||||
// IsUpgrade returns true if this process is part of an upgrade
|
||||
// where a parent caddy process spawned this one to upgrade
|
||||
// the binary.
|
||||
func IsUpgrade() bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return isUpgrade
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
host = addr // happens if the addr is just a hostname, missing port
|
||||
// if we encounter an error, the brackets need to be stripped
|
||||
// because SplitHostPort didn't do it for us
|
||||
host = strings.Trim(host, "[]")
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
for _, privateNetwork := range privateNetworks {
|
||||
_, ipnet, _ := net.ParseCIDR(privateNetwork)
|
||||
if ipnet.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Started returns true if at least one instance has been
|
||||
@@ -867,3 +1031,6 @@ var (
|
||||
// by default if no other file is specified.
|
||||
DefaultConfigFile = "Caddyfile"
|
||||
)
|
||||
|
||||
// CtxKey is a value type for use with context.WithValue.
|
||||
type CtxKey string
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/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}"
|
||||
+416
-65
@@ -1,44 +1,73 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddymain
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyfile"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy/telemetry"
|
||||
"github.com/google/uuid"
|
||||
"github.com/klauspost/cpuid"
|
||||
"github.com/mholt/certmagic"
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
// plug in the HTTP server type
|
||||
_ "github.com/mholt/caddy/caddyhttp"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp" // plug in the HTTP server type
|
||||
// This is where other plugins get plugged in (imported)
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.TrapSignals()
|
||||
setVersion()
|
||||
|
||||
flag.BoolVar(&caddytls.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
|
||||
flag.StringVar(&caddytls.DefaultCAUrl, "ca", "https://acme-v01.api.letsencrypt.org/directory", "URL to certificate authority's ACME server directory")
|
||||
flag.BoolVar(&certmagic.Default.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
|
||||
flag.StringVar(&certmagic.Default.CA, "ca", certmagic.Default.CA, "URL to certificate authority's ACME server directory")
|
||||
flag.StringVar(&certmagic.Default.DefaultServerName, "default-sni", certmagic.Default.DefaultServerName, "If a ClientHello ServerName is empty, use this ServerName to choose a TLS certificate")
|
||||
flag.BoolVar(&certmagic.Default.DisableHTTPChallenge, "disable-http-challenge", certmagic.Default.DisableHTTPChallenge, "Disable the ACME HTTP challenge")
|
||||
flag.BoolVar(&certmagic.Default.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", certmagic.Default.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge")
|
||||
flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable")
|
||||
flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")")
|
||||
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
|
||||
flag.BoolVar(&printEnv, "env", false, "Enable to print environment variables")
|
||||
flag.StringVar(&envFile, "envfile", "", "Path to file with environment variables to load in KEY=VALUE format")
|
||||
flag.BoolVar(&fromJSON, "json-to-caddyfile", false, "From JSON stdin to Caddyfile stdout")
|
||||
flag.BoolVar(&plugins, "plugins", false, "List installed plugins")
|
||||
flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address")
|
||||
flag.DurationVar(&acme.HTTPClient.Timeout, "catimeout", acme.HTTPClient.Timeout, "Default ACME CA HTTP timeout")
|
||||
flag.StringVar(&certmagic.Default.Email, "email", "", "Default ACME CA account email address")
|
||||
flag.DurationVar(&certmagic.HTTPTimeout, "catimeout", certmagic.HTTPTimeout, "Default ACME CA HTTP timeout")
|
||||
flag.StringVar(&logfile, "log", "", "Process log file")
|
||||
flag.BoolVar(&logTimestamps, "log-timestamps", true, "Enable timestamps for the process log")
|
||||
flag.IntVar(&logRollMB, "log-roll-mb", 100, "Roll process log when it reaches this many megabytes (0 to disable rolling)")
|
||||
flag.BoolVar(&logRollCompress, "log-roll-compress", true, "Gzip-compress rolled process log files")
|
||||
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
|
||||
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
|
||||
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
|
||||
flag.StringVar(&serverType, "type", "http", "Type of server to run")
|
||||
flag.BoolVar(&toJSON, "caddyfile-to-json", false, "From Caddyfile stdin to JSON stdout")
|
||||
flag.BoolVar(&version, "version", false, "Show version")
|
||||
flag.BoolVar(&validate, "validate", false, "Parse the Caddyfile but do not start the server")
|
||||
|
||||
@@ -50,9 +79,18 @@ func init() {
|
||||
func Run() {
|
||||
flag.Parse()
|
||||
|
||||
module := getBuildModule()
|
||||
cleanModVersion := strings.TrimPrefix(module.Version, "v")
|
||||
|
||||
caddy.AppName = appName
|
||||
caddy.AppVersion = appVersion
|
||||
acme.UserAgent = appName + "/" + appVersion
|
||||
caddy.AppVersion = module.Version
|
||||
caddy.OnProcessExit = append(caddy.OnProcessExit, certmagic.CleanUpOwnLocks)
|
||||
certmagic.UserAgent = appName + "/" + cleanModVersion
|
||||
|
||||
if !logTimestamps {
|
||||
// Disable timestamps for logging
|
||||
log.SetFlags(0)
|
||||
}
|
||||
|
||||
// Set up process log before anything bad happens
|
||||
switch logfile {
|
||||
@@ -63,12 +101,47 @@ func Run() {
|
||||
case "":
|
||||
log.SetOutput(ioutil.Discard)
|
||||
default:
|
||||
log.SetOutput(&lumberjack.Logger{
|
||||
Filename: logfile,
|
||||
MaxSize: 100,
|
||||
MaxAge: 14,
|
||||
MaxBackups: 10,
|
||||
})
|
||||
if logRollMB > 0 {
|
||||
log.SetOutput(&lumberjack.Logger{
|
||||
Filename: logfile,
|
||||
MaxSize: logRollMB,
|
||||
MaxAge: 14,
|
||||
MaxBackups: 10,
|
||||
Compress: logRollCompress,
|
||||
})
|
||||
} else {
|
||||
err := os.MkdirAll(filepath.Dir(logfile), 0755)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
f, err := os.OpenFile(logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
// don't close file; log should be writeable for duration of process
|
||||
log.SetOutput(f)
|
||||
}
|
||||
}
|
||||
|
||||
// load all additional envs as soon as possible
|
||||
if err := LoadEnvFromFile(envFile); err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
|
||||
if printEnv {
|
||||
for _, v := range os.Environ() {
|
||||
fmt.Println(v)
|
||||
}
|
||||
}
|
||||
|
||||
// initialize telemetry client
|
||||
if EnableTelemetry {
|
||||
err := initTelemetry()
|
||||
if err != nil {
|
||||
mustLogFatalf("[ERROR] Initializing telemetry: %v", err)
|
||||
}
|
||||
} else if disabledMetrics != "" {
|
||||
mustLogFatalf("[ERROR] Cannot disable specific metrics because telemetry is disabled")
|
||||
}
|
||||
|
||||
// Check for one-time actions
|
||||
@@ -81,9 +154,11 @@ func Run() {
|
||||
os.Exit(0)
|
||||
}
|
||||
if version {
|
||||
fmt.Printf("%s %s\n", appName, appVersion)
|
||||
if devBuild && gitShortStat != "" {
|
||||
fmt.Printf("%s\n%s\n", gitShortStat, gitFilesModified)
|
||||
if module.Sum != "" {
|
||||
// a build with a known version will also have a checksum
|
||||
fmt.Printf("Caddy %s (%s)\n", module.Version, module.Sum)
|
||||
} else {
|
||||
fmt.Println(module.Version)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -92,17 +167,17 @@ func Run() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Check if we just need to do a Caddyfile Convert and exit
|
||||
checkJSONCaddyfile()
|
||||
|
||||
// Set CPU cap
|
||||
err := setCPU(cpu)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
|
||||
// Execute plugins that are registered to run as the process starts
|
||||
err = caddy.StartupHooks(serverType)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
// Executes Startup events
|
||||
caddy.EmitEvent(caddy.StartupEvent, nil)
|
||||
|
||||
// Get Caddyfile input
|
||||
caddyfileinput, err := caddy.LoadCaddyfile(serverType)
|
||||
@@ -121,12 +196,35 @@ func Run() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Log Caddy version before start
|
||||
log.Printf("[INFO] Caddy version: %s", module.Version)
|
||||
|
||||
// Start your engines
|
||||
instance, err := caddy.Start(caddyfileinput)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
|
||||
// Begin telemetry (these are no-ops if telemetry disabled)
|
||||
telemetry.Set("caddy_version", module.Version)
|
||||
telemetry.Set("num_listeners", len(instance.Servers()))
|
||||
telemetry.Set("server_type", serverType)
|
||||
telemetry.Set("os", runtime.GOOS)
|
||||
telemetry.Set("arch", runtime.GOARCH)
|
||||
telemetry.Set("cpu", struct {
|
||||
BrandName string `json:"brand_name,omitempty"`
|
||||
NumLogical int `json:"num_logical,omitempty"`
|
||||
AESNI bool `json:"aes_ni,omitempty"`
|
||||
}{
|
||||
BrandName: cpuid.CPU.BrandName,
|
||||
NumLogical: runtime.NumCPU(),
|
||||
AESNI: cpuid.CPU.AesNi(),
|
||||
})
|
||||
if containerized := detectContainer(); containerized {
|
||||
telemetry.Set("container", containerized)
|
||||
}
|
||||
telemetry.StartEmitting()
|
||||
|
||||
// Twiddle your thumbs
|
||||
instance.Wait()
|
||||
}
|
||||
@@ -154,10 +252,18 @@ func confLoader(serverType string) (caddy.Input, error) {
|
||||
return caddy.CaddyfileFromPipe(os.Stdin, serverType)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var contents []byte
|
||||
if strings.Contains(conf, "*") {
|
||||
// Let caddyfile.doImport logic handle the globbed path
|
||||
contents = []byte("import " + conf)
|
||||
} else {
|
||||
var err error
|
||||
contents, err = ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: conf,
|
||||
@@ -181,26 +287,63 @@ func defaultLoader(serverType string) (caddy.Input, error) {
|
||||
}, 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")
|
||||
// getBuildModule returns the build info of Caddy
|
||||
// from debug.BuildInfo (requires Go modules). If
|
||||
// no version information is available, a non-nil
|
||||
// value will still be returned, but with an
|
||||
// unknown version.
|
||||
func getBuildModule() *debug.Module {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if ok {
|
||||
// The recommended way to build Caddy involves
|
||||
// creating a separate main module, which
|
||||
// preserves caddy a read-only dependency
|
||||
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
|
||||
for _, mod := range bi.Deps {
|
||||
if mod.Path == "github.com/caddyserver/caddy" {
|
||||
return mod
|
||||
}
|
||||
}
|
||||
}
|
||||
return &debug.Module{Version: "unknown"}
|
||||
}
|
||||
|
||||
func checkJSONCaddyfile() {
|
||||
if fromJSON {
|
||||
jsonBytes, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Read stdin failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
caddyfileBytes, err := caddyfile.FromJSON(jsonBytes)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Converting from JSON failed: %v", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
fmt.Println(string(caddyfileBytes))
|
||||
os.Exit(0)
|
||||
}
|
||||
if toJSON {
|
||||
caddyfileBytes, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Read stdin failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
jsonBytes, err := caddyfile.ToJSON(caddyfileBytes)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Converting to JSON failed: %v", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
fmt.Println(string(jsonBytes))
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// setCPU parses string cpu and sets GOMAXPROCS
|
||||
// according to its value. It accepts either
|
||||
// a number (e.g. 3) or a percent (e.g. 50%).
|
||||
// If the percent resolves to less than a single
|
||||
// GOMAXPROCS, it rounds it up to GOMAXPROCS=1.
|
||||
func setCPU(cpu string) error {
|
||||
var numCPU int
|
||||
|
||||
@@ -216,6 +359,9 @@ func setCPU(cpu string) error {
|
||||
}
|
||||
percent = float32(pctInt) / 100
|
||||
numCPU = int(float32(availCPU) * percent)
|
||||
if numCPU < 1 {
|
||||
numCPU = 1
|
||||
}
|
||||
} else {
|
||||
// Number
|
||||
num, err := strconv.Atoi(cpu)
|
||||
@@ -233,29 +379,234 @@ func setCPU(cpu string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectContainer attempts to determine whether the process is
|
||||
// being run inside a container. References:
|
||||
// https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/
|
||||
// https://stackoverflow.com/a/20012536/1048862
|
||||
// https://gist.github.com/anantkamath/623ce7f5432680749e087cf8cfba9b69
|
||||
func detectContainer() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
|
||||
file, err := os.Open("/proc/1/cgroup")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
i := 0
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
i++
|
||||
if i > 1000 {
|
||||
return false
|
||||
}
|
||||
|
||||
line := scanner.Text()
|
||||
parts := strings.SplitN(line, ":", 3)
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(parts[2], "docker") ||
|
||||
strings.Contains(parts[2], "lxc") ||
|
||||
strings.Contains(parts[2], "moby") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// initTelemetry initializes the telemetry engine.
|
||||
func initTelemetry() error {
|
||||
uuidFilename := filepath.Join(caddy.AssetsPath(), "uuid")
|
||||
if customUUIDFile := os.Getenv("CADDY_UUID_FILE"); customUUIDFile != "" {
|
||||
uuidFilename = customUUIDFile
|
||||
}
|
||||
|
||||
newUUID := func() uuid.UUID {
|
||||
id := uuid.New()
|
||||
err := os.MkdirAll(caddy.AssetsPath(), 0700)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Persisting instance UUID: %v", err)
|
||||
return id
|
||||
}
|
||||
err = ioutil.WriteFile(uuidFilename, []byte(id.String()), 0600) // human-readable as a string
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Persisting instance UUID: %v", err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
var id uuid.UUID
|
||||
|
||||
// load UUID from storage, or create one if we don't have one
|
||||
if uuidFile, err := os.Open(uuidFilename); os.IsNotExist(err) {
|
||||
// no UUID exists yet; create a new one and persist it
|
||||
id = newUUID()
|
||||
} else if err != nil {
|
||||
log.Printf("[ERROR] Loading persistent UUID: %v", err)
|
||||
id = newUUID()
|
||||
} else {
|
||||
defer uuidFile.Close()
|
||||
uuidBytes, err := ioutil.ReadAll(uuidFile)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Reading persistent UUID: %v", err)
|
||||
id = newUUID()
|
||||
} else {
|
||||
id, err = uuid.ParseBytes(uuidBytes)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Parsing UUID: %v", err)
|
||||
id = newUUID()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse and check the list of disabled metrics
|
||||
var disabledMetricsSlice []string
|
||||
if len(disabledMetrics) > 0 {
|
||||
if len(disabledMetrics) > 1024 {
|
||||
// mitigate disk space exhaustion at the collection endpoint
|
||||
return fmt.Errorf("too many metrics to disable")
|
||||
}
|
||||
disabledMetricsSlice = splitTrim(disabledMetrics, ",")
|
||||
for _, metric := range disabledMetricsSlice {
|
||||
if metric == "instance_id" || metric == "timestamp" || metric == "disabled_metrics" {
|
||||
return fmt.Errorf("instance_id, timestamp, and disabled_metrics cannot be disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initialize telemetry
|
||||
telemetry.Init(id, disabledMetricsSlice)
|
||||
|
||||
// if any metrics were disabled, report which ones (so we know how representative the data is)
|
||||
if len(disabledMetricsSlice) > 0 {
|
||||
telemetry.Set("disabled_metrics", disabledMetricsSlice)
|
||||
log.Printf("[NOTICE] The following telemetry metrics are disabled: %s", disabledMetrics)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Split string s into all substrings separated by sep and returns a slice of
|
||||
// the substrings between those separators.
|
||||
//
|
||||
// If s does not contain sep and sep is not empty, Split returns a
|
||||
// slice of length 1 whose only element is s.
|
||||
//
|
||||
// If sep is empty, Split splits after each UTF-8 sequence. If both s
|
||||
// and sep are empty, Split returns an empty slice.
|
||||
//
|
||||
// Each item that in result is trim space and not empty string
|
||||
func splitTrim(s string, sep string) []string {
|
||||
splitItems := strings.Split(s, sep)
|
||||
trimItems := make([]string, 0, len(splitItems))
|
||||
for _, item := range splitItems {
|
||||
if item = strings.TrimSpace(item); item != "" {
|
||||
trimItems = append(trimItems, item)
|
||||
}
|
||||
}
|
||||
return trimItems
|
||||
}
|
||||
|
||||
// LoadEnvFromFile loads additional envs if file provided and exists
|
||||
// Envs in file should be in KEY=VALUE format
|
||||
func LoadEnvFromFile(envFile string) error {
|
||||
if envFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(envFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
envMap, err := ParseEnvFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := range envMap {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseEnvFile implements parse logic for environment files
|
||||
func ParseEnvFile(envInput io.Reader) (map[string]string, error) {
|
||||
envMap := make(map[string]string)
|
||||
|
||||
scanner := bufio.NewScanner(envInput)
|
||||
var line string
|
||||
lineNumber := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
line = strings.TrimSpace(scanner.Text())
|
||||
lineNumber++
|
||||
|
||||
// skip lines starting with comment
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip empty line
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.SplitN(line, "=", 2)
|
||||
if len(fields) != 2 {
|
||||
return nil, fmt.Errorf("Can't parse line %d; line should be in KEY=VALUE format", lineNumber)
|
||||
}
|
||||
|
||||
if strings.Contains(fields[0], " ") {
|
||||
return nil, fmt.Errorf("Can't parse line %d; KEY contains whitespace", lineNumber)
|
||||
}
|
||||
|
||||
key := fields[0]
|
||||
val := fields[1]
|
||||
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("Can't parse line %d; KEY can't be empty string", lineNumber)
|
||||
}
|
||||
envMap[key] = val
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return envMap, nil
|
||||
}
|
||||
|
||||
const appName = "Caddy"
|
||||
|
||||
// Flags that control program flow or startup
|
||||
var (
|
||||
serverType string
|
||||
conf string
|
||||
cpu string
|
||||
logfile string
|
||||
revoke string
|
||||
version bool
|
||||
plugins bool
|
||||
validate bool
|
||||
serverType string
|
||||
conf string
|
||||
cpu string
|
||||
envFile string
|
||||
fromJSON bool
|
||||
logfile string
|
||||
logTimestamps bool
|
||||
logRollMB int
|
||||
logRollCompress bool
|
||||
revoke string
|
||||
toJSON bool
|
||||
version bool
|
||||
plugins bool
|
||||
printEnv bool
|
||||
validate bool
|
||||
disabledMetrics string
|
||||
)
|
||||
|
||||
// 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
|
||||
)
|
||||
// EnableTelemetry defines whether telemetry is enabled in Run.
|
||||
var EnableTelemetry = true
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddymain
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -27,6 +43,7 @@ func TestSetCPU(t *testing.T) {
|
||||
{"invalid input", currentCPU, true},
|
||||
{"invalid input%", currentCPU, true},
|
||||
{"9999", maxCPU, false}, // over available CPU
|
||||
{"1%", 1, false}, // under a single CPU; assume maxCPU < 100
|
||||
} {
|
||||
err := setCPU(test.input)
|
||||
if test.shouldErr && err == nil {
|
||||
@@ -42,3 +59,60 @@ func TestSetCPU(t *testing.T) {
|
||||
runtime.GOMAXPROCS(currentCPU)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitTrim(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
output []string
|
||||
sep string
|
||||
}{
|
||||
{"os,arch,cpu,caddy_version", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
{"os,arch,cpu,caddy_version,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
{"os,,, arch, cpu, caddy_version,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
{", , os, arch, cpu , caddy_version,, ,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
{"os, ,, arch, cpu , caddy_version,, ,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
} {
|
||||
got := splitTrim(test.input, test.sep)
|
||||
if len(got) != len(test.output) {
|
||||
t.Errorf("Test %d: spliteTrim() = %v, want %v", i, got, test.output)
|
||||
continue
|
||||
}
|
||||
for j, item := range test.output {
|
||||
if item != got[j] {
|
||||
t.Errorf("Test %d: spliteTrim() = %v, want %v", i, got, test.output)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEnvFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{"parsing KEY=VALUE", "PORT=4096", map[string]string{"PORT": "4096"}, false},
|
||||
{"empty KEY", "=4096", nil, true},
|
||||
{"one value", "test", nil, true},
|
||||
{"comments skipped", "#TEST=1\nPORT=8888", map[string]string{"PORT": "8888"}, false},
|
||||
{"empty line", "\nPORT=7777", map[string]string{"PORT": "7777"}, false},
|
||||
{"comments with space skipped", " #TEST=1", map[string]string{}, false},
|
||||
{"KEY with space", "PORT =8888", nil, true},
|
||||
{"only spaces", " ", map[string]string{}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := strings.NewReader(tt.input)
|
||||
got, err := ParseEnvFile(reader)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseEnvFile() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ParseEnvFile() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+16
-2
@@ -1,11 +1,25 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// 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
|
||||
// https://caddy.community/t/my-wish-for-0-9-go-gettable-custom-builds/59?u=matt
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/mholt/caddy/caddy/caddymain"
|
||||
import "github.com/caddyserver/caddy/caddy/caddymain"
|
||||
|
||||
var run = caddymain.Run // replaced for tests
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
+144
-30
@@ -1,9 +1,27 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyfile"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -34,6 +52,77 @@ func TestCaddyStartStop(t *testing.T) {
|
||||
}
|
||||
*/
|
||||
|
||||
// CallbackTestContext implements Context interface
|
||||
type CallbackTestContext struct {
|
||||
// If MakeServersFail is set to true then MakeServers returns an error
|
||||
MakeServersFail bool
|
||||
}
|
||||
|
||||
func (h *CallbackTestContext) InspectServerBlocks(name string, sblock []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
|
||||
return sblock, nil
|
||||
}
|
||||
func (h *CallbackTestContext) MakeServers() ([]Server, error) {
|
||||
if h.MakeServersFail {
|
||||
return make([]Server, 0), fmt.Errorf("MakeServers failed")
|
||||
}
|
||||
return make([]Server, 0), nil
|
||||
}
|
||||
|
||||
func TestCaddyRestartCallbacks(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
restartFail bool
|
||||
expectedCalls []string
|
||||
}{
|
||||
{false, []string{"OnRestart", "OnShutdown"}},
|
||||
{true, []string{"OnRestart", "OnRestartFailed"}},
|
||||
} {
|
||||
serverName := fmt.Sprintf("%v", i)
|
||||
// RegisterServerType to make successful restart possible
|
||||
RegisterServerType(serverName, ServerType{
|
||||
Directives: func() []string { return []string{} },
|
||||
// If MakeServersFail is true then the restart will fail due to context failure
|
||||
NewContext: func(inst *Instance) Context { return &CallbackTestContext{MakeServersFail: test.restartFail} },
|
||||
})
|
||||
c := NewTestController(serverName, "")
|
||||
c.instance = &Instance{
|
||||
serverType: serverName,
|
||||
wg: new(sync.WaitGroup),
|
||||
}
|
||||
|
||||
// Register callbacks which save the calls order
|
||||
calls := make([]string, 0)
|
||||
c.OnRestart(func() error {
|
||||
calls = append(calls, "OnRestart")
|
||||
return nil
|
||||
})
|
||||
c.OnRestartFailed(func() error {
|
||||
calls = append(calls, "OnRestartFailed")
|
||||
return nil
|
||||
})
|
||||
c.OnShutdown(func() error {
|
||||
calls = append(calls, "OnShutdown")
|
||||
return nil
|
||||
})
|
||||
|
||||
_, err := c.instance.Restart(CaddyfileInput{Contents: []byte(""), ServerTypeName: serverName})
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Restart failed: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(calls, test.expectedCalls) {
|
||||
t.Errorf("Test %d: Callbacks expected: %v, got: %v", i, test.expectedCalls, calls)
|
||||
}
|
||||
|
||||
err = c.instance.Stop()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Stop failed: %v", err)
|
||||
}
|
||||
|
||||
c.instance.Wait()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsLoopback(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
@@ -61,38 +150,63 @@ func TestIsLoopback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerAddrEqual(t *testing.T) {
|
||||
ln1, err := net.Listen("tcp", "[::]:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln1.Close()
|
||||
ln1port := strconv.Itoa(ln1.Addr().(*net.TCPAddr).Port)
|
||||
|
||||
ln2, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln2.Close()
|
||||
ln2port := strconv.Itoa(ln2.Addr().(*net.TCPAddr).Port)
|
||||
|
||||
func TestIsInternal(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
ln net.Listener
|
||||
addr string
|
||||
input string
|
||||
expect bool
|
||||
}{
|
||||
{ln1, ":1234", false},
|
||||
{ln1, "0.0.0.0:1234", false},
|
||||
{ln1, "0.0.0.0", false},
|
||||
{ln1, ":" + ln1port, true},
|
||||
{ln1, "0.0.0.0:" + ln1port, true},
|
||||
{ln2, ":" + ln2port, false},
|
||||
{ln2, "127.0.0.1:1234", false},
|
||||
{ln2, "127.0.0.1", false},
|
||||
{ln2, "127.0.0.1:" + ln2port, true},
|
||||
{"9.255.255.255", false},
|
||||
{"10.0.0.0", true},
|
||||
{"10.0.0.1", true},
|
||||
{"10.255.255.254", true},
|
||||
{"10.255.255.255", true},
|
||||
{"11.0.0.0", false},
|
||||
{"10.0.0.5:1234", true},
|
||||
{"11.0.0.5:1234", false},
|
||||
|
||||
{"172.15.255.255", false},
|
||||
{"172.16.0.0", true},
|
||||
{"172.16.0.1", true},
|
||||
{"172.31.255.254", true},
|
||||
{"172.31.255.255", true},
|
||||
{"172.32.0.0", false},
|
||||
{"172.16.0.1:1234", true},
|
||||
|
||||
{"192.167.255.255", false},
|
||||
{"192.168.0.0", true},
|
||||
{"192.168.0.1", true},
|
||||
{"192.168.255.254", true},
|
||||
{"192.168.255.255", true},
|
||||
{"192.169.0.0", false},
|
||||
{"192.168.0.1:1234", true},
|
||||
|
||||
{"fbff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", false},
|
||||
{"fc00::", true},
|
||||
{"fc00::1", true},
|
||||
{"[fc00::1]", true},
|
||||
{"[fc00::1]:8888", true},
|
||||
{"fdff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", true},
|
||||
{"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true},
|
||||
{"fe00::", false},
|
||||
{"fd12:3456:789a:1::1:1234", true},
|
||||
|
||||
{"example.com", false},
|
||||
{"localhost", false},
|
||||
{"localhost:1234", false},
|
||||
{"localhost:", false},
|
||||
{"127.0.0.1", false},
|
||||
{"127.0.0.1:443", false},
|
||||
{"127.0.1.5", false},
|
||||
{"12.7.0.1", false},
|
||||
{"[::1]", false},
|
||||
{"[::1]:1234", false},
|
||||
{"::1", false},
|
||||
{"::", false},
|
||||
{"[::]", false},
|
||||
{"local", false},
|
||||
} {
|
||||
if got, want := listenerAddrEqual(test.ln, test.addr), test.expect; got != want {
|
||||
t.Errorf("Test %d (%s == %s): expected %v but was %v", i, test.addr, test.ln.Addr().String(), want, got)
|
||||
if got, want := IsInternal(test.input), test.expect; got != want {
|
||||
t.Errorf("Test %d (%s): expected %v but was %v", i, test.input, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-8
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
@@ -120,12 +134,6 @@ func (d *Dispenser) NextBlock() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IncrNest adds a level of nesting to the dispenser.
|
||||
func (d *Dispenser) IncrNest() {
|
||||
d.nesting++
|
||||
return
|
||||
}
|
||||
|
||||
// Val gets the text of the current token. If there is no token
|
||||
// loaded, it returns empty string.
|
||||
func (d *Dispenser) Val() string {
|
||||
@@ -216,9 +224,9 @@ func (d *Dispenser) EOFErr() error {
|
||||
return d.Errf("Unexpected EOF")
|
||||
}
|
||||
|
||||
// Err generates a custom parse error with a message of msg.
|
||||
// Err generates a custom parse-time error with a message of msg.
|
||||
func (d *Dispenser) Err(msg string) error {
|
||||
msg = fmt.Sprintf("%s:%d - Parse error: %s", d.File(), d.Line(), msg)
|
||||
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
@@ -34,6 +48,9 @@ func (l *lexer) load(input io.Reader) error {
|
||||
// discard byte order mark, if present
|
||||
firstCh, _, err := l.reader.ReadRune()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if firstCh != 0xFEFF {
|
||||
|
||||
+18
-1
@@ -1,6 +1,21 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -144,7 +159,9 @@ func TestLexer(t *testing.T) {
|
||||
|
||||
func tokenize(input string) (tokens []Token) {
|
||||
l := lexer{}
|
||||
l.load(strings.NewReader(input))
|
||||
if err := l.load(strings.NewReader(input)); err != nil {
|
||||
log.Printf("[ERROR] load failed: %v", err)
|
||||
}
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
|
||||
+134
-60
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
@@ -40,6 +54,7 @@ type parser struct {
|
||||
block ServerBlock // current server block being parsed
|
||||
validDirectives []string // a directive must be valid or it's an error
|
||||
eof bool // if we encounter a valid EOF in a hard place
|
||||
definedSnippets map[string][]Token
|
||||
}
|
||||
|
||||
func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||
@@ -81,6 +96,24 @@ func (p *parser) begin() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ok, name := p.isSnippet(); ok {
|
||||
if p.definedSnippets == nil {
|
||||
p.definedSnippets = map[string][]Token{}
|
||||
}
|
||||
if _, found := p.definedSnippets[name]; found {
|
||||
return p.Errf("redeclaration of previously declared snippet %s", name)
|
||||
}
|
||||
// consume all tokens til matched close brace
|
||||
tokens, err := p.snippetTokens()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.definedSnippets[name] = tokens
|
||||
// empty block keys so we don't save this block as a real server.
|
||||
p.block.Keys = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.blockContents()
|
||||
}
|
||||
|
||||
@@ -207,69 +240,57 @@ func (p *parser) doImport() error {
|
||||
if p.NextArg() {
|
||||
return p.Err("Import takes only one argument (glob pattern or file)")
|
||||
}
|
||||
|
||||
// make path relative to Caddyfile rather than current working directory (issue #867)
|
||||
// and then use glob to get list of matching filenames
|
||||
absFile, err := filepath.Abs(p.Dispenser.filename)
|
||||
if err != nil {
|
||||
return p.Errf("Failed to get absolute path of file: %s", p.Dispenser.filename)
|
||||
}
|
||||
|
||||
var matches []string
|
||||
var globPattern string
|
||||
if !filepath.IsAbs(importPattern) {
|
||||
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
|
||||
} else {
|
||||
globPattern = importPattern
|
||||
}
|
||||
matches, err = filepath.Glob(globPattern)
|
||||
|
||||
if err != nil {
|
||||
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
if strings.Contains(globPattern, "*") {
|
||||
log.Printf("[WARNING] No files matching import pattern: %s", importPattern)
|
||||
} else {
|
||||
return p.Errf("File to import not found: %s", importPattern)
|
||||
}
|
||||
}
|
||||
|
||||
// splice out the import directive and its argument (2 tokens total)
|
||||
tokensBefore := p.tokens[:p.cursor-1]
|
||||
tokensAfter := p.tokens[p.cursor+1:]
|
||||
|
||||
// collect all the imported tokens
|
||||
var importedTokens []Token
|
||||
for _, importFile := range matches {
|
||||
newTokens, err := p.doSingleImport(importFile)
|
||||
|
||||
// first check snippets. That is a simple, non-recursive replacement
|
||||
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
|
||||
importedTokens = p.definedSnippets[importPattern]
|
||||
} else {
|
||||
// make path relative to the file of the _token_ being processed rather
|
||||
// than current working directory (issue #867) and then use glob to get
|
||||
// list of matching filenames
|
||||
absFile, err := filepath.Abs(p.Dispenser.File())
|
||||
if err != nil {
|
||||
return err
|
||||
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, 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(token.Text) {
|
||||
abs = token.Text
|
||||
} else if !filepath.IsAbs(importFile) {
|
||||
abs = filepath.Join(filepath.Dir(absFile), token.Text)
|
||||
} else {
|
||||
abs = filepath.Join(importDir, token.Text)
|
||||
}
|
||||
newTokens[i] = Token{
|
||||
Text: abs,
|
||||
Line: token.Line,
|
||||
File: token.File,
|
||||
}
|
||||
|
||||
var matches []string
|
||||
var globPattern string
|
||||
if !filepath.IsAbs(importPattern) {
|
||||
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
|
||||
} else {
|
||||
globPattern = importPattern
|
||||
}
|
||||
if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 ||
|
||||
(strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) {
|
||||
// See issue #2096 - a pattern with many glob expansions can hang for too long
|
||||
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
|
||||
}
|
||||
matches, err = filepath.Glob(globPattern)
|
||||
|
||||
if err != nil {
|
||||
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
if strings.ContainsAny(globPattern, "*?[]") {
|
||||
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
|
||||
} else {
|
||||
return p.Errf("File to import not found: %s", importPattern)
|
||||
}
|
||||
}
|
||||
importedTokens = append(importedTokens, newTokens...)
|
||||
|
||||
// collect all the imported tokens
|
||||
|
||||
for _, importFile := range matches {
|
||||
newTokens, err := p.doSingleImport(importFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
importedTokens = append(importedTokens, newTokens...)
|
||||
}
|
||||
}
|
||||
|
||||
// splice the imported tokens in the place of the import statement
|
||||
@@ -300,8 +321,12 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
|
||||
}
|
||||
|
||||
// Tack the filename onto these tokens so errors show the imported file's name
|
||||
filename := filepath.Base(importFile)
|
||||
// Tack the file path onto these tokens so errors show the imported file's name
|
||||
// (we use full, absolute path to avoid bugs: issue #1892)
|
||||
filename, err := filepath.Abs(importFile)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
|
||||
}
|
||||
for i := 0; i < len(importedTokens); i++ {
|
||||
importedTokens[i].File = filename
|
||||
}
|
||||
@@ -316,7 +341,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
// are loaded into the current server block for later use
|
||||
// by directive setup functions.
|
||||
func (p *parser) directive() error {
|
||||
dir := p.Val()
|
||||
dir := replaceEnvVars(p.Val())
|
||||
nesting := 0
|
||||
|
||||
// TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type")
|
||||
@@ -337,6 +362,12 @@ func (p *parser) directive() error {
|
||||
nesting--
|
||||
} else if p.Val() == "}" && nesting == 0 {
|
||||
return p.Err("Unexpected '}' because no matching opening brace")
|
||||
} else if p.Val() == "import" && p.isNewLine() {
|
||||
if err := p.doImport(); err != nil {
|
||||
return err
|
||||
}
|
||||
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||
continue
|
||||
}
|
||||
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
@@ -396,8 +427,13 @@ func replaceEnvVars(s string) string {
|
||||
func replaceEnvReferences(s, refStart, refEnd string) string {
|
||||
index := strings.Index(s, refStart)
|
||||
for index != -1 {
|
||||
endIndex := strings.Index(s, refEnd)
|
||||
if endIndex != -1 {
|
||||
endIndex := strings.Index(s[index:], refEnd)
|
||||
if endIndex == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
endIndex += index
|
||||
if endIndex > index+len(refStart) {
|
||||
ref := s[index : endIndex+len(refEnd)]
|
||||
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
|
||||
} else {
|
||||
@@ -414,3 +450,41 @@ type ServerBlock struct {
|
||||
Keys []string
|
||||
Tokens map[string][]Token
|
||||
}
|
||||
|
||||
func (p *parser) isSnippet() (bool, string) {
|
||||
keys := p.block.Keys
|
||||
// A snippet block is a single key with parens. Nothing else qualifies.
|
||||
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") {
|
||||
return true, strings.TrimSuffix(keys[0][1:], ")")
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// read and store everything in a block for later replay.
|
||||
func (p *parser) snippetTokens() ([]Token, error) {
|
||||
// TODO: disallow imports in snippets for simplicity at import time
|
||||
// snippet must have curlies.
|
||||
err := p.openCurlyBrace()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count := 1
|
||||
tokens := []Token{}
|
||||
for p.Next() {
|
||||
if p.Val() == "}" {
|
||||
count--
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if p.Val() == "{" {
|
||||
count++
|
||||
}
|
||||
tokens = append(tokens, p.tokens[p.cursor])
|
||||
}
|
||||
// make sure we're matched up
|
||||
if count != 0 {
|
||||
return nil, p.SyntaxErr("}")
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
+257
-13
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
@@ -9,21 +23,47 @@ import (
|
||||
)
|
||||
|
||||
func TestAllTokens(t *testing.T) {
|
||||
input := strings.NewReader("a b c\nd e")
|
||||
expected := []string{"a", "b", "c", "d", "e"}
|
||||
tokens, err := allTokens(input)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if len(tokens) != len(expected) {
|
||||
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "not-empty",
|
||||
input: "a b c\nd e",
|
||||
expected: []string{"a", "b", "c", "d", "e"},
|
||||
}, {
|
||||
name: "empty",
|
||||
input: "",
|
||||
}, {
|
||||
name: "newline",
|
||||
input: "\n",
|
||||
}, {
|
||||
name: "space",
|
||||
input: " ",
|
||||
}, {
|
||||
name: "tab and newline",
|
||||
input: "\t\n",
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tokens, err := allTokens(strings.NewReader(tt.input))
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if len(tokens) != len(tt.expected) {
|
||||
t.Fatalf("Expected %d tokens, got %d", len(tt.expected), len(tokens))
|
||||
}
|
||||
|
||||
for i, val := range tt.expected {
|
||||
if tokens[i].Text != val {
|
||||
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +254,17 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
{`""`, false, []string{}, map[string]int{}},
|
||||
|
||||
{``, false, []string{}, map[string]int{}},
|
||||
|
||||
// test cases found by fuzzing!
|
||||
{`import }{$"`, true, []string{}, map[string]int{}},
|
||||
{`import /*/*.txt`, true, []string{}, map[string]int{}},
|
||||
{`import /???/?*?o`, true, []string{}, map[string]int{}},
|
||||
{`import /??`, true, []string{}, map[string]int{}},
|
||||
{`import /[a-z]`, true, []string{}, map[string]int{}},
|
||||
{`import {$}`, true, []string{}, map[string]int{}},
|
||||
{`import {%}`, true, []string{}, map[string]int{}},
|
||||
{`import {$$}`, true, []string{}, map[string]int{}},
|
||||
{`import {%%}`, true, []string{}, map[string]int{}},
|
||||
} {
|
||||
result, err := testParseOne(test.input)
|
||||
|
||||
@@ -346,6 +397,68 @@ func TestRecursiveImport(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectiveImport(t *testing.T) {
|
||||
testParseOne := func(input string) (ServerBlock, error) {
|
||||
p := testParser(input)
|
||||
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||
err := p.parseOne()
|
||||
return p.block, err
|
||||
}
|
||||
|
||||
isExpected := func(got ServerBlock) bool {
|
||||
if len(got.Keys) != 1 || got.Keys[0] != "localhost" {
|
||||
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
|
||||
return false
|
||||
}
|
||||
if len(got.Tokens) != 2 {
|
||||
t.Errorf("got wrong number of tokens: expect 2, got %d", len(got.Tokens))
|
||||
return false
|
||||
}
|
||||
if len(got.Tokens["dir1"]) != 1 || len(got.Tokens["proxy"]) != 8 {
|
||||
t.Errorf("got unexpect tokens: %v", got.Tokens)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
directiveFile, err := filepath.Abs("testdata/directive_import_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(directiveFile, []byte(`prop1 1
|
||||
prop2 2`), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(directiveFile)
|
||||
|
||||
// import from existing file
|
||||
result, err := testParseOne(`localhost
|
||||
dir1
|
||||
proxy {
|
||||
import testdata/directive_import_test
|
||||
transparent
|
||||
}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !isExpected(result) {
|
||||
t.Error("directive import failed")
|
||||
}
|
||||
|
||||
// import from nonexistent file
|
||||
_, err = testParseOne(`localhost
|
||||
dir1
|
||||
proxy {
|
||||
import testdata/nonexistent_file
|
||||
transparent
|
||||
}`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when importing a nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAll(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
@@ -427,6 +540,7 @@ func TestEnvironmentReplacement(t *testing.T) {
|
||||
os.Setenv("PORT", "8080")
|
||||
os.Setenv("ADDRESS", "servername.com")
|
||||
os.Setenv("FOOBAR", "foobar")
|
||||
os.Setenv("PARTIAL_DIR", "r1")
|
||||
|
||||
// basic test; unix-style env vars
|
||||
p := testParser(`{$ADDRESS}`)
|
||||
@@ -435,6 +549,13 @@ func TestEnvironmentReplacement(t *testing.T) {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// basic test; unix-style env vars
|
||||
p = testParser(`di{$PARTIAL_DIR}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "dir1"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// multiple vars per token
|
||||
p = testParser(`{$ADDRESS}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
@@ -493,6 +614,13 @@ func TestEnvironmentReplacement(t *testing.T) {
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "Test foobar test"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// after end token
|
||||
p = testParser(":1234\nanswer \"{{ .Name }} {$FOOBAR}\"")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["answer"][1].Text, "{{ .Name }} foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
@@ -500,3 +628,119 @@ func testParser(input string) parser {
|
||||
p := parser{Dispenser: NewDispenser("Caddyfile", buf)}
|
||||
return p
|
||||
}
|
||||
|
||||
func TestSnippets(t *testing.T) {
|
||||
p := testParser(`
|
||||
(common) {
|
||||
gzip foo
|
||||
errors stderr
|
||||
}
|
||||
http://example.com {
|
||||
import common
|
||||
}
|
||||
`)
|
||||
blocks, err := p.parseAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
t.Log(b.Keys)
|
||||
t.Log(b.Tokens)
|
||||
}
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
|
||||
}
|
||||
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
|
||||
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if len(blocks[0].Tokens) != 2 {
|
||||
t.Fatalf("Server block should have tokens from import")
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["errors"][1].Text, "stderr"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
|
||||
file, err := ioutil.TempFile("", t.Name())
|
||||
if err != nil {
|
||||
panic(err) // get a stack trace so we know where this was called from.
|
||||
}
|
||||
if _, err := file.WriteString(str); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return file.Name()
|
||||
}
|
||||
|
||||
func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
|
||||
fileName := writeStringToTempFileOrDie(t, `
|
||||
http://example.com {
|
||||
# This isn't an import directive, it's just an arg with value 'import'
|
||||
basicauth / import password
|
||||
}
|
||||
`)
|
||||
// Parse the root file that imports the other one.
|
||||
p := testParser(`import ` + fileName)
|
||||
blocks, err := p.parseAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
t.Log(b.Keys)
|
||||
t.Log(b.Tokens)
|
||||
}
|
||||
auth := blocks[0].Tokens["basicauth"]
|
||||
line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text
|
||||
if line != "basicauth / import password" {
|
||||
// Previously, it would be changed to:
|
||||
// basicauth / import /path/to/test/dir/password
|
||||
// referencing a file that (probably) doesn't exist and changing the
|
||||
// password!
|
||||
t.Errorf("Expected basicauth tokens to be 'basicauth / import password' but got %#q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnippetAcrossMultipleFiles(t *testing.T) {
|
||||
// Make the derived Caddyfile that expects (common) to be defined.
|
||||
fileName := writeStringToTempFileOrDie(t, `
|
||||
http://example.com {
|
||||
import common
|
||||
}
|
||||
`)
|
||||
|
||||
// Parse the root file that defines (common) and then imports the other one.
|
||||
p := testParser(`
|
||||
(common) {
|
||||
gzip foo
|
||||
}
|
||||
import ` + fileName + `
|
||||
`)
|
||||
|
||||
blocks, err := p.parseAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
t.Log(b.Keys)
|
||||
t.Log(b.Tokens)
|
||||
}
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
|
||||
}
|
||||
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
|
||||
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if len(blocks[0].Tokens) != 1 {
|
||||
t.Fatalf("Server block should have tokens from import")
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package basicauth implements HTTP Basic Authentication for Caddy.
|
||||
//
|
||||
// This is useful for simple protections on a website, like requiring
|
||||
@@ -7,18 +21,20 @@ package basicauth
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/jimstudt/http-authentication/basic"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// BasicAuth is middleware to protect resources with a username and password.
|
||||
@@ -35,6 +51,16 @@ type BasicAuth struct {
|
||||
// ServeHTTP implements the httpserver.Handler interface.
|
||||
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var protected, isAuthenticated bool
|
||||
var realm string
|
||||
var username string
|
||||
var password string
|
||||
var ok bool
|
||||
|
||||
// do not check for basic auth on OPTIONS call
|
||||
if r.Method == http.MethodOptions {
|
||||
// Pass-through when no paths match
|
||||
return a.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
for _, rule := range a.Rules {
|
||||
for _, res := range rule.Resources {
|
||||
@@ -44,9 +70,10 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
|
||||
|
||||
// path matches; this endpoint is protected
|
||||
protected = true
|
||||
realm = rule.Realm
|
||||
|
||||
// parse auth header
|
||||
username, password, ok := r.BasicAuth()
|
||||
username, password, ok = r.BasicAuth()
|
||||
|
||||
// check credentials
|
||||
if !ok ||
|
||||
@@ -58,8 +85,14 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
|
||||
// by this point, authentication was successful
|
||||
isAuthenticated = true
|
||||
|
||||
// remove credentials from request to avoid leaking upstream
|
||||
r.Header.Del("Authorization")
|
||||
// let upstream middleware (e.g. fastcgi and cgi) know about authenticated
|
||||
// user; this replaces the request with a wrapped instance
|
||||
r = r.WithContext(context.WithValue(r.Context(),
|
||||
httpserver.RemoteUserCtxKey, username))
|
||||
|
||||
// Provide username to be used in log by replacer
|
||||
repl := httpserver.NewReplacer(r, nil, "-")
|
||||
repl.Set("user", username)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +100,17 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
|
||||
// browsers show a message that says something like:
|
||||
// "The website says: <realm>"
|
||||
// which is kinda dumb, but whatever.
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"Restricted\"")
|
||||
return http.StatusUnauthorized, nil
|
||||
if realm == "" {
|
||||
realm = "Restricted"
|
||||
}
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\""+realm+"\"")
|
||||
|
||||
// Get a replacer so we can provide basic info for the authentication error.
|
||||
repl := httpserver.NewReplacer(r, nil, "-")
|
||||
repl.Set("user", username)
|
||||
errstr := repl.Replace("BasicAuth: user \"{user}\" was not found or password was incorrect. {remote} {host} {uri} {proto}")
|
||||
err := fmt.Errorf("%s", errstr)
|
||||
return http.StatusUnauthorized, err
|
||||
}
|
||||
|
||||
// Pass-through when no paths match
|
||||
@@ -82,6 +124,7 @@ type Rule struct {
|
||||
Username string
|
||||
Password func(string) bool
|
||||
Resources []string
|
||||
Realm string // See RFC 1945 and RFC 2617, default: "Restricted"
|
||||
}
|
||||
|
||||
// PasswordMatcher determines whether a password matches a rule.
|
||||
@@ -151,11 +194,15 @@ 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))
|
||||
if _, err := passwHash.Write([]byte(passw)); err != nil {
|
||||
log.Printf("[ERROR] unable to write password hash: %v", err)
|
||||
}
|
||||
passwSum := passwHash.Sum(nil)
|
||||
return func(pw string) bool {
|
||||
pwHash := sha1.New()
|
||||
pwHash.Write([]byte(pw))
|
||||
if _, err := pwHash.Write([]byte(pw)); err != nil {
|
||||
log.Printf("[ERROR] unable to write password hash: %v", err)
|
||||
}
|
||||
pwSum := pwHash.Sum(nil)
|
||||
return subtle.ConstantTimeCompare([]byte(pwSum), []byte(passwSum)) == 1
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
@@ -8,63 +22,97 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
rw := BasicAuth{
|
||||
Next: httpserver.HandlerFunc(contentHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "okuser", Password: PlainMatcher("okpass"), Resources: []string{"/testing"}},
|
||||
var i int
|
||||
// This handler is registered for tests in which the only authorized user is
|
||||
// "okuser"
|
||||
upstreamHandler := func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string)
|
||||
if remoteUser != "okuser" {
|
||||
t.Errorf("Test %d: expecting remote user 'okuser', got '%s'", i, remoteUser)
|
||||
}
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
rws := []BasicAuth{
|
||||
{
|
||||
Next: httpserver.HandlerFunc(upstreamHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "okuser", Password: PlainMatcher("okpass"),
|
||||
Resources: []string{"/testing"}, Realm: "Resources"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Next: httpserver.HandlerFunc(upstreamHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "okuser", Password: PlainMatcher("okpass"),
|
||||
Resources: []string{"/testing"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
type testType struct {
|
||||
from string
|
||||
result int
|
||||
user string
|
||||
password string
|
||||
}{
|
||||
{"/testing", http.StatusOK, "okuser", "okpass"},
|
||||
{"/testing", http.StatusUnauthorized, "baduser", "okpass"},
|
||||
{"/testing", http.StatusUnauthorized, "okuser", "badpass"},
|
||||
{"/testing", http.StatusUnauthorized, "OKuser", "okpass"},
|
||||
{"/testing", http.StatusUnauthorized, "OKuser", "badPASS"},
|
||||
{"/testing", http.StatusUnauthorized, "", "okpass"},
|
||||
{"/testing", http.StatusUnauthorized, "okuser", ""},
|
||||
{"/testing", http.StatusUnauthorized, "", ""},
|
||||
haserror bool
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
req, err := http.NewRequest("GET", test.from, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not create HTTP request: %v", i, err)
|
||||
}
|
||||
req.SetBasicAuth(test.user, test.password)
|
||||
tests := []testType{
|
||||
{"/testing", http.StatusOK, "okuser", "okpass", false},
|
||||
{"/testing", http.StatusUnauthorized, "baduser", "okpass", true},
|
||||
{"/testing", http.StatusUnauthorized, "okuser", "badpass", true},
|
||||
{"/testing", http.StatusUnauthorized, "OKuser", "okpass", true},
|
||||
{"/testing", http.StatusUnauthorized, "OKuser", "badPASS", true},
|
||||
{"/testing", http.StatusUnauthorized, "", "okpass", true},
|
||||
{"/testing", http.StatusUnauthorized, "okuser", "", true},
|
||||
{"/testing", http.StatusUnauthorized, "", "", true},
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP: %v", i, err)
|
||||
var test testType
|
||||
for _, rw := range rws {
|
||||
expectRealm := rw.Rules[0].Realm
|
||||
if expectRealm == "" {
|
||||
expectRealm = "Restricted" // Default if Realm not specified in rule
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("Test %d: Expected status code %d but was %d",
|
||||
i, test.result, result)
|
||||
}
|
||||
if test.result == http.StatusUnauthorized {
|
||||
headers := rec.Header()
|
||||
if val, ok := headers["Www-Authenticate"]; ok {
|
||||
if got, want := val[0], "Basic realm=\"Restricted\""; got != want {
|
||||
t.Errorf("Test %d: Www-Authenticate header should be '%s', got: '%s'", i, want, got)
|
||||
for i, test = range tests {
|
||||
req, err := http.NewRequest("GET", test.from, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not create HTTP request: %v", i, err)
|
||||
}
|
||||
req.SetBasicAuth(test.user, test.password)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
if !test.haserror || !strings.HasPrefix(err.Error(), "BasicAuth: user") {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP: %v", i, err)
|
||||
}
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("Test %d: Expected status code %d but was %d",
|
||||
i, test.result, result)
|
||||
}
|
||||
if test.result == http.StatusUnauthorized {
|
||||
headers := rec.Header()
|
||||
if val, ok := headers["Www-Authenticate"]; ok {
|
||||
if got, want := val[0], "Basic realm=\""+expectRealm+"\""; got != want {
|
||||
t.Errorf("Test %d: Www-Authenticate header should be '%s', got: '%s'", i, want, got)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Test %d: response should have a 'Www-Authenticate' header", i)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Test %d: response should have a 'Www-Authenticate' header", i)
|
||||
}
|
||||
} else {
|
||||
if got, want := req.Header.Get("Authorization"), ""; got != want {
|
||||
t.Errorf("Test %d: Expected Authorization header to be stripped from request after successful authentication, but is: %s", i, got)
|
||||
if req.Header.Get("Authorization") == "" {
|
||||
// see issue #1508: https://github.com/caddyserver/caddy/issues/1508
|
||||
t.Errorf("Test %d: Expected Authorization header to be retained after successful auth, but was empty", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,16 +128,17 @@ func TestMultipleOverlappingRules(t *testing.T) {
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
from string
|
||||
result int
|
||||
cred string
|
||||
from string
|
||||
result int
|
||||
cred string
|
||||
haserror bool
|
||||
}{
|
||||
{"/t", http.StatusOK, "t:p1"},
|
||||
{"/t/t", http.StatusOK, "t:p1"},
|
||||
{"/t/t", http.StatusOK, "t1:p2"},
|
||||
{"/a", http.StatusOK, "t1:p2"},
|
||||
{"/t/t", http.StatusUnauthorized, "t1:p3"},
|
||||
{"/t", http.StatusUnauthorized, "t1:p2"},
|
||||
{"/t", http.StatusOK, "t:p1", false},
|
||||
{"/t/t", http.StatusOK, "t:p1", false},
|
||||
{"/t/t", http.StatusOK, "t1:p2", false},
|
||||
{"/a", http.StatusOK, "t1:p2", false},
|
||||
{"/t/t", http.StatusUnauthorized, "t1:p3", true},
|
||||
{"/t", http.StatusUnauthorized, "t1:p2", true},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
@@ -104,7 +153,9 @@ func TestMultipleOverlappingRules(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP %v", i, err)
|
||||
if !test.haserror || !strings.HasPrefix(err.Error(), "BasicAuth: user") {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP %v", i, err)
|
||||
}
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("Test %d: Expected Header '%d' but was '%d'",
|
||||
@@ -127,7 +178,7 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
|
||||
htfh, err := ioutil.TempFile("", "basicauth-")
|
||||
if err != nil {
|
||||
t.Skipf("Error creating temp file (%v), will skip htpassword test")
|
||||
t.Skip("Error creating temp file, will skip htpassword test")
|
||||
return
|
||||
}
|
||||
defer os.Remove(htfh.Name())
|
||||
@@ -150,3 +201,30 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionsMethod(t *testing.T) {
|
||||
rw := BasicAuth{
|
||||
Next: httpserver.HandlerFunc(contentHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "username", Password: PlainMatcher("password"), Resources: []string{"/testing"}},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodOptions, "/testing", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create HTTP request: %v", err)
|
||||
}
|
||||
|
||||
// add basic auth with invalid username
|
||||
// and password to make sure basic auth is ignored
|
||||
req.SetBasicAuth("invaliduser", "invalidpassword")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not ServeHTTP: %v", err)
|
||||
}
|
||||
if result != http.StatusOK {
|
||||
t.Errorf("Expected status code %d but was %d", http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -51,13 +65,6 @@ func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
rule.Resources = append(rule.Resources, c.Val())
|
||||
if c.NextArg() {
|
||||
return rules, c.Errf("Expecting only one resource per line (extra '%s')", c.Val())
|
||||
}
|
||||
}
|
||||
case 3:
|
||||
rule.Resources = append(rule.Resources, args[0])
|
||||
rule.Username = args[1]
|
||||
@@ -68,6 +75,29 @@ func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
|
||||
// If nested block is present, process it here
|
||||
for c.NextBlock() {
|
||||
val := c.Val()
|
||||
args = c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 0:
|
||||
// Assume single argument is path resource
|
||||
rule.Resources = append(rule.Resources, val)
|
||||
case 1:
|
||||
if val == "realm" {
|
||||
if rule.Realm == "" {
|
||||
rule.Realm = strings.Replace(args[0], `"`, `\"`, -1)
|
||||
} else {
|
||||
return rules, c.Errf("\"realm\" subdirective can only be specified once")
|
||||
}
|
||||
} else {
|
||||
return rules, c.Errf("expecting \"realm\", got \"%s\"", val)
|
||||
}
|
||||
default:
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
@@ -75,8 +105,9 @@ func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
|
||||
}
|
||||
|
||||
func passwordMatcher(username, passw, siteRoot string) (PasswordMatcher, error) {
|
||||
if !strings.HasPrefix(passw, "htpasswd=") {
|
||||
htpasswdPrefix := "htpasswd="
|
||||
if !strings.HasPrefix(passw, htpasswdPrefix) {
|
||||
return PlainMatcher(passw), nil
|
||||
}
|
||||
return GetHtpasswdMatcher(passw[9:], username, siteRoot)
|
||||
return GetHtpasswdMatcher(passw[len(htpasswdPrefix):], username, siteRoot)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
@@ -7,8 +21,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
@@ -64,12 +78,39 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth /resource1 user pwd {
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource1"}},
|
||||
}},
|
||||
{`basicauth /resource1 user pwd {
|
||||
realm Resources
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource1"}, Realm: "Resources"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
/resource2
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource1", "/resource2"}},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
/resource2
|
||||
realm "Secure resources"
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource1", "/resource2"}, Realm: "Secure resources"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
realm "Secure resources"
|
||||
realm Extra
|
||||
/resource2
|
||||
}`, true, "pwd", []Rule{}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
foo "Resources"
|
||||
/resource2
|
||||
}`, true, "pwd", []Rule{}},
|
||||
{`basicauth /resource user pwd`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource"}},
|
||||
}},
|
||||
@@ -109,6 +150,11 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
i, j, expectedRule.Username, actualRule.Username)
|
||||
}
|
||||
|
||||
if actualRule.Realm != expectedRule.Realm {
|
||||
t.Errorf("Test %d, rule %d: Expected realm '%s', got '%s'",
|
||||
i, j, expectedRule.Realm, actualRule.Realm)
|
||||
}
|
||||
|
||||
if strings.Contains(test.input, "htpasswd=") && skipHtpassword {
|
||||
continue
|
||||
}
|
||||
|
||||
+17
-3
@@ -1,8 +1,22 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bind
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -18,7 +32,7 @@ func setupBind(c *caddy.Controller) error {
|
||||
if !c.Args(&config.ListenHost) {
|
||||
return c.ArgErr()
|
||||
}
|
||||
config.TLS.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309
|
||||
config.TLS.Manager.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bind
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetupBind(t *testing.T) {
|
||||
@@ -18,7 +32,7 @@ func TestSetupBind(t *testing.T) {
|
||||
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 {
|
||||
if got, want := cfg.TLS.Manager.ListenHost, "1.2.3.4"; got != want {
|
||||
t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
+137
-53
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package browse provides middleware for listing files in a directory
|
||||
// when directory path is requested instead of a specific file.
|
||||
package browse
|
||||
@@ -15,15 +29,16 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
const (
|
||||
sortByName = "name"
|
||||
sortBySize = "size"
|
||||
sortByTime = "time"
|
||||
sortByName = "name"
|
||||
sortByNameDirFirst = "namedirfirst"
|
||||
sortBySize = "size"
|
||||
sortByTime = "time"
|
||||
)
|
||||
|
||||
// Browse is an http.Handler that can show a file listing when
|
||||
@@ -36,7 +51,7 @@ type Browse struct {
|
||||
|
||||
// Config is a configuration for browsing in a particular path.
|
||||
type Config struct {
|
||||
PathScope string
|
||||
PathScope string // the base path the URL must match to enable browsing
|
||||
Fs staticfiles.FileServer
|
||||
Variables interface{}
|
||||
Template *template.Template
|
||||
@@ -44,43 +59,48 @@ type Config struct {
|
||||
|
||||
// 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)
|
||||
// The name of the directory (the last element of the path).
|
||||
Name string
|
||||
|
||||
// The full path of the request
|
||||
// The full path of the request.
|
||||
Path string
|
||||
|
||||
// Whether the parent directory is browsable
|
||||
// Whether the parent directory is browse-able.
|
||||
CanGoUp bool
|
||||
|
||||
// The items (files and folders) in the path
|
||||
// The items (files and folders) in the path.
|
||||
Items []FileInfo
|
||||
|
||||
// The number of directories in the listing
|
||||
// The number of directories in the listing.
|
||||
NumDirs int
|
||||
|
||||
// The number of files (items that aren't directories) in the listing
|
||||
// The number of files (items that aren't directories) in the listing.
|
||||
NumFiles int
|
||||
|
||||
// Which sorting order is used
|
||||
// Which sorting order is used.
|
||||
Sort string
|
||||
|
||||
// And which order
|
||||
// And which order.
|
||||
Order string
|
||||
|
||||
// If ≠0 then Items have been limited to that many elements
|
||||
// If ≠0 then Items have been limited to that many elements.
|
||||
ItemsLimitedTo int
|
||||
|
||||
// Optional custom variables for use in browse templates
|
||||
// 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{}
|
||||
// Crumb represents part of a breadcrumb menu.
|
||||
type Crumb struct {
|
||||
Link, Text string
|
||||
}
|
||||
|
||||
// Breadcrumbs returns l.Path where every element maps
|
||||
// the link to the text to display.
|
||||
func (l Listing) Breadcrumbs() []Crumb {
|
||||
var result []Crumb
|
||||
|
||||
if len(l.Path) == 0 {
|
||||
return result
|
||||
@@ -93,13 +113,12 @@ func (l Listing) BreadcrumbMap() map[string]string {
|
||||
}
|
||||
|
||||
parts := strings.Split(lpath, "/")
|
||||
for i, part := range parts {
|
||||
if i == 0 && part == "" {
|
||||
// Leading slash (root)
|
||||
result["/"] = "/"
|
||||
continue
|
||||
for i := range parts {
|
||||
txt := parts[i]
|
||||
if i == 0 && parts[i] == "" {
|
||||
txt = "/"
|
||||
}
|
||||
result[strings.Join(parts[:i+1], "/")] = part
|
||||
result = append(result, Crumb{Link: strings.Repeat("../", len(parts)-i-1), Text: txt})
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -107,12 +126,13 @@ func (l Listing) BreadcrumbMap() map[string]string {
|
||||
|
||||
// FileInfo is the info about a particular file or directory
|
||||
type FileInfo struct {
|
||||
Name string
|
||||
Size int64
|
||||
URL string
|
||||
ModTime time.Time
|
||||
Mode os.FileMode
|
||||
IsDir bool
|
||||
Name string
|
||||
Size int64
|
||||
URL string
|
||||
ModTime time.Time
|
||||
Mode os.FileMode
|
||||
IsDir bool
|
||||
IsSymlink bool
|
||||
}
|
||||
|
||||
// HumanSize returns the size of the file as a human-readable string
|
||||
@@ -128,6 +148,7 @@ func (fi FileInfo) HumanModTime(format string) string {
|
||||
|
||||
// Implement sorting for Listing
|
||||
type byName Listing
|
||||
type byNameDirFirst Listing
|
||||
type bySize Listing
|
||||
type byTime Listing
|
||||
|
||||
@@ -140,6 +161,22 @@ func (l byName) Less(i, j int) bool {
|
||||
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
|
||||
}
|
||||
|
||||
// By Name Dir First
|
||||
func (l byNameDirFirst) Len() int { return len(l.Items) }
|
||||
func (l byNameDirFirst) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
||||
|
||||
// Treat upper and lower case equally
|
||||
func (l byNameDirFirst) Less(i, j int) bool {
|
||||
|
||||
// if both are dir or file sort normally
|
||||
if l.Items[i].IsDir == l.Items[j].IsDir {
|
||||
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
|
||||
}
|
||||
|
||||
// always sort dir ahead of file
|
||||
return l.Items[i].IsDir
|
||||
}
|
||||
|
||||
// By Size
|
||||
func (l bySize) Len() int { return len(l.Items) }
|
||||
func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
||||
@@ -147,12 +184,21 @@ func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
||||
const directoryOffset = -1 << 31 // = math.MinInt32
|
||||
func (l bySize) Less(i, j int) bool {
|
||||
iSize, jSize := l.Items[i].Size, l.Items[j].Size
|
||||
|
||||
// Directory sizes depend on the filesystem implementation,
|
||||
// which is opaque to a visitor, and should indeed does not change if the operator chooses to change the fs.
|
||||
// For a consistent user experience directories are pulled to the front…
|
||||
if l.Items[i].IsDir {
|
||||
iSize = directoryOffset + iSize
|
||||
iSize = directoryOffset
|
||||
}
|
||||
if l.Items[j].IsDir {
|
||||
jSize = directoryOffset + jSize
|
||||
jSize = directoryOffset
|
||||
}
|
||||
// … and sorted by name.
|
||||
if l.Items[i].IsDir && l.Items[j].IsDir {
|
||||
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
|
||||
}
|
||||
|
||||
return iSize < jSize
|
||||
}
|
||||
|
||||
@@ -169,6 +215,8 @@ func (l Listing) applySort() {
|
||||
switch l.Sort {
|
||||
case sortByName:
|
||||
sort.Sort(sort.Reverse(byName(l)))
|
||||
case sortByNameDirFirst:
|
||||
sort.Sort(sort.Reverse(byNameDirFirst(l)))
|
||||
case sortBySize:
|
||||
sort.Sort(sort.Reverse(bySize(l)))
|
||||
case sortByTime:
|
||||
@@ -181,6 +229,8 @@ func (l Listing) applySort() {
|
||||
switch l.Sort {
|
||||
case sortByName:
|
||||
sort.Sort(byName(l))
|
||||
case sortByNameDirFirst:
|
||||
sort.Sort(byNameDirFirst(l))
|
||||
case sortBySize:
|
||||
sort.Sort(bySize(l))
|
||||
case sortByTime:
|
||||
@@ -194,7 +244,7 @@ func (l Listing) applySort() {
|
||||
|
||||
func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config *Config) (Listing, bool) {
|
||||
var (
|
||||
fileinfos []FileInfo
|
||||
fileInfos []FileInfo
|
||||
dirCount, fileCount int
|
||||
hasIndexFile bool
|
||||
)
|
||||
@@ -202,33 +252,36 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
|
||||
for _, indexName := range staticfiles.IndexPages {
|
||||
for _, indexName := range config.Fs.IndexPages {
|
||||
if name == indexName {
|
||||
hasIndexFile = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
isDir := f.IsDir() || isSymlinkTargetDir(f, urlPath, config)
|
||||
|
||||
if isDir {
|
||||
name += "/"
|
||||
dirCount++
|
||||
} else {
|
||||
fileCount++
|
||||
}
|
||||
|
||||
url := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
|
||||
|
||||
if config.Fs.IsHidden(f) {
|
||||
continue
|
||||
}
|
||||
|
||||
fileinfos = append(fileinfos, FileInfo{
|
||||
IsDir: f.IsDir(),
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
URL: url.String(),
|
||||
ModTime: f.ModTime().UTC(),
|
||||
Mode: f.Mode(),
|
||||
u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
|
||||
|
||||
fileInfos = append(fileInfos, FileInfo{
|
||||
IsDir: isDir,
|
||||
IsSymlink: isSymlink(f),
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
URL: u.String(),
|
||||
ModTime: f.ModTime().UTC(),
|
||||
Mode: f.Mode(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -236,12 +289,38 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config
|
||||
Name: path.Base(urlPath),
|
||||
Path: urlPath,
|
||||
CanGoUp: canGoUp,
|
||||
Items: fileinfos,
|
||||
Items: fileInfos,
|
||||
NumDirs: dirCount,
|
||||
NumFiles: fileCount,
|
||||
}, hasIndexFile
|
||||
}
|
||||
|
||||
// isSymlink return true if f is a symbolic link
|
||||
func isSymlink(f os.FileInfo) bool {
|
||||
return f.Mode()&os.ModeSymlink != 0
|
||||
}
|
||||
|
||||
// isSymlinkTargetDir return true if f's symbolic link target
|
||||
// is a directory. Return false if not a symbolic link.
|
||||
func isSymlinkTargetDir(f os.FileInfo, urlPath string, config *Config) bool {
|
||||
if !isSymlink(f) {
|
||||
return false
|
||||
}
|
||||
|
||||
// a bit strange, but we want Stat thru the jailed filesystem to be safe
|
||||
target, err := config.Fs.Root.Open(path.Join(urlPath, f.Name()))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer target.Close()
|
||||
targetInfo, err := target.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return targetInfo.IsDir()
|
||||
}
|
||||
|
||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||
// If so, control is handed over to ServeListing.
|
||||
func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
@@ -298,9 +377,14 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
|
||||
// Browsing navigation gets messed up if browsing a directory
|
||||
// that doesn't end in "/" (which it should, anyway)
|
||||
if !strings.HasSuffix(r.URL.Path, "/") {
|
||||
staticfiles.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
|
||||
return 0, nil
|
||||
u := *r.URL
|
||||
if u.Path == "" {
|
||||
u.Path = "/"
|
||||
}
|
||||
if u.Path[len(u.Path)-1] != '/' {
|
||||
u.Path += "/"
|
||||
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
|
||||
return http.StatusMovedPermanently, nil
|
||||
}
|
||||
|
||||
return b.ServeListing(w, r, requestedFilepath, bc)
|
||||
@@ -338,11 +422,11 @@ func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope st
|
||||
// If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies
|
||||
switch sort {
|
||||
case "":
|
||||
sort = sortByName
|
||||
sort = sortByNameDirFirst
|
||||
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
|
||||
sort = sortCookie.Value
|
||||
}
|
||||
case sortByName, sortBySize, sortByTime:
|
||||
case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
|
||||
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil})
|
||||
}
|
||||
|
||||
@@ -420,7 +504,7 @@ func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFi
|
||||
|
||||
}
|
||||
|
||||
buf.WriteTo(w)
|
||||
_, _ = buf.WriteTo(w)
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
+288
-27
@@ -1,21 +1,42 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package browse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
const testDirPrefix = "caddy_browse_test"
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
// making up []fileInfo with bogus values;
|
||||
// to be used to make up our "listing"
|
||||
@@ -69,6 +90,13 @@ func TestSort(t *testing.T) {
|
||||
t.Errorf("The listing isn't time sorted: %v", listing.Items)
|
||||
}
|
||||
|
||||
// sort by name dir first
|
||||
listing.Sort = "namedirfirst"
|
||||
listing.applySort()
|
||||
if !sort.IsSorted(byNameDirFirst(listing)) {
|
||||
t.Errorf("The listing isn't namedirfirst sorted: %v", listing.Items)
|
||||
}
|
||||
|
||||
// reverse by name
|
||||
listing.Sort = "name"
|
||||
listing.Order = "desc"
|
||||
@@ -92,6 +120,14 @@ func TestSort(t *testing.T) {
|
||||
if !isReversed(byTime(listing)) {
|
||||
t.Errorf("The listing isn't reversed by time: %v", listing.Items)
|
||||
}
|
||||
|
||||
// reverse by name dir first
|
||||
listing.Sort = "namedirfirst"
|
||||
listing.Order = "desc"
|
||||
listing.applySort()
|
||||
if !isReversed(byNameDirFirst(listing)) {
|
||||
t.Errorf("The listing isn't reversed by namedirfirst: %v", listing.Items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseHTTPMethods(t *testing.T) {
|
||||
@@ -126,6 +162,8 @@ func TestBrowseHTTPMethods(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Test: Could not create HTTP request: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
code, _ := b.ServeHTTP(rec, req)
|
||||
if code != expected {
|
||||
@@ -161,6 +199,8 @@ func TestBrowseTemplate(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Test: Could not create HTTP request: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -180,6 +220,8 @@ func TestBrowseTemplate(t *testing.T) {
|
||||
|
||||
<h1>/photos/</h1>
|
||||
|
||||
<a href="./test1/">test1</a><br>
|
||||
|
||||
<a href="./test.html">test.html</a><br>
|
||||
|
||||
<a href="./test2.html">test2.html</a><br>
|
||||
@@ -191,7 +233,7 @@ func TestBrowseTemplate(t *testing.T) {
|
||||
`
|
||||
|
||||
if respBody != expectedBody {
|
||||
t.Fatalf("Expected body: %v got: %v", expectedBody, respBody)
|
||||
t.Fatalf("Expected body: '%v' got: '%v'", expectedBody, respBody)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -199,7 +241,7 @@ func TestBrowseTemplate(t *testing.T) {
|
||||
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")
|
||||
t.Fatalf("Next shouldn't be called: %s", r.URL)
|
||||
return 0, nil
|
||||
}),
|
||||
Configs: []Config{
|
||||
@@ -254,6 +296,9 @@ func TestBrowseJson(t *testing.T) {
|
||||
Mode: f.Mode(),
|
||||
})
|
||||
}
|
||||
|
||||
// Test that sort=name returns correct listing.
|
||||
|
||||
listing := Listing{Items: fileinfos} // this listing will be used for validation inside the tests
|
||||
|
||||
tests := []struct {
|
||||
@@ -266,33 +311,33 @@ func TestBrowseJson(t *testing.T) {
|
||||
}{
|
||||
//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},
|
||||
{"/?sort=name", "", "", -1, false, listing.Items},
|
||||
//test case 2: limit is set to 1, orderBy and sortBy is default
|
||||
{"/?limit=1", "", "", 1, false, listing.Items[:1]},
|
||||
{"/?limit=1&sort=name", "", "", 1, false, listing.Items[:1]},
|
||||
//test case 3 : if the listing request is bigger than total size of listing then it should return everything
|
||||
{"/?limit=100000000", "", "", 100000000, false, listing.Items},
|
||||
{"/?limit=100000000&sort=name", "", "", 100000000, false, listing.Items},
|
||||
//test case 4 : testing for negative limit
|
||||
{"/?limit=-1", "", "", -1, false, listing.Items},
|
||||
{"/?limit=-1&sort=name", "", "", -1, false, listing.Items},
|
||||
//test case 5 : testing with limit set to -1 and order set to descending
|
||||
{"/?limit=-1&order=desc", "", "desc", -1, false, listing.Items},
|
||||
{"/?limit=-1&order=desc&sort=name", "", "desc", -1, false, listing.Items},
|
||||
//test case 6 : testing with limit set to 2 and order set to descending
|
||||
{"/?limit=2&order=desc", "", "desc", 2, false, listing.Items},
|
||||
{"/?limit=2&order=desc&sort=name", "", "desc", 2, false, listing.Items},
|
||||
//test case 7 : testing with limit set to 3 and order set to descending
|
||||
{"/?limit=3&order=desc", "", "desc", 3, false, listing.Items},
|
||||
{"/?limit=3&order=desc&sort=name", "", "desc", 3, false, listing.Items},
|
||||
//test case 8 : testing with limit set to 3 and order set to ascending
|
||||
{"/?limit=3&order=asc", "", "asc", 3, false, listing.Items},
|
||||
{"/?limit=3&order=asc&sort=name", "", "asc", 3, false, listing.Items},
|
||||
//test case 9 : testing with limit set to 1111111 and order set to ascending
|
||||
{"/?limit=1111111&order=asc", "", "asc", 1111111, false, listing.Items},
|
||||
{"/?limit=1111111&order=asc&sort=name", "", "asc", 1111111, false, listing.Items},
|
||||
//test case 10 : testing with limit set to default and order set to ascending and sorting by size
|
||||
{"/?order=asc&sort=size", "size", "asc", -1, false, listing.Items},
|
||||
{"/?order=asc&sort=size&sort=name", "size", "asc", -1, false, listing.Items},
|
||||
//test case 11 : testing with limit set to default and order set to ascending and sorting by last modified
|
||||
{"/?order=asc&sort=time", "time", "asc", -1, false, listing.Items},
|
||||
{"/?order=asc&sort=time&sort=name", "time", "asc", -1, false, listing.Items},
|
||||
//test case 12 : testing with limit set to 1 and order set to ascending and sorting by last modified
|
||||
{"/?order=asc&sort=time&limit=1", "time", "asc", 1, false, listing.Items},
|
||||
{"/?order=asc&sort=time&limit=1&sort=name", "time", "asc", 1, false, listing.Items},
|
||||
//test case 13 : testing with limit set to -100 and order set to ascending and sorting by last modified
|
||||
{"/?order=asc&sort=time&limit=-100", "time", "asc", -100, false, listing.Items},
|
||||
{"/?order=asc&sort=time&limit=-100&sort=name", "time", "asc", -100, false, listing.Items},
|
||||
//test case 14 : testing with limit set to -100 and order set to ascending and sorting by size
|
||||
{"/?order=asc&sort=size&limit=-100", "size", "asc", -100, false, listing.Items},
|
||||
{"/?order=asc&sort=size&limit=-100&sort=name", "size", "asc", -100, false, listing.Items},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
@@ -301,6 +346,8 @@ func TestBrowseJson(t *testing.T) {
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored when making request, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -319,25 +366,25 @@ func TestBrowseJson(t *testing.T) {
|
||||
}
|
||||
|
||||
actualJSONResponse := rec.Body.String()
|
||||
copyOflisting := listing
|
||||
copyOfListing := listing
|
||||
if test.SortBy == "" {
|
||||
copyOflisting.Sort = "name"
|
||||
copyOfListing.Sort = "name"
|
||||
} else {
|
||||
copyOflisting.Sort = test.SortBy
|
||||
copyOfListing.Sort = test.SortBy
|
||||
}
|
||||
if test.OrderBy == "" {
|
||||
copyOflisting.Order = "asc"
|
||||
copyOfListing.Order = "asc"
|
||||
} else {
|
||||
copyOflisting.Order = test.OrderBy
|
||||
copyOfListing.Order = test.OrderBy
|
||||
}
|
||||
|
||||
copyOflisting.applySort()
|
||||
copyOfListing.applySort()
|
||||
|
||||
limit := test.Limit
|
||||
if limit <= len(copyOflisting.Items) && limit > 0 {
|
||||
marsh, err = json.Marshal(copyOflisting.Items[: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)
|
||||
marsh, err = json.Marshal(copyOfListing.Items)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -362,3 +409,217 @@ func isReversed(data sort.Interface) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestBrowseRedirect(t *testing.T) {
|
||||
testCases := []struct {
|
||||
url string
|
||||
statusCode int
|
||||
returnCode int
|
||||
location string
|
||||
}{
|
||||
{
|
||||
"http://www.example.com/photos",
|
||||
http.StatusMovedPermanently,
|
||||
http.StatusMovedPermanently,
|
||||
"http://www.example.com/photos/",
|
||||
},
|
||||
{
|
||||
"/photos",
|
||||
http.StatusMovedPermanently,
|
||||
http.StatusMovedPermanently,
|
||||
"/photos/",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
b := Browse{
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Test %d - Next shouldn't be called", i)
|
||||
return 0, nil
|
||||
}),
|
||||
Configs: []Config{
|
||||
{
|
||||
PathScope: "/photos",
|
||||
Fs: staticfiles.FileServer{
|
||||
Root: http.Dir("./testdata"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", tc.url, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d - could not create HTTP request: %v", i, err)
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
returnCode, _ := b.ServeHTTP(rec, req)
|
||||
if returnCode != tc.returnCode {
|
||||
t.Fatalf("Test %d - wrong return code, expected %d, got %d",
|
||||
i, tc.returnCode, returnCode)
|
||||
}
|
||||
|
||||
if got := rec.Code; got != tc.statusCode {
|
||||
t.Errorf("Test %d - wrong status, expected %d, got %d",
|
||||
i, tc.statusCode, got)
|
||||
}
|
||||
|
||||
if got := rec.Header().Get("Location"); got != tc.location {
|
||||
t.Errorf("Test %d - wrong Location header, expected %s, got %s",
|
||||
i, tc.location, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirSymlink(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows support for symlinks is limited, and we had a hard time getting
|
||||
// all these tests to pass with the permissions of CI; so just skip them
|
||||
fmt.Println("Skipping browse symlink tests on Windows...")
|
||||
return
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
source string
|
||||
target string
|
||||
pathScope string
|
||||
url string
|
||||
expectedName string
|
||||
expectedURL string
|
||||
}{
|
||||
// test case can expect a directory "dir" and a symlink to it called "symlink"
|
||||
|
||||
{"dir", "$TMP/rel_symlink_to_dir", "/", "/",
|
||||
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
|
||||
{"$TMP/dir", "$TMP/abs_symlink_to_dir", "/", "/",
|
||||
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
|
||||
|
||||
{"../../dir", "$TMP/sub/dir/rel_symlink_to_dir", "/", "/sub/dir/",
|
||||
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
|
||||
{"$TMP/dir", "$TMP/sub/dir/abs_symlink_to_dir", "/", "/sub/dir/",
|
||||
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
|
||||
|
||||
{"../../dir", "$TMP/with/scope/rel_symlink_to_dir", "/with/scope", "/with/scope/",
|
||||
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
|
||||
{"$TMP/dir", "$TMP/with/scope/abs_symlink_to_dir", "/with/scope", "/with/scope/",
|
||||
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
|
||||
|
||||
{"../../../../dir", "$TMP/with/scope/sub/dir/rel_symlink_to_dir", "/with/scope", "/with/scope/sub/dir/",
|
||||
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
|
||||
{"$TMP/dir", "$TMP/with/scope/sub/dir/abs_symlink_to_dir", "/with/scope", "/with/scope/sub/dir/",
|
||||
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
|
||||
|
||||
{"symlink", "$TMP/rel_symlink_to_symlink", "/", "/",
|
||||
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
|
||||
{"$TMP/symlink", "$TMP/abs_symlink_to_symlink", "/", "/",
|
||||
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
|
||||
|
||||
{"../../symlink", "$TMP/sub/dir/rel_symlink_to_symlink", "/", "/sub/dir/",
|
||||
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
|
||||
{"$TMP/symlink", "$TMP/sub/dir/abs_symlink_to_symlink", "/", "/sub/dir/",
|
||||
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
|
||||
|
||||
{"../../symlink", "$TMP/with/scope/rel_symlink_to_symlink", "/with/scope", "/with/scope/",
|
||||
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
|
||||
{"$TMP/symlink", "$TMP/with/scope/abs_symlink_to_symlink", "/with/scope", "/with/scope/",
|
||||
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
|
||||
|
||||
{"../../../../symlink", "$TMP/with/scope/sub/dir/rel_symlink_to_symlink", "/with/scope", "/with/scope/sub/dir/",
|
||||
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
|
||||
{"$TMP/symlink", "$TMP/with/scope/sub/dir/abs_symlink_to_symlink", "/with/scope", "/with/scope/sub/dir/",
|
||||
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
func() {
|
||||
tmpdir, err := ioutil.TempDir("", testDirPrefix)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(tmpdir, "dir"), 0755); err != nil {
|
||||
t.Fatalf("failed to create test dir 'dir': %v", err)
|
||||
}
|
||||
if err := os.Symlink("dir", filepath.Join(tmpdir, "symlink")); err != nil {
|
||||
t.Fatalf("failed to create test symlink 'symlink': %v", err)
|
||||
}
|
||||
|
||||
sourceResolved := strings.Replace(tc.source, "$TMP", tmpdir, -1)
|
||||
targetResolved := strings.Replace(tc.target, "$TMP", tmpdir, -1)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(sourceResolved), 0755); err != nil {
|
||||
t.Fatalf("failed to create source symlink dir: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(targetResolved), 0755); err != nil {
|
||||
t.Fatalf("failed to create target symlink dir: %v", err)
|
||||
}
|
||||
if err := os.Symlink(sourceResolved, targetResolved); err != nil {
|
||||
t.Fatalf("failed to create test symlink: %v", err)
|
||||
}
|
||||
|
||||
b := Browse{
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Test %d - Next shouldn't be called", i)
|
||||
return 0, nil
|
||||
}),
|
||||
Configs: []Config{
|
||||
{
|
||||
PathScope: tc.pathScope,
|
||||
Fs: staticfiles.FileServer{
|
||||
Root: http.Dir(tmpdir),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", tc.url, nil)
|
||||
req.Header.Add("Accept", "application/json")
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d - could not create HTTP request: %v", i, err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
returnCode, _ := b.ServeHTTP(rec, req)
|
||||
if returnCode != http.StatusOK {
|
||||
t.Fatalf("Test %d - wrong return code, expected %d, got %d",
|
||||
i, http.StatusOK, returnCode)
|
||||
}
|
||||
|
||||
type jsonEntry struct {
|
||||
Name string
|
||||
IsDir bool
|
||||
IsSymlink bool
|
||||
URL string
|
||||
}
|
||||
var entries []jsonEntry
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &entries); err != nil {
|
||||
t.Fatalf("Test %d - failed to parse json: %v", i, err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, e := range entries {
|
||||
if e.Name != tc.expectedName {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if !e.IsDir {
|
||||
t.Errorf("Test %d - expected to be a dir, got %v", i, e.IsDir)
|
||||
}
|
||||
if !e.IsSymlink {
|
||||
t.Errorf("Test %d - expected to be a symlink, got %v", i, e.IsSymlink)
|
||||
}
|
||||
if e.URL != tc.expectedURL {
|
||||
t.Errorf("Test %d - wrong URL, expected %v, got %v", i, tc.expectedURL, e.URL)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Test %d - failed, could not find name %v", i, tc.expectedName)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
+136
-60
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package browse
|
||||
|
||||
import (
|
||||
@@ -6,9 +20,9 @@ import (
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -64,8 +78,9 @@ func browseParse(c *caddy.Controller) ([]Config, error) {
|
||||
}
|
||||
|
||||
bc.Fs = staticfiles.FileServer{
|
||||
Root: http.Dir(cfg.Root),
|
||||
Hide: httpserver.GetConfig(c).HiddenFiles,
|
||||
Root: http.Dir(cfg.Root),
|
||||
Hide: cfg.HiddenFiles,
|
||||
IndexPages: cfg.IndexPages,
|
||||
}
|
||||
|
||||
// Second argument would be the template file to use
|
||||
@@ -110,6 +125,7 @@ const defaultTemplate = `<!DOCTYPE html>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
text-rendering: optimizespeed;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -130,12 +146,12 @@ header,
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 5%;
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 5%;
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
header {
|
||||
@@ -150,16 +166,22 @@ h1 {
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
h1 a {
|
||||
color: inherit;
|
||||
color: #000;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
h1 a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1 a:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
@@ -216,19 +238,24 @@ th svg {
|
||||
}
|
||||
|
||||
td {
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 50%;
|
||||
td:nth-child(2) {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
td:nth-child(3) {
|
||||
padding: 0 20px 0 20px;
|
||||
}
|
||||
|
||||
th:nth-child(4),
|
||||
td:nth-child(4) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td:first-child svg {
|
||||
td:nth-child(2) svg {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@@ -240,6 +267,30 @@ td .goup {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.icon.sort {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
position: relative;
|
||||
top: .2em;
|
||||
}
|
||||
|
||||
.icon.sort .top {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.icon.sort .bottom {
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 40px 20px;
|
||||
font-size: 12px;
|
||||
@@ -251,61 +302,60 @@ footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
td:nth-child(2) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
th:nth-child(3),
|
||||
td:nth-child(3) {
|
||||
padding-right: 5%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h1 a {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#filter {
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body onload='initFilter()'>
|
||||
<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 id="folder" fill-rule="nonzero" fill="none">
|
||||
<path d="M285.22 37.55h-142.6L110.9 0H31.7C14.25 0 0 16.9 0 37.55v75.1h316.92V75.1c0-20.65-14.26-37.55-31.7-37.55z" fill="#FFA000"/>
|
||||
<path d="M285.22 36H31.7C14.25 36 0 50.28 0 67.74v158.7c0 17.47 14.26 31.75 31.7 31.75H285.2c17.44 0 31.7-14.3 31.7-31.75V67.75c0-17.47-14.26-31.75-31.7-31.75z" fill="#FFCA28"/>
|
||||
</g>
|
||||
<g id="folder-shortcut" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="folder-shortcut-group" fill-rule="nonzero">
|
||||
<g id="folder-shortcut-shape">
|
||||
<path d="M285.224876,37.5486902 L142.612438,37.5486902 L110.920785,0 L31.6916529,0 C14.2612438,0 0,16.8969106 0,37.5486902 L0,112.646071 L316.916529,112.646071 L316.916529,75.0973805 C316.916529,54.4456008 302.655285,37.5486902 285.224876,37.5486902 Z" id="Shape" fill="#FFA000"></path>
|
||||
<path d="M285.224876,36 L31.6916529,36 C14.2612438,36 0,50.2838568 0,67.7419039 L0,226.451424 C0,243.909471 14.2612438,258.193328 31.6916529,258.193328 L285.224876,258.193328 C302.655285,258.193328 316.916529,243.909471 316.916529,226.451424 L316.916529,67.7419039 C316.916529,50.2838568 302.655285,36 285.224876,36 Z" id="Shape" fill="#FFCA28"></path>
|
||||
</g>
|
||||
<path d="M126.154134,250.559184 C126.850974,251.883673 127.300549,253.006122 127.772602,254.106122 C128.469442,255.206122 128.919016,256.104082 129.638335,257.002041 C130.559962,258.326531 131.728855,259 133.100057,259 C134.493737,259 135.415364,258.55102 136.112204,257.67551 C136.809044,257.002041 137.258619,255.902041 137.258619,254.577551 C137.258619,253.904082 137.258619,252.804082 137.033832,251.457143 C136.786566,249.908163 136.561779,249.032653 136.561779,248.583673 C136.089726,242.814286 135.864939,237.920408 135.864939,233.273469 C135.864939,225.057143 136.786566,217.514286 138.180246,210.846939 C139.798713,204.202041 141.889234,198.634694 144.429328,193.763265 C147.216689,188.869388 150.678411,184.873469 154.836973,181.326531 C158.995535,177.779592 163.626149,174.883673 168.481552,172.661224 C173.336954,170.438776 179.113983,168.665306 185.587852,167.340816 C192.061722,166.218367 198.760378,165.342857 205.481514,164.669388 C212.18017,164.220408 219.598146,163.995918 228.162535,163.995918 L246.055591,163.995918 L246.055591,195.514286 C246.055591,197.736735 246.752431,199.510204 248.370899,201.059184 C250.214153,202.608163 252.079886,203.506122 254.372715,203.506122 C256.463236,203.506122 258.531277,202.608163 260.172223,201.059184 L326.102289,137.797959 C327.720757,136.24898 328.642384,134.47551 328.642384,132.253061 C328.642384,130.030612 327.720757,128.257143 326.102289,126.708163 L260.172223,63.4469388 C258.553756,61.8979592 256.463236,61 254.395194,61 C252.079886,61 250.236632,61.8979592 248.393377,63.4469388 C246.77491,64.9959184 246.07807,66.7693878 246.07807,68.9918367 L246.07807,100.510204 L228.162535,100.510204 C166.863084,100.510204 129.166282,117.167347 115.274437,150.459184 C110.666301,161.54898 108.350993,175.310204 108.350993,191.742857 C108.350993,205.279592 113.903236,223.912245 124.760454,247.438776 C125.00772,248.112245 125.457294,249.010204 126.154134,250.559184 Z" id="Shape" fill="#FFFFFF" transform="translate(218.496689, 160.000000) scale(-1, 1) translate(-218.496689, -160.000000) "></path>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- File -->
|
||||
<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 id="file" stroke="#000" stroke-width="25" fill="#FFF" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 24.12v274.76c0 6.16 5.87 11.12 13.17 11.12H239c7.3 0 13.17-4.96 13.17-11.12V136.15S132.6 13 128.37 13H26.17C18.87 13 13 17.96 13 24.12z"/>
|
||||
<path d="M129.37 13L129 113.9c0 10.58 7.26 19.1 16.27 19.1H249L129.37 13z"/>
|
||||
</g>
|
||||
<g id="file-shortcut" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="file-shortcut-group" transform="translate(13.000000, 13.000000)">
|
||||
<g id="file-shortcut-shape" stroke="#000000" stroke-width="25" fill="#FFFFFF" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M0,11.1214886 L0,285.878477 C0,292.039924 5.87498876,296.999983 13.1728373,296.999983 L225.997983,296.999983 C233.295974,296.999983 239.17082,292.039942 239.17082,285.878477 L239.17082,123.145388 C239.17082,123.145388 119.58541,2.84217094e-14 115.369423,2.84217094e-14 L13.1728576,2.84217094e-14 C5.87500907,-1.71479982e-05 0,4.96022995 0,11.1214886 Z" id="rect1171"></path>
|
||||
<path d="M116.37005,0 L116,100.904964 C116,111.483663 123.258008,120 132.273377,120 L236,120 L116.37005,0 L116.37005,0 Z" id="rect1794"></path>
|
||||
</g>
|
||||
<path d="M47.803141,294.093878 C48.4999811,295.177551 48.9495553,296.095918 49.4216083,296.995918 C50.1184484,297.895918 50.5680227,298.630612 51.2873415,299.365306 C52.2089688,300.44898 53.3778619,301 54.7490634,301 C56.1427436,301 57.0643709,300.632653 57.761211,299.916327 C58.4580511,299.365306 58.9076254,298.465306 58.9076254,297.381633 C58.9076254,296.830612 58.9076254,295.930612 58.6828382,294.828571 C58.4355724,293.561224 58.2107852,292.844898 58.2107852,292.477551 C57.7387323,287.757143 57.5139451,283.753061 57.5139451,279.95102 C57.5139451,273.228571 58.4355724,267.057143 59.8292526,261.602041 C61.44772,256.165306 63.5382403,251.610204 66.0783349,247.62449 C68.8656954,243.620408 72.3274172,240.35102 76.4859792,237.44898 C80.6445412,234.546939 85.2751561,232.177551 90.1305582,230.359184 C94.9859603,228.540816 100.76299,227.089796 107.236859,226.006122 C113.710728,225.087755 120.409385,224.371429 127.13052,223.820408 C133.829177,223.453061 141.247152,223.269388 149.811542,223.269388 L167.704598,223.269388 L167.704598,249.057143 C167.704598,250.87551 168.401438,252.326531 170.019905,253.593878 C171.86316,254.861224 173.728893,255.595918 176.021722,255.595918 C178.112242,255.595918 180.180284,254.861224 181.82123,253.593878 L247.751296,201.834694 C249.369763,200.567347 250.291391,199.116327 250.291391,197.297959 C250.291391,195.479592 249.369763,194.028571 247.751296,192.761224 L181.82123,141.002041 C180.202763,139.734694 178.112242,139 176.044201,139 C173.728893,139 171.885639,139.734694 170.042384,141.002041 C168.423917,142.269388 167.727077,143.720408 167.727077,145.538776 L167.727077,171.326531 L149.811542,171.326531 C88.5120908,171.326531 50.8152886,184.955102 36.9234437,212.193878 C32.3153075,221.267347 30,232.526531 30,245.971429 C30,257.046939 35.5522422,272.291837 46.4094607,291.540816 C46.6567266,292.091837 47.1063009,292.826531 47.803141,294.093878 Z" id="Shape-Copy" fill="#000000" fill-rule="nonzero" transform="translate(140.145695, 220.000000) scale(-1, 1) translate(-140.145695, -220.000000) "></path>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
@@ -323,7 +373,7 @@ footer {
|
||||
|
||||
<header>
|
||||
<h1>
|
||||
{{range $url, $name := .BreadcrumbMap}}<a href="{{html $url}}">{{html $name}}</a>{{if ne $url "/"}}/{{end}}{{end}}
|
||||
{{range $i, $crumb := .Breadcrumbs}}<a href="{{html $crumb.Link}}">{{html $crumb.Text}}</a>{{if ne $i 0}}/{{end}}{{end}}
|
||||
</h1>
|
||||
</header>
|
||||
<main>
|
||||
@@ -341,38 +391,49 @@ footer {
|
||||
<table aria-describedby="summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
{{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}}
|
||||
<a href="?sort=namedirfirst&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}" class="icon"><svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
|
||||
{{- else if and (eq .Sort "namedirfirst") (ne .Order "asc")}}
|
||||
<a href="?sort=namedirfirst&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}" class="icon"><svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
|
||||
{{- else}}
|
||||
<a href="?sort=namedirfirst&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}" class="icon sort"><svg class="top" width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg><svg class="bottom" width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
|
||||
{{- end}}
|
||||
|
||||
{{- 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>
|
||||
<a href="?sort=name&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name <svg width="1em" height=".5em" 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>
|
||||
<a href="?sort=name&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name <svg width="1em" height=".5em" 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>
|
||||
<a href="?sort=size&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size <svg width="1em" height=".5em" 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>
|
||||
<a href="?sort=size&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size <svg width="1em" height=".5em" 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>
|
||||
<a href="?sort=time&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified <svg width="1em" height=".5em" 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>
|
||||
<a href="?sort=time&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified <svg width="1em" height=".5em" 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>
|
||||
<th class="hideable"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{- if .CanGoUp}}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="..">
|
||||
<span class="goup">Go up</span>
|
||||
@@ -380,16 +441,18 @@ footer {
|
||||
</td>
|
||||
<td>—</td>
|
||||
<td class="hideable">—</td>
|
||||
<td class="hideable"></td>
|
||||
</tr>
|
||||
{{- end}}
|
||||
{{- range .Items}}
|
||||
<tr class="file">
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="{{html .URL}}">
|
||||
{{- if .IsDir}}
|
||||
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 35.678803 28.527945"><use xlink:href="#folder"></use></svg>
|
||||
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 317 259"><use xlink:href="#folder{{if .IsSymlink}}-shortcut{{end}}"></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>
|
||||
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 265 323"><use xlink:href="#file{{if .IsSymlink}}-shortcut{{end}}"></use></svg>
|
||||
{{- end}}
|
||||
<span class="name">{{html .Name}}</span>
|
||||
</a>
|
||||
@@ -400,6 +463,7 @@ footer {
|
||||
<td data-order="{{.Size}}">{{.HumanSize}}</td>
|
||||
{{- end}}
|
||||
<td class="hideable"><time datetime="{{.HumanModTime "2006-01-02T15:04:05Z"}}">{{.HumanModTime "01/02/2006 03:04:05 PM -07:00"}}</time></td>
|
||||
<td class="hideable"></td>
|
||||
</tr>
|
||||
{{- end}}
|
||||
</tbody>
|
||||
@@ -411,6 +475,18 @@ footer {
|
||||
</footer>
|
||||
<script>
|
||||
var filterEl = document.getElementById('filter');
|
||||
filterEl.focus();
|
||||
|
||||
function initFilter() {
|
||||
if (!filterEl.value) {
|
||||
var filterParam = new URL(window.location.href).searchParams.get('filter');
|
||||
if (filterParam) {
|
||||
filterEl.value = filterParam;
|
||||
}
|
||||
}
|
||||
filter();
|
||||
}
|
||||
|
||||
function filter() {
|
||||
var q = filterEl.value.trim().toLowerCase();
|
||||
var elems = document.querySelectorAll('tr.file');
|
||||
@@ -440,7 +516,7 @@ footer {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.textContent = d.toLocaleString();
|
||||
e.textContent = d.toLocaleString([], {day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
}
|
||||
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
|
||||
timeList.forEach(localizeDatetime);
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package browse
|
||||
|
||||
import (
|
||||
@@ -8,8 +22,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
@@ -39,7 +53,7 @@ func TestSetup(t *testing.T) {
|
||||
// test case #1 tests instantiation of Config with default values
|
||||
{"browse /", []string{"/"}, false},
|
||||
|
||||
// test case #2 tests detectaction of custom template
|
||||
// test case #2 tests detection of custom template
|
||||
{"browse . " + tempTemplatePath, []string{"."}, false},
|
||||
|
||||
// test case #3 tests detection of non-existent template
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
+42
-25
@@ -1,32 +1,49 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
// plug in the server
|
||||
_ "github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
_ "github.com/caddyserver/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/maxrequestbody"
|
||||
_ "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/status"
|
||||
_ "github.com/mholt/caddy/caddyhttp/templates"
|
||||
_ "github.com/mholt/caddy/caddyhttp/timeouts"
|
||||
_ "github.com/mholt/caddy/caddyhttp/websocket"
|
||||
_ "github.com/mholt/caddy/startupshutdown"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/basicauth"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/bind"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/browse"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/errors"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/expvar"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/extensions"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/fastcgi"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/gzip"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/header"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/index"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/internalsrv"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/limits"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/log"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/markdown"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/mime"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/pprof"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/proxy"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/push"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/redirect"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/requestid"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/rewrite"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/root"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/status"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/templates"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/timeouts"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/websocket"
|
||||
_ "github.com/caddyserver/caddy/onevent"
|
||||
)
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
// TODO: this test could be improved; the purpose is to
|
||||
// ensure that the standard plugins are in fact plugged in
|
||||
// and registered properly; this is a quick/naive way to do it.
|
||||
func TestStandardPlugins(t *testing.T) {
|
||||
numStandardPlugins := 29 // importing caddyhttp plugs in this many plugins
|
||||
numStandardPlugins := 32 // importing caddyhttp plugs in this many plugins
|
||||
s := caddy.DescribePlugins()
|
||||
if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want {
|
||||
if got, want := strings.Count(s, "\n"), numStandardPlugins+4; got != want {
|
||||
t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s)
|
||||
}
|
||||
}
|
||||
|
||||
+31
-20
@@ -1,18 +1,32 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package errors implements an HTTP error handling middleware.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -27,11 +41,8 @@ type ErrorHandler struct {
|
||||
Next httpserver.Handler
|
||||
GenericErrorPage string // default error page filename
|
||||
ErrorPages map[int]string // map of status code to filename
|
||||
LogFile string
|
||||
Log *log.Logger
|
||||
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
|
||||
Log *httpserver.Logger
|
||||
Debug bool // if true, errors are written out to client rather than to a log
|
||||
}
|
||||
|
||||
func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
@@ -40,7 +51,7 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
|
||||
status, err := h.Next.ServeHTTP(w, r)
|
||||
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("%s [ERROR %d %s] %v", time.Now().Format(timeFormat), status, r.URL.Path, err)
|
||||
errMsg := fmt.Sprintf("[ERROR %d %s] %v", status, r.URL.Path, err)
|
||||
if h.Debug {
|
||||
// Write error to response instead of to log
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
@@ -69,22 +80,24 @@ func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int
|
||||
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)
|
||||
h.Log.Printf("[NOTICE %d %s] could not load error page: %v", code, r.URL.String(), err)
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
return
|
||||
}
|
||||
defer errorPage.Close()
|
||||
|
||||
// Get content type by extension
|
||||
contentType := mime.TypeByExtension(filepath.Ext(pagePath))
|
||||
if contentType == "" {
|
||||
contentType = "text/html; charset=utf-8"
|
||||
}
|
||||
// Copy the page body into the response
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.WriteHeader(code)
|
||||
_, err = io.Copy(w, errorPage)
|
||||
|
||||
if err != nil {
|
||||
// Epic fail... sigh.
|
||||
h.Log.Printf("%s [NOTICE %d %s] could not respond with %s: %v",
|
||||
time.Now().Format(timeFormat), code, r.URL.String(), pagePath, err)
|
||||
h.Log.Printf("[NOTICE %d %s] could not respond with %s: %v", code, r.URL.String(), pagePath, err)
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
}
|
||||
|
||||
@@ -132,13 +145,13 @@ func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Trim file path
|
||||
delim := "/caddy/"
|
||||
delim := "/github.com/caddyserver/caddy/"
|
||||
pkgPathPos := strings.Index(file, delim)
|
||||
if pkgPathPos > -1 && len(file) > pkgPathPos+len(delim) {
|
||||
file = file[pkgPathPos+len(delim):]
|
||||
}
|
||||
|
||||
panicMsg := fmt.Sprintf("%s [PANIC %s] %s:%d - %v", time.Now().Format(timeFormat), r.URL.String(), file, line, rec)
|
||||
panicMsg := fmt.Sprintf("[PANIC %s] %s:%d - %v", r.URL.String(), file, line, rec)
|
||||
if h.Debug {
|
||||
// Write error and stack trace to the response rather than to a log
|
||||
var stackBuf [4096]byte
|
||||
@@ -150,5 +163,3 @@ func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
|
||||
h.errorPage(w, r, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
const timeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -13,7 +26,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
@@ -32,7 +45,7 @@ func TestErrors(t *testing.T) {
|
||||
http.StatusNotFound: path,
|
||||
http.StatusForbidden: "not_exist_file",
|
||||
},
|
||||
Log: log.New(&buf, "", 0),
|
||||
Log: httpserver.NewTestLogger(&buf),
|
||||
}
|
||||
_, notExistErr := os.Open("not_exist_file")
|
||||
|
||||
@@ -140,7 +153,7 @@ func TestVisibleErrorWithPanic(t *testing.T) {
|
||||
|
||||
body := rec.Body.String()
|
||||
|
||||
if !strings.Contains(body, "[PANIC /] caddyhttp/errors/errors_test.go") {
|
||||
if !strings.Contains(body, "[PANIC /]") {
|
||||
t.Errorf("Expected response body to contain error log line, but it didn't:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, panicMsg) {
|
||||
@@ -154,6 +167,7 @@ func TestVisibleErrorWithPanic(t *testing.T) {
|
||||
func TestGenericErrorPage(t *testing.T) {
|
||||
// create temporary generic error page
|
||||
const genericErrorContent = "This is a generic error page"
|
||||
const badRequestErrorJSONContent = `{"message":"This is a error json message"}`
|
||||
|
||||
genericErrorPagePath, err := createErrorPageFile("generic_error_test.html", genericErrorContent)
|
||||
if err != nil {
|
||||
@@ -170,13 +184,20 @@ func TestGenericErrorPage(t *testing.T) {
|
||||
}
|
||||
defer os.Remove(notFoundErrorPagePath)
|
||||
|
||||
badRequestErrorJSONPath, err := createErrorPageFile("not_found.json", badRequestErrorJSONContent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(badRequestErrorJSONPath)
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
em := ErrorHandler{
|
||||
GenericErrorPage: genericErrorPagePath,
|
||||
ErrorPages: map[int]string{
|
||||
http.StatusNotFound: notFoundErrorPagePath,
|
||||
http.StatusBadRequest:badRequestErrorJSONPath,
|
||||
},
|
||||
Log: log.New(&buf, "", 0),
|
||||
Log: httpserver.NewTestLogger(&buf),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
@@ -185,6 +206,7 @@ func TestGenericErrorPage(t *testing.T) {
|
||||
expectedBody string
|
||||
expectedLog string
|
||||
expectedErr error
|
||||
expectedContentType string
|
||||
}{
|
||||
{
|
||||
next: genErrorHandler(http.StatusNotFound, nil, ""),
|
||||
@@ -192,6 +214,15 @@ func TestGenericErrorPage(t *testing.T) {
|
||||
expectedBody: notFoundErrorContent,
|
||||
expectedLog: "",
|
||||
expectedErr: nil,
|
||||
expectedContentType: "text/html; charset=utf-8",
|
||||
},
|
||||
{
|
||||
next: genErrorHandler(http.StatusBadRequest, nil, ""),
|
||||
expectedCode: 0,
|
||||
expectedBody: badRequestErrorJSONContent,
|
||||
expectedLog: "",
|
||||
expectedErr: nil,
|
||||
expectedContentType: "application/json",
|
||||
},
|
||||
{
|
||||
next: genErrorHandler(http.StatusInternalServerError, nil, ""),
|
||||
@@ -199,6 +230,7 @@ func TestGenericErrorPage(t *testing.T) {
|
||||
expectedBody: genericErrorContent,
|
||||
expectedLog: "",
|
||||
expectedErr: nil,
|
||||
expectedContentType:"text/html; charset=utf-8",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -225,6 +257,10 @@ func TestGenericErrorPage(t *testing.T) {
|
||||
t.Errorf("Test %d: Expected body %q, but got %q",
|
||||
i, test.expectedBody, body)
|
||||
}
|
||||
if contentType := rec.Header().Get("Content-Type"); contentType != test.expectedContentType{
|
||||
t.Errorf("Test %d: Expected Content-Type %s, but got %s",
|
||||
i, test.expectedContentType, contentType)
|
||||
}
|
||||
if log := buf.String(); !strings.Contains(log, test.expectedLog) {
|
||||
t.Errorf("Test %d: Expected log %q, but got %q",
|
||||
i, test.expectedLog, log)
|
||||
|
||||
+59
-94
@@ -1,73 +1,38 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/go-syslog"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// setup configures a new errors middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
handler, err := errorsParse(c)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Open the log file for writing when the server starts
|
||||
c.OnStartup(func() error {
|
||||
var err error
|
||||
var writer io.Writer
|
||||
|
||||
switch handler.LogFile {
|
||||
case "visible":
|
||||
handler.Debug = true
|
||||
case "stdout":
|
||||
writer = os.Stdout
|
||||
case "stderr":
|
||||
writer = os.Stderr
|
||||
case "syslog":
|
||||
writer, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if handler.LogFile == "" {
|
||||
writer = os.Stderr // default
|
||||
break
|
||||
}
|
||||
|
||||
var file *os.File
|
||||
file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if handler.LogRoller != nil {
|
||||
file.Close()
|
||||
handler.LogRoller.Filename = handler.LogFile
|
||||
writer = handler.LogRoller.GetLogWriter()
|
||||
} else {
|
||||
handler.file = file
|
||||
writer = file
|
||||
}
|
||||
}
|
||||
|
||||
handler.Log = log.New(writer, "", 0)
|
||||
return nil
|
||||
})
|
||||
|
||||
// When server stops, close any open log file
|
||||
c.OnShutdown(func() error {
|
||||
if handler.file != nil {
|
||||
handler.file.Close()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
handler.Log.Attach(c)
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
handler.Next = next
|
||||
@@ -78,46 +43,40 @@ func setup(c *caddy.Controller) error {
|
||||
}
|
||||
|
||||
func errorsParse(c *caddy.Controller) (*ErrorHandler, error) {
|
||||
|
||||
// Very important that we make a pointer because the startup
|
||||
// function that opens the log file must have access to the
|
||||
// same instance of the handler, not a copy.
|
||||
handler := &ErrorHandler{ErrorPages: make(map[int]string)}
|
||||
handler := &ErrorHandler{
|
||||
ErrorPages: make(map[int]string),
|
||||
Log: &httpserver.Logger{},
|
||||
}
|
||||
|
||||
cfg := httpserver.GetConfig(c)
|
||||
|
||||
optionalBlock := func() (bool, error) {
|
||||
var hadBlock bool
|
||||
|
||||
optionalBlock := func() error {
|
||||
for c.NextBlock() {
|
||||
hadBlock = true
|
||||
|
||||
what := c.Val()
|
||||
if !c.NextArg() {
|
||||
return hadBlock, c.ArgErr()
|
||||
}
|
||||
where := c.Val()
|
||||
where := c.RemainingArgs()
|
||||
|
||||
if what == "log" {
|
||||
if where == "visible" {
|
||||
handler.Debug = true
|
||||
} else {
|
||||
handler.LogFile = where
|
||||
if c.NextArg() {
|
||||
if c.Val() == "{" {
|
||||
c.IncrNest()
|
||||
logRoller, err := httpserver.ParseRoller(c)
|
||||
if err != nil {
|
||||
return hadBlock, err
|
||||
}
|
||||
handler.LogRoller = logRoller
|
||||
}
|
||||
}
|
||||
if httpserver.IsLogRollerSubdirective(what) {
|
||||
var err error
|
||||
err = httpserver.ParseRoller(handler.Log.Roller, what, where...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if len(where) != 1 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
where := where[0]
|
||||
|
||||
// Error page; ensure it exists
|
||||
if !filepath.IsAbs(where) {
|
||||
where = filepath.Join(cfg.Root, where)
|
||||
}
|
||||
|
||||
f, err := os.Open(where)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Unable to open error page '%s': %v", where, err)
|
||||
@@ -126,24 +85,24 @@ func errorsParse(c *caddy.Controller) (*ErrorHandler, error) {
|
||||
|
||||
if what == "*" {
|
||||
if handler.GenericErrorPage != "" {
|
||||
return hadBlock, c.Errf("Duplicate status code entry: %s", what)
|
||||
return c.Errf("Duplicate status code entry: %s", what)
|
||||
}
|
||||
handler.GenericErrorPage = where
|
||||
} else {
|
||||
whatInt, err := strconv.Atoi(what)
|
||||
if err != nil {
|
||||
return hadBlock, c.Err("Expecting a numeric status code or '*', got '" + what + "'")
|
||||
return c.Err("Expecting a numeric status code or '*', got '" + what + "'")
|
||||
}
|
||||
|
||||
if _, exists := handler.ErrorPages[whatInt]; exists {
|
||||
return hadBlock, c.Errf("Duplicate status code entry: %s", what)
|
||||
return c.Errf("Duplicate status code entry: %s", what)
|
||||
}
|
||||
|
||||
handler.ErrorPages[whatInt] = where
|
||||
}
|
||||
}
|
||||
}
|
||||
return hadBlock, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
@@ -151,21 +110,27 @@ func errorsParse(c *caddy.Controller) (*ErrorHandler, error) {
|
||||
if c.Val() == "}" {
|
||||
continue
|
||||
}
|
||||
// Configuration may be in a block
|
||||
hadBlock, err := optionalBlock()
|
||||
if err != nil {
|
||||
return handler, err
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) == 1 {
|
||||
switch args[0] {
|
||||
case "visible":
|
||||
handler.Debug = true
|
||||
default:
|
||||
handler.Log.Output = args[0]
|
||||
handler.Log.Roller = httpserver.DefaultLogRoller()
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, the only argument would be an error log file name or 'visible'
|
||||
if !hadBlock {
|
||||
if c.NextArg() {
|
||||
if c.Val() == "visible" {
|
||||
handler.Debug = true
|
||||
} else {
|
||||
handler.LogFile = c.Val()
|
||||
}
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return handler, c.Errf("Only 1 Argument expected for errors directive")
|
||||
}
|
||||
|
||||
// Configuration may be in a block
|
||||
err := optionalBlock()
|
||||
if err != nil {
|
||||
return handler, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
@@ -5,8 +19,8 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
@@ -26,12 +40,12 @@ func TestSetup(t *testing.T) {
|
||||
t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.LogFile != "" {
|
||||
t.Errorf("Expected '%s' as the default LogFile", "")
|
||||
}
|
||||
if myHandler.LogRoller != nil {
|
||||
t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller)
|
||||
expectedLogger := &httpserver.Logger{}
|
||||
|
||||
if !reflect.DeepEqual(expectedLogger, myHandler.Log) {
|
||||
t.Errorf("Expected '%v' as the default Log, got: '%v'", expectedLogger, myHandler.Log)
|
||||
}
|
||||
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
@@ -58,65 +72,81 @@ func TestErrorsParse(t *testing.T) {
|
||||
}{
|
||||
{`errors`, false, ErrorHandler{
|
||||
ErrorPages: map[int]string{},
|
||||
Log: &httpserver.Logger{},
|
||||
}},
|
||||
{`errors errors.txt`, false, ErrorHandler{
|
||||
ErrorPages: map[int]string{},
|
||||
LogFile: "errors.txt",
|
||||
Log: &httpserver.Logger{
|
||||
Output: "errors.txt",
|
||||
Roller: httpserver.DefaultLogRoller(),
|
||||
},
|
||||
}},
|
||||
{`errors visible`, false, ErrorHandler{
|
||||
ErrorPages: map[int]string{},
|
||||
Debug: true,
|
||||
Log: &httpserver.Logger{},
|
||||
}},
|
||||
{`errors { log visible }`, false, ErrorHandler{
|
||||
ErrorPages: map[int]string{},
|
||||
Debug: true,
|
||||
}},
|
||||
{`errors { log errors.txt
|
||||
{`errors errors.txt {
|
||||
404 404.html
|
||||
500 500.html
|
||||
}`, 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, ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
LogRoller: &httpserver.LogRoller{
|
||||
MaxSize: 2,
|
||||
MaxAge: 10,
|
||||
MaxBackups: 3,
|
||||
LocalTime: true,
|
||||
Log: &httpserver.Logger{
|
||||
Output: "errors.txt",
|
||||
Roller: httpserver.DefaultLogRoller(),
|
||||
},
|
||||
ErrorPages: map[int]string{},
|
||||
}},
|
||||
{`errors { log errors.txt {
|
||||
size 3
|
||||
age 11
|
||||
keep 5
|
||||
}
|
||||
{`errors errors.txt {
|
||||
rotate_size 2
|
||||
rotate_age 10
|
||||
rotate_keep 3
|
||||
rotate_compress
|
||||
}`, false, ErrorHandler{
|
||||
ErrorPages: map[int]string{},
|
||||
Log: &httpserver.Logger{
|
||||
Output: "errors.txt", Roller: &httpserver.LogRoller{
|
||||
MaxSize: 2,
|
||||
MaxAge: 10,
|
||||
MaxBackups: 3,
|
||||
Compress: true,
|
||||
LocalTime: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{`errors errors.txt {
|
||||
rotate_size 3
|
||||
rotate_age 11
|
||||
rotate_keep 5
|
||||
404 404.html
|
||||
503 503.html
|
||||
}`, false, ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
503: "503.html",
|
||||
},
|
||||
LogRoller: &httpserver.LogRoller{
|
||||
MaxSize: 3,
|
||||
MaxAge: 11,
|
||||
MaxBackups: 5,
|
||||
LocalTime: true,
|
||||
Log: &httpserver.Logger{
|
||||
Output: "errors.txt",
|
||||
Roller: &httpserver.LogRoller{
|
||||
MaxSize: 3,
|
||||
MaxAge: 11,
|
||||
MaxBackups: 5,
|
||||
Compress: false,
|
||||
LocalTime: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{`errors { log errors.txt
|
||||
{`errors errors.txt {
|
||||
* generic_error.html
|
||||
404 404.html
|
||||
503 503.html
|
||||
}`, false, ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
Log: &httpserver.Logger{
|
||||
Output: "errors.txt",
|
||||
Roller: httpserver.DefaultLogRoller(),
|
||||
},
|
||||
GenericErrorPage: "generic_error.html",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
@@ -131,17 +161,31 @@ func TestErrorsParse(t *testing.T) {
|
||||
ErrorPages: map[int]string{
|
||||
404: testAbs,
|
||||
},
|
||||
Log: &httpserver.Logger{},
|
||||
}},
|
||||
{`errors errors.txt { rotate_size 2 rotate_age 10 rotate_keep 3 rotate_compress }`,
|
||||
true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
{`errors errors.txt {
|
||||
rotate_compress invalid
|
||||
}`,
|
||||
true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
// Next two test cases is the detection of duplicate status codes
|
||||
{`errors {
|
||||
503 503.html
|
||||
503 503.html
|
||||
}`, true, ErrorHandler{ErrorPages: map[int]string{}}},
|
||||
503 503.html
|
||||
503 503.html
|
||||
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
|
||||
{`errors {
|
||||
* generic_error.html
|
||||
* generic_error.html
|
||||
}`, true, ErrorHandler{ErrorPages: map[int]string{}}},
|
||||
* generic_error.html
|
||||
* generic_error.html
|
||||
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
{`errors /path error.txt {
|
||||
404
|
||||
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
|
||||
{`errors /path error.txt`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
actualErrorsRule, err := errorsParse(caddy.NewTestController("http", test.inputErrorsRules))
|
||||
|
||||
@@ -154,7 +198,7 @@ func TestErrorsParse(t *testing.T) {
|
||||
}
|
||||
if !reflect.DeepEqual(actualErrorsRule, &test.expectedErrorHandler) {
|
||||
t.Errorf("Test %d expect %v, but got %v", i,
|
||||
actualErrorsRule, test.expectedErrorHandler)
|
||||
test.expectedErrorHandler, actualErrorsRule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package expvar
|
||||
|
||||
import (
|
||||
@@ -5,7 +19,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// ExpVar is a simple struct to hold expvar's configuration
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package expvar
|
||||
|
||||
import (
|
||||
@@ -6,7 +20,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestExpVar(t *testing.T) {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package expvar
|
||||
|
||||
import (
|
||||
@@ -5,8 +19,8 @@ import (
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package expvar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
|
||||
+18
-12
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package extensions contains middleware for clean URLs.
|
||||
//
|
||||
// The root path of the site is passed in as well as possible extensions
|
||||
@@ -12,7 +26,7 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Ext can assume an extension from clean URLs.
|
||||
@@ -31,9 +45,10 @@ type Ext struct {
|
||||
// ServeHTTP implements the httpserver.Handler interface.
|
||||
func (e Ext) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
urlpath := strings.TrimSuffix(r.URL.Path, "/")
|
||||
if path.Ext(urlpath) == "" && len(r.URL.Path) > 0 && r.URL.Path[len(r.URL.Path)-1] != '/' {
|
||||
if len(r.URL.Path) > 0 && path.Ext(urlpath) == "" && r.URL.Path[len(r.URL.Path)-1] != '/' {
|
||||
for _, ext := range e.Extensions {
|
||||
if resourceExists(e.Root, urlpath+ext) {
|
||||
_, err := os.Stat(httpserver.SafePath(e.Root, urlpath) + ext)
|
||||
if err == nil {
|
||||
r.URL.Path = urlpath + ext
|
||||
break
|
||||
}
|
||||
@@ -41,12 +56,3 @@ func (e Ext) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
}
|
||||
return e.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// resourceExists returns true if the file specified at
|
||||
// root + path exists; false otherwise.
|
||||
func resourceExists(root, path string) bool {
|
||||
_, err := os.Stat(root + path)
|
||||
// technically we should use os.IsNotExist(err)
|
||||
// but we don't handle any other kinds of errors anyway
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type dialer interface {
|
||||
Dial() (Client, error)
|
||||
Close(Client) error
|
||||
}
|
||||
|
||||
// basicDialer is a basic dialer that wraps default fcgi functions.
|
||||
type basicDialer struct {
|
||||
network string
|
||||
address string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (b basicDialer) Dial() (Client, error) {
|
||||
return DialTimeout(b.network, b.address, b.timeout)
|
||||
}
|
||||
|
||||
func (b basicDialer) Close(c Client) error { return c.Close() }
|
||||
|
||||
// persistentDialer keeps a pool of fcgi connections.
|
||||
// connections are not closed after use, rather added back to the pool for reuse.
|
||||
type persistentDialer struct {
|
||||
size int
|
||||
network string
|
||||
address string
|
||||
timeout time.Duration
|
||||
pool []Client
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (p *persistentDialer) Dial() (Client, error) {
|
||||
p.Lock()
|
||||
// connection is available, return first one.
|
||||
if len(p.pool) > 0 {
|
||||
client := p.pool[0]
|
||||
p.pool = p.pool[1:]
|
||||
p.Unlock()
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
p.Unlock()
|
||||
|
||||
// no connection available, create new one
|
||||
return DialTimeout(p.network, p.address, p.timeout)
|
||||
}
|
||||
|
||||
func (p *persistentDialer) Close(client Client) error {
|
||||
p.Lock()
|
||||
if len(p.pool) < p.size {
|
||||
// pool is not full yet, add connection for reuse
|
||||
p.pool = append(p.pool, client)
|
||||
p.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
p.Unlock()
|
||||
|
||||
// otherwise, close the connection.
|
||||
return client.Close()
|
||||
}
|
||||
|
||||
type loadBalancingDialer struct {
|
||||
current int64
|
||||
dialers []dialer
|
||||
}
|
||||
|
||||
func (m *loadBalancingDialer) Dial() (Client, error) {
|
||||
nextDialerIndex := atomic.AddInt64(&m.current, 1) % int64(len(m.dialers))
|
||||
currentDialer := m.dialers[nextDialerIndex]
|
||||
|
||||
client, err := currentDialer.Dial()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dialerAwareClient{Client: client, dialer: currentDialer}, nil
|
||||
}
|
||||
|
||||
func (m *loadBalancingDialer) Close(c Client) error {
|
||||
// Close the client according to dialer behaviour
|
||||
if da, ok := c.(*dialerAwareClient); ok {
|
||||
return da.dialer.Close(c)
|
||||
}
|
||||
|
||||
return errors.New("Cannot close client")
|
||||
}
|
||||
|
||||
type dialerAwareClient struct {
|
||||
Client
|
||||
dialer dialer
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadbalancingDialer(t *testing.T) {
|
||||
// given
|
||||
runs := 100
|
||||
mockDialer1 := new(mockDialer)
|
||||
mockDialer2 := new(mockDialer)
|
||||
|
||||
dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1, mockDialer2}}
|
||||
|
||||
// when
|
||||
for i := 0; i < runs; i++ {
|
||||
client, err := dialer.Dial()
|
||||
dialer.Close(client)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected error to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
// then
|
||||
if mockDialer1.dialCalled != mockDialer2.dialCalled && mockDialer1.dialCalled != 50 {
|
||||
t.Errorf("Expected dialer to call Dial() on multiple backend dialers %d times [actual: %d, %d]", 50, mockDialer1.dialCalled, mockDialer2.dialCalled)
|
||||
}
|
||||
|
||||
if mockDialer1.closeCalled != mockDialer2.closeCalled && mockDialer1.closeCalled != 50 {
|
||||
t.Errorf("Expected dialer to call Close() on multiple backend dialers %d times [actual: %d, %d]", 50, mockDialer1.closeCalled, mockDialer2.closeCalled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBalancingDialerShouldReturnDialerAwareClient(t *testing.T) {
|
||||
// given
|
||||
mockDialer1 := new(mockDialer)
|
||||
dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1}}
|
||||
|
||||
// when
|
||||
client, err := dialer.Dial()
|
||||
|
||||
// then
|
||||
if err != nil {
|
||||
t.Errorf("Expected error to be nil")
|
||||
}
|
||||
|
||||
if awareClient, ok := client.(*dialerAwareClient); !ok {
|
||||
t.Error("Expected dialer to wrap client")
|
||||
} else {
|
||||
if awareClient.dialer != mockDialer1 {
|
||||
t.Error("Expected wrapped client to have reference to dialer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBalancingDialerShouldUnderlyingReturnDialerError(t *testing.T) {
|
||||
// given
|
||||
mockDialer1 := new(errorReturningDialer)
|
||||
dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1}}
|
||||
|
||||
// when
|
||||
_, err := dialer.Dial()
|
||||
|
||||
// then
|
||||
if err.Error() != "Error during dial" {
|
||||
t.Errorf("Expected 'Error during dial', got: '%s'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBalancingDialerShouldCloseClient(t *testing.T) {
|
||||
// given
|
||||
mockDialer1 := new(mockDialer)
|
||||
mockDialer2 := new(mockDialer)
|
||||
|
||||
dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1, mockDialer2}}
|
||||
client, _ := dialer.Dial()
|
||||
|
||||
// when
|
||||
err := dialer.Close(client)
|
||||
|
||||
// then
|
||||
if err != nil {
|
||||
t.Error("Expected error not to occur")
|
||||
}
|
||||
|
||||
// load balancing starts from index 1
|
||||
if mockDialer2.client != client {
|
||||
t.Errorf("Expected Close() to be called on referenced dialer")
|
||||
}
|
||||
}
|
||||
|
||||
type mockDialer struct {
|
||||
dialCalled int
|
||||
closeCalled int
|
||||
client Client
|
||||
}
|
||||
|
||||
type mockClient struct {
|
||||
Client
|
||||
}
|
||||
|
||||
func (m *mockDialer) Dial() (Client, error) {
|
||||
m.dialCalled++
|
||||
return mockClient{Client: &FCGIClient{}}, nil
|
||||
}
|
||||
|
||||
func (m *mockDialer) Close(c Client) error {
|
||||
m.client = c
|
||||
m.closeCalled++
|
||||
return nil
|
||||
}
|
||||
|
||||
type errorReturningDialer struct {
|
||||
client Client
|
||||
}
|
||||
|
||||
func (m *errorReturningDialer) Dial() (Client, error) {
|
||||
return mockClient{Client: &FCGIClient{}}, errors.New("Error during dial")
|
||||
}
|
||||
|
||||
func (m *errorReturningDialer) Close(c Client) error {
|
||||
m.client = c
|
||||
return errors.New("Error during close")
|
||||
}
|
||||
+182
-46
@@ -1,21 +1,43 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package fastcgi has middleware that acts as a FastCGI client. Requests
|
||||
// that get forwarded to FastCGI stop the middleware execution chain.
|
||||
// The most common use for this package is to serve PHP websites via php-fpm.
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
)
|
||||
|
||||
// Handler is a middleware type that can handle requests as a FastCGI client.
|
||||
@@ -32,17 +54,28 @@ type Handler struct {
|
||||
ServerPort string
|
||||
}
|
||||
|
||||
// When a rewrite is performed, a header field of this name
|
||||
// is added to the request
|
||||
// It contains the original request URI before the rewrite.
|
||||
const internalRewriteFieldName = "Caddy-Rewrite-Original-URI"
|
||||
|
||||
// ServeHTTP satisfies the httpserver.Handler interface.
|
||||
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
for _, rule := range h.Rules {
|
||||
|
||||
// First requirement: Base path must match and the path must be allowed.
|
||||
if !httpserver.Path(r.URL.Path).Matches(rule.Path) || !rule.AllowedPath(r.URL.Path) {
|
||||
// First requirement: Base path must match request path. If it doesn't,
|
||||
// we check to make sure the leading slash is not missing, and if so,
|
||||
// we check again with it prepended. This is in case people forget
|
||||
// a leading slash when performing rewrites, and we don't want to expose
|
||||
// the contents of the (likely PHP) script. See issue #1645.
|
||||
hpath := httpserver.Path(r.URL.Path)
|
||||
if !hpath.Matches(rule.Path) {
|
||||
if strings.HasPrefix(string(hpath), "/") {
|
||||
// this is a normal-looking path, and it doesn't match; try next rule
|
||||
continue
|
||||
}
|
||||
hpath = httpserver.Path("/" + string(hpath)) // prepend leading slash
|
||||
if !hpath.Matches(rule.Path) {
|
||||
// even after fixing the request path, it still doesn't match; try next rule
|
||||
continue
|
||||
}
|
||||
}
|
||||
// The path must also be allowed (not ignored).
|
||||
if !rule.AllowedPath(r.URL.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -52,6 +85,8 @@ 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
|
||||
// We trim those characters because they are served as plain text if appended after .php on Windows
|
||||
fpath = strings.TrimRight(fpath, " .")
|
||||
|
||||
if idx, ok := httpserver.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
|
||||
fpath = idx
|
||||
@@ -69,7 +104,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
}
|
||||
|
||||
// These criteria work well in this order for PHP sites
|
||||
if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, rule.Ext) {
|
||||
// We lower path and Ext as on Windows, the system is case insensitive, so .PHP is served as .php
|
||||
if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(strings.ToLower(fpath), strings.ToLower(rule.Ext)) {
|
||||
|
||||
// Create environment for CGI script
|
||||
env, err := h.buildEnv(r, rule, fpath)
|
||||
@@ -78,30 +114,57 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
}
|
||||
|
||||
// Connect to FastCGI gateway
|
||||
fcgiBackend, err := rule.dialer.Dial()
|
||||
address, err := rule.Address()
|
||||
if err != nil {
|
||||
return http.StatusBadGateway, err
|
||||
}
|
||||
network, address := parseAddress(address)
|
||||
|
||||
ctx := context.Background()
|
||||
if rule.ConnectTimeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, rule.ConnectTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
fcgiBackend, err := DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
return http.StatusGatewayTimeout, err
|
||||
}
|
||||
return http.StatusBadGateway, err
|
||||
}
|
||||
defer fcgiBackend.Close()
|
||||
fcgiBackend.SetReadTimeout(rule.ReadTimeout)
|
||||
fcgiBackend.SetSendTimeout(rule.SendTimeout)
|
||||
|
||||
// read/write timeouts
|
||||
if err := fcgiBackend.SetReadTimeout(rule.ReadTimeout); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
if err := fcgiBackend.SetSendTimeout(rule.SendTimeout); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
contentLength, _ := strconv.Atoi(r.Header.Get("Content-Length"))
|
||||
|
||||
var contentLength int64
|
||||
// if ContentLength is already set
|
||||
if r.ContentLength > 0 {
|
||||
contentLength = r.ContentLength
|
||||
} else {
|
||||
contentLength, _ = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
|
||||
}
|
||||
switch r.Method {
|
||||
case "HEAD":
|
||||
resp, err = fcgiBackend.Head(env)
|
||||
case "GET":
|
||||
resp, err = fcgiBackend.Get(env)
|
||||
resp, err = fcgiBackend.Get(env, r.Body, contentLength)
|
||||
case "OPTIONS":
|
||||
resp, err = fcgiBackend.Options(env)
|
||||
default:
|
||||
resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength)
|
||||
}
|
||||
|
||||
if resp != nil && resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
return http.StatusGatewayTimeout, err
|
||||
@@ -120,9 +183,9 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
}
|
||||
|
||||
// Log any stderr output from upstream
|
||||
if stderr := fcgiBackend.StdErr(); stderr.Len() != 0 {
|
||||
if fcgiBackend.stderr.Len() != 0 {
|
||||
// Remove trailing newline, error logger already does this.
|
||||
err = LogError(strings.TrimSuffix(stderr.String(), "\n"))
|
||||
err = LogError(strings.TrimSuffix(fcgiBackend.stderr.String(), "\n"))
|
||||
}
|
||||
|
||||
// Normally we would return the status code if it is an error status (>= 400),
|
||||
@@ -182,9 +245,6 @@ func (h Handler) exists(path string) bool {
|
||||
func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]string, error) {
|
||||
var env map[string]string
|
||||
|
||||
// Get absolute path of requested resource
|
||||
absPath := filepath.Join(rule.Root, fpath)
|
||||
|
||||
// Separate remote IP and port; more lenient than net.SplitHostPort
|
||||
var ip, port string
|
||||
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 {
|
||||
@@ -206,24 +266,36 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
|
||||
docURI := fpath[:splitPos+len(rule.SplitPath)]
|
||||
pathInfo := fpath[splitPos+len(rule.SplitPath):]
|
||||
scriptName := fpath
|
||||
scriptFilename := absPath
|
||||
|
||||
// Strip PATH_INFO from SCRIPT_NAME
|
||||
scriptName = strings.TrimSuffix(scriptName, pathInfo)
|
||||
|
||||
// Get the request URI. The request URI might be as it came in over the wire,
|
||||
// or it might have been rewritten internally by the rewrite middleware (see issue #256).
|
||||
// If it was rewritten, there will be a header indicating the original URL,
|
||||
// which is needed to get the correct RequestURI value for PHP apps.
|
||||
reqURI := r.URL.RequestURI()
|
||||
if origURI := r.Header.Get(internalRewriteFieldName); origURI != "" {
|
||||
reqURI = origURI
|
||||
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
||||
scriptFilename := filepath.Join(rule.Root, scriptName)
|
||||
|
||||
// Add vhost path prefix to scriptName. Otherwise, some PHP software will
|
||||
// have difficulty discovering its URL.
|
||||
pathPrefix, _ := r.Context().Value(caddy.CtxKey("path_prefix")).(string)
|
||||
scriptName = path.Join(pathPrefix, scriptName)
|
||||
|
||||
// Get the request URI from context. The context stores the original URI in case
|
||||
// it was changed by a middleware such as rewrite. By default, we pass the
|
||||
// original URI in as the value of REQUEST_URI (the user can overwrite this
|
||||
// if desired). Most PHP apps seem to want the original URI. Besides, this is
|
||||
// how nginx defaults: http://stackoverflow.com/a/12485156/1048862
|
||||
reqURL, _ := r.Context().Value(httpserver.OriginalURLCtxKey).(url.URL)
|
||||
|
||||
// Retrieve name of remote user that was set by some downstream middleware such as basicauth.
|
||||
remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string)
|
||||
|
||||
requestScheme := "http"
|
||||
if r.TLS != nil {
|
||||
requestScheme = "https"
|
||||
}
|
||||
|
||||
// Some variables are unused but cleared explicitly to prevent
|
||||
// the parent environment from interfering.
|
||||
env = map[string]string{
|
||||
|
||||
// Variables defined in CGI 1.1 spec
|
||||
"AUTH_TYPE": "", // Not used
|
||||
"CONTENT_LENGTH": r.Header.Get("Content-Length"),
|
||||
@@ -235,8 +307,9 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
|
||||
"REMOTE_HOST": ip, // For speed, remote host lookups disabled
|
||||
"REMOTE_PORT": port,
|
||||
"REMOTE_IDENT": "", // Not used
|
||||
"REMOTE_USER": "", // Not used
|
||||
"REMOTE_USER": remoteUser,
|
||||
"REQUEST_METHOD": r.Method,
|
||||
"REQUEST_SCHEME": requestScheme,
|
||||
"SERVER_NAME": h.ServerName,
|
||||
"SERVER_PORT": h.ServerPort,
|
||||
"SERVER_PROTOCOL": r.Proto,
|
||||
@@ -246,13 +319,13 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
|
||||
"DOCUMENT_ROOT": rule.Root,
|
||||
"DOCUMENT_URI": docURI,
|
||||
"HTTP_HOST": r.Host, // added here, since not always part of headers
|
||||
"REQUEST_URI": reqURI,
|
||||
"REQUEST_URI": reqURL.RequestURI(),
|
||||
"SCRIPT_FILENAME": scriptFilename,
|
||||
"SCRIPT_NAME": scriptName,
|
||||
}
|
||||
|
||||
// compliance with the CGI specification that PATH_TRANSLATED
|
||||
// should only exist if PATH_INFO is defined.
|
||||
// compliance with the CGI specification requires that
|
||||
// PATH_TRANSLATED should only exist if PATH_INFO is defined.
|
||||
// Info: https://www.ietf.org/rfc/rfc3875 Page 14
|
||||
if env["PATH_INFO"] != "" {
|
||||
env["PATH_TRANSLATED"] = filepath.Join(rule.Root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
|
||||
@@ -261,20 +334,29 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
|
||||
// Some web apps rely on knowing HTTPS or not
|
||||
if r.TLS != nil {
|
||||
env["HTTPS"] = "on"
|
||||
// and pass the protocol details in a manner compatible with apache's mod_ssl
|
||||
// (which is why they have a SSL_ prefix and not TLS_).
|
||||
v, ok := tlsProtocolStringToMap[r.TLS.Version]
|
||||
if ok {
|
||||
env["SSL_PROTOCOL"] = v
|
||||
}
|
||||
// and pass the cipher suite in a manner compatible with apache's mod_ssl
|
||||
for k, v := range caddytls.SupportedCiphersMap {
|
||||
if v == r.TLS.CipherSuite {
|
||||
env["SSL_CIPHER"] = k
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add env variables from config (with support for placeholders in values)
|
||||
replacer := httpserver.NewReplacer(r, nil, "")
|
||||
// Add env variables from config
|
||||
for _, envVar := range rule.EnvVars {
|
||||
// replace request placeholders in environment variables
|
||||
env[envVar[0]] = replacer.Replace(envVar[1])
|
||||
}
|
||||
|
||||
// Add all HTTP headers (except Caddy-Rewrite-Original-URI ) to env variables
|
||||
// Add all HTTP headers to env variables
|
||||
for field, val := range r.Header {
|
||||
if strings.ToLower(field) == strings.ToLower(internalRewriteFieldName) {
|
||||
continue
|
||||
}
|
||||
header := strings.ToUpper(field)
|
||||
header = headerNameReplacer.Replace(header)
|
||||
env["HTTP_"+header] = strings.Join(val, ", ")
|
||||
@@ -288,8 +370,8 @@ type Rule struct {
|
||||
// The base path to match. Required.
|
||||
Path string
|
||||
|
||||
// The address of the FastCGI server. Required.
|
||||
Address string
|
||||
// upstream load balancer
|
||||
balancer
|
||||
|
||||
// Always process files with this extension with fastcgi.
|
||||
Ext string
|
||||
@@ -314,14 +396,60 @@ type Rule struct {
|
||||
// Ignored paths
|
||||
IgnoredSubPaths []string
|
||||
|
||||
// The duration used to set a deadline when connecting to an upstream.
|
||||
ConnectTimeout time.Duration
|
||||
|
||||
// The duration used to set a deadline when reading from the FastCGI server.
|
||||
ReadTimeout time.Duration
|
||||
|
||||
// The duration used to set a deadline when sending to the FastCGI server.
|
||||
SendTimeout time.Duration
|
||||
}
|
||||
|
||||
// FCGI dialer
|
||||
dialer dialer
|
||||
// balancer is a fastcgi upstream load balancer.
|
||||
type balancer interface {
|
||||
// Address picks an upstream address from the
|
||||
// underlying load balancer.
|
||||
Address() (string, error)
|
||||
}
|
||||
|
||||
// roundRobin is a round robin balancer for fastcgi upstreams.
|
||||
type roundRobin struct {
|
||||
// Known Go bug: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
||||
// must be first field for 64 bit alignment
|
||||
// on x86 and arm.
|
||||
index int64
|
||||
addresses []string
|
||||
}
|
||||
|
||||
func (r *roundRobin) Address() (string, error) {
|
||||
index := atomic.AddInt64(&r.index, 1) % int64(len(r.addresses))
|
||||
return r.addresses[index], nil
|
||||
}
|
||||
|
||||
// srvResolver is a private interface used to abstract
|
||||
// the DNS resolver. It is mainly used to facilitate testing.
|
||||
type srvResolver interface {
|
||||
LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error)
|
||||
}
|
||||
|
||||
// srv is a service locator for fastcgi upstreams
|
||||
type srv struct {
|
||||
resolver srvResolver
|
||||
service string
|
||||
}
|
||||
|
||||
// Address looks up the service and returns the address:port
|
||||
// from first result in resolved list.
|
||||
// No explicit balancing is required because net.LookupSRV
|
||||
// sorts the results by priority and randomizes within priority.
|
||||
func (s *srv) Address() (string, error) {
|
||||
_, addrs, err := s.resolver.LookupSRV(context.Background(), "", "", s.service)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%d", strings.TrimRight(addrs[0].Target, "."), addrs[0].Port), nil
|
||||
}
|
||||
|
||||
// canSplit checks if path can split into two based on rule.SplitPath.
|
||||
@@ -361,3 +489,11 @@ type LogError string
|
||||
func (l LogError) Error() string {
|
||||
return string(l)
|
||||
}
|
||||
|
||||
// Map of supported protocols to Apache ssl_mod format
|
||||
// Note that these are slightly different from SupportedProtocols in caddytls/config.go's
|
||||
var tlsProtocolStringToMap = map[uint16]string{
|
||||
tls.VersionTLS10: "TLSv1",
|
||||
tls.VersionTLS11: "TLSv1.1",
|
||||
tls.VersionTLS12: "TLSv1.2",
|
||||
}
|
||||
|
||||
+131
-157
@@ -1,16 +1,35 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/fcgi"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
@@ -21,22 +40,24 @@ func TestServeHTTP(t *testing.T) {
|
||||
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))
|
||||
}))
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
go func() {
|
||||
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Length", bodyLenStr)
|
||||
_, err := w.Write([]byte(body))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] unable to write header: %v", err)
|
||||
}
|
||||
}))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] unable to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
network, address := parseAddress(listener.Addr().String())
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
Rules: []Rule{
|
||||
{
|
||||
Path: "/",
|
||||
Address: listener.Addr().String(),
|
||||
dialer: basicDialer{network: network, address: address},
|
||||
},
|
||||
},
|
||||
Next: nil,
|
||||
Rules: []Rule{{Path: "/", balancer: address(listener.Addr().String())}},
|
||||
}
|
||||
r, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
@@ -60,120 +81,29 @@ func TestServeHTTP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// connectionCounter in fact is a listener with an added counter to keep track
|
||||
// of the number of accepted connections.
|
||||
type connectionCounter struct {
|
||||
net.Listener
|
||||
sync.Mutex
|
||||
counter int
|
||||
}
|
||||
|
||||
func (l *connectionCounter) Accept() (net.Conn, error) {
|
||||
l.Lock()
|
||||
l.counter++
|
||||
l.Unlock()
|
||||
return l.Listener.Accept()
|
||||
}
|
||||
|
||||
// TestPersistent ensures that persistent
|
||||
// as well as the non-persistent fastCGI servers
|
||||
// send the answers corresnponding to the correct request.
|
||||
// It also checks the number of tcp connections used.
|
||||
func TestPersistent(t *testing.T) {
|
||||
numberOfRequests := 32
|
||||
|
||||
for _, poolsize := range []int{0, 1, 5, numberOfRequests} {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create listener for test: %v", err)
|
||||
}
|
||||
|
||||
listener := &connectionCounter{l, *new(sync.Mutex), 0}
|
||||
|
||||
// this fcgi server replies with the request URL
|
||||
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body := "This answers a request to " + r.URL.Path
|
||||
bodyLenStr := strconv.Itoa(len(body))
|
||||
|
||||
w.Header().Set("Content-Length", bodyLenStr)
|
||||
w.Write([]byte(body))
|
||||
}))
|
||||
|
||||
network, address := parseAddress(listener.Addr().String())
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
Rules: []Rule{{Path: "/", Address: listener.Addr().String(), dialer: &persistentDialer{size: poolsize, network: network, address: address}}},
|
||||
}
|
||||
|
||||
var semaphore sync.WaitGroup
|
||||
serialMutex := new(sync.Mutex)
|
||||
|
||||
serialCounter := 0
|
||||
parallelCounter := 0
|
||||
// make some serial followed by some
|
||||
// parallel requests to challenge the handler
|
||||
for _, serialize := range []bool{true, false, false, false} {
|
||||
if serialize {
|
||||
serialCounter++
|
||||
} else {
|
||||
parallelCounter++
|
||||
}
|
||||
semaphore.Add(numberOfRequests)
|
||||
|
||||
for i := 0; i < numberOfRequests; i++ {
|
||||
go func(i int, serialize bool) {
|
||||
defer semaphore.Done()
|
||||
if serialize {
|
||||
serialMutex.Lock()
|
||||
defer serialMutex.Unlock()
|
||||
}
|
||||
r, err := http.NewRequest("GET", "/"+strconv.Itoa(i), nil)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to create request: %v", err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
status, err := handler.ServeHTTP(w, r)
|
||||
|
||||
if status != 0 {
|
||||
t.Errorf("Handler(pool: %v) return status %v", poolsize, status)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Handler(pool: %v) Error: %v", poolsize, err)
|
||||
}
|
||||
want := "This answers a request to /" + strconv.Itoa(i)
|
||||
if got := w.Body.String(); got != want {
|
||||
t.Errorf("Expected response from handler(pool: %v) to be '%s', got: '%s'", poolsize, want, got)
|
||||
}
|
||||
}(i, serialize)
|
||||
} //next request
|
||||
semaphore.Wait()
|
||||
} // next set of requests (serial/parallel)
|
||||
|
||||
listener.Close()
|
||||
t.Logf("The pool: %v test used %v tcp connections to answer %v * %v serial and %v * %v parallel requests.", poolsize, listener.counter, serialCounter, numberOfRequests, parallelCounter, numberOfRequests)
|
||||
} // next handler (persistent/non-persistent)
|
||||
}
|
||||
|
||||
func TestRuleParseAddress(t *testing.T) {
|
||||
getClientTestTable := []struct {
|
||||
rule *Rule
|
||||
expectednetwork string
|
||||
expectedaddress string
|
||||
}{
|
||||
{&Rule{Address: "tcp://172.17.0.1:9000"}, "tcp", "172.17.0.1:9000"},
|
||||
{&Rule{Address: "fastcgi://localhost:9000"}, "tcp", "localhost:9000"},
|
||||
{&Rule{Address: "172.17.0.15"}, "tcp", "172.17.0.15"},
|
||||
{&Rule{Address: "/my/unix/socket"}, "unix", "/my/unix/socket"},
|
||||
{&Rule{Address: "unix:/second/unix/socket"}, "unix", "/second/unix/socket"},
|
||||
{&Rule{balancer: address("tcp://172.17.0.1:9000")}, "tcp", "172.17.0.1:9000"},
|
||||
{&Rule{balancer: address("fastcgi://localhost:9000")}, "tcp", "localhost:9000"},
|
||||
{&Rule{balancer: address("172.17.0.15")}, "tcp", "172.17.0.15"},
|
||||
{&Rule{balancer: address("/my/unix/socket")}, "unix", "/my/unix/socket"},
|
||||
{&Rule{balancer: address("unix:/second/unix/socket")}, "unix", "/second/unix/socket"},
|
||||
}
|
||||
|
||||
for _, entry := range getClientTestTable {
|
||||
if actualnetwork, _ := parseAddress(entry.rule.Address); actualnetwork != entry.expectednetwork {
|
||||
t.Errorf("Unexpected network for address string %v. Got %v, expected %v", entry.rule.Address, actualnetwork, entry.expectednetwork)
|
||||
addr, err := entry.rule.Address()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error in retrieving address: %s", err.Error())
|
||||
}
|
||||
if _, actualaddress := parseAddress(entry.rule.Address); actualaddress != entry.expectedaddress {
|
||||
t.Errorf("Unexpected parsed address for address string %v. Got %v, expected %v", entry.rule.Address, actualaddress, entry.expectedaddress)
|
||||
if actualnetwork, _ := parseAddress(addr); actualnetwork != entry.expectednetwork {
|
||||
t.Errorf("Unexpected network for address string %v. Got %v, expected %v", addr, actualnetwork, entry.expectednetwork)
|
||||
}
|
||||
if _, actualaddress := parseAddress(addr); actualaddress != entry.expectedaddress {
|
||||
t.Errorf("Unexpected parsed address for address string %v. Got %v, expected %v", addr, actualaddress, entry.expectedaddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,16 +152,20 @@ func TestBuildEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
rule := Rule{}
|
||||
url, err := url.Parse("http://localhost:2015/fgci_test.php?test=blabla")
|
||||
rule := Rule{
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
IndexFiles: []string{"index.php"},
|
||||
}
|
||||
u, err := url.Parse("http://localhost:2015/fgci_test.php?test=foobar")
|
||||
if err != nil {
|
||||
t.Error("Unexpected error:", err.Error())
|
||||
}
|
||||
|
||||
var newReq = func() *http.Request {
|
||||
return &http.Request{
|
||||
r := http.Request{
|
||||
Method: "GET",
|
||||
URL: url,
|
||||
URL: u,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
@@ -242,6 +176,8 @@ func TestBuildEnv(t *testing.T) {
|
||||
"Foo": {"Bar", "two"},
|
||||
},
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), httpserver.OriginalURLCtxKey, *r.URL)
|
||||
return r.WithContext(ctx)
|
||||
}
|
||||
|
||||
fpath := "/fgci_test.php"
|
||||
@@ -251,9 +187,10 @@ func TestBuildEnv(t *testing.T) {
|
||||
"REMOTE_ADDR": "2b02:1810:4f2d:9400:70ab:f822:be8a:9093",
|
||||
"REMOTE_PORT": "51688",
|
||||
"SERVER_PROTOCOL": "HTTP/1.1",
|
||||
"QUERY_STRING": "test=blabla",
|
||||
"QUERY_STRING": "test=foobar",
|
||||
"REQUEST_METHOD": "GET",
|
||||
"HTTP_HOST": "localhost:2015",
|
||||
"SCRIPT_NAME": "/fgci_test.php",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,29 +238,32 @@ func TestBuildEnv(t *testing.T) {
|
||||
}
|
||||
envExpected = newEnv()
|
||||
envExpected["HTTP_HOST"] = "localhost:2015"
|
||||
envExpected["CUSTOM_URI"] = "custom_uri/fgci_test.php?test=blabla"
|
||||
envExpected["CUSTOM_QUERY"] = "custom=true&test=blabla"
|
||||
envExpected["CUSTOM_URI"] = "custom_uri/fgci_test.php?test=foobar"
|
||||
envExpected["CUSTOM_QUERY"] = "custom=true&test=foobar"
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
|
||||
// 6. Test Caddy-Rewrite-Original-URI header is not removed
|
||||
// 6. Test SCRIPT_NAME includes path prefix
|
||||
r = newReq()
|
||||
rule.EnvVars = [][2]string{
|
||||
{"HTTP_HOST", "{host}"},
|
||||
{"CUSTOM_URI", "custom_uri{uri}"},
|
||||
{"CUSTOM_QUERY", "custom=true&{query}"},
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), caddy.CtxKey("path_prefix"), "/test")
|
||||
r = r.WithContext(ctx)
|
||||
envExpected = newEnv()
|
||||
envExpected["HTTP_HOST"] = "localhost:2015"
|
||||
envExpected["CUSTOM_URI"] = "custom_uri/fgci_test.php?test=blabla"
|
||||
envExpected["CUSTOM_QUERY"] = "custom=true&test=blabla"
|
||||
httpFieldName := strings.ToUpper(internalRewriteFieldName)
|
||||
envExpected["HTTP_"+httpFieldName] = ""
|
||||
r.Header.Add(internalRewriteFieldName, "/apath/torewrite/index.php")
|
||||
envExpected["SCRIPT_NAME"] = "/test/fgci_test.php"
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
if r.Header.Get(internalRewriteFieldName) == "" {
|
||||
t.Errorf("Error: Header Expected %v", internalRewriteFieldName)
|
||||
}
|
||||
|
||||
// 7. Test SCRIPT_NAME,SCRIPT_FILENAME do not include PATH_INFO
|
||||
fpath = "/fgci_test.php/extra/paths"
|
||||
r = newReq()
|
||||
envExpected = newEnv()
|
||||
envExpected["PATH_INFO"] = "/extra/paths"
|
||||
envExpected["SCRIPT_NAME"] = "/fgci_test.php"
|
||||
envExpected["SCRIPT_FILENAME"] = filepath.FromSlash("/fgci_test.php")
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
|
||||
// 8. Test REQUEST_SCHEME in env
|
||||
r = newReq()
|
||||
envExpected = newEnv()
|
||||
envExpected["REQUEST_SCHEME"] = "http"
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
}
|
||||
|
||||
func TestReadTimeout(t *testing.T) {
|
||||
@@ -344,16 +284,14 @@ func TestReadTimeout(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
network, address := parseAddress(listener.Addr().String())
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
Rules: []Rule{
|
||||
{
|
||||
Path: "/",
|
||||
Address: listener.Addr().String(),
|
||||
dialer: basicDialer{network: network, address: address},
|
||||
balancer: address(listener.Addr().String()),
|
||||
ReadTimeout: test.readTimeout,
|
||||
},
|
||||
},
|
||||
@@ -365,11 +303,16 @@ func TestReadTimeout(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
wg.Add(1)
|
||||
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(test.sleep)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
wg.Done()
|
||||
}))
|
||||
go func() {
|
||||
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(test.sleep)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
wg.Done()
|
||||
}))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] unable to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
got, err := handler.ServeHTTP(w, r)
|
||||
if test.shouldErr {
|
||||
@@ -406,16 +349,14 @@ func TestSendTimeout(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
network, address := parseAddress(listener.Addr().String())
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
Rules: []Rule{
|
||||
{
|
||||
Path: "/",
|
||||
Address: listener.Addr().String(),
|
||||
dialer: basicDialer{network: network, address: address},
|
||||
balancer: address(listener.Addr().String()),
|
||||
SendTimeout: test.sendTimeout,
|
||||
},
|
||||
},
|
||||
@@ -426,9 +367,14 @@ func TestSendTimeout(t *testing.T) {
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
go func() {
|
||||
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] unable to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
got, err := handler.ServeHTTP(w, r)
|
||||
if test.shouldErr {
|
||||
@@ -448,3 +394,31 @@ func TestSendTimeout(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBalancer(t *testing.T) {
|
||||
tests := [][]string{
|
||||
{"localhost", "host.local"},
|
||||
{"localhost"},
|
||||
{"localhost", "host.local", "example.com"},
|
||||
{"localhost", "host.local", "example.com", "127.0.0.1"},
|
||||
}
|
||||
for i, test := range tests {
|
||||
b := address(test...)
|
||||
for _, host := range test {
|
||||
a, err := b.Address()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error in trying to retrieve address: %s", err.Error())
|
||||
}
|
||||
if a != host {
|
||||
t.Errorf("Test %d: expected %s, found %s", i, host, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func address(addresses ...string) balancer {
|
||||
return &roundRobin{
|
||||
addresses: addresses,
|
||||
index: -1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Forked Jan. 2015 from http://bitbucket.org/PinIdea/fcgi_client
|
||||
// (which is forked from https://code.google.com/p/go-fastcgi-client/)
|
||||
|
||||
@@ -13,6 +27,7 @@ package fastcgi
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
@@ -107,18 +122,6 @@ const (
|
||||
maxPad = 255
|
||||
)
|
||||
|
||||
// Client interface
|
||||
type Client interface {
|
||||
Get(pair map[string]string) (response *http.Response, err error)
|
||||
Head(pair map[string]string) (response *http.Response, err error)
|
||||
Options(pairs map[string]string) (response *http.Response, err error)
|
||||
Post(pairs map[string]string, method string, bodyType string, body io.Reader, contentLength int) (response *http.Response, err error)
|
||||
Close() error
|
||||
StdErr() bytes.Buffer
|
||||
SetReadTimeout(time.Duration) error
|
||||
SetSendTimeout(time.Duration) error
|
||||
}
|
||||
|
||||
type header struct {
|
||||
Version uint8
|
||||
Type uint8
|
||||
@@ -150,7 +153,7 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
|
||||
return
|
||||
}
|
||||
if rec.h.Version != 1 {
|
||||
err = errInvalidHeaderVersion
|
||||
err = errors.New("fcgi: invalid header version")
|
||||
return
|
||||
}
|
||||
if rec.h.Type == EndRequest {
|
||||
@@ -172,64 +175,66 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
|
||||
// FCGIClient implements a FastCGI client, which is a standard for
|
||||
// interfacing external applications with Web servers.
|
||||
type FCGIClient struct {
|
||||
mutex sync.Mutex
|
||||
conn net.Conn
|
||||
h header
|
||||
buf bytes.Buffer
|
||||
stderr bytes.Buffer
|
||||
keepAlive bool
|
||||
reqID uint16
|
||||
readTimeout time.Duration
|
||||
sendTimeout time.Duration
|
||||
mutex sync.Mutex
|
||||
rwc io.ReadWriteCloser
|
||||
h header
|
||||
buf bytes.Buffer
|
||||
stderr bytes.Buffer
|
||||
keepAlive bool
|
||||
reqID uint16
|
||||
}
|
||||
|
||||
// DialTimeout connects to the fcgi responder at the specified network address, using default net.Dialer.
|
||||
// DialWithDialerContext connects to the fcgi responder at the specified network address, using custom net.Dialer
|
||||
// and a context.
|
||||
// See func net.Dial for a description of the network and address parameters.
|
||||
func DialTimeout(network string, address string, timeout time.Duration) (fcgi *FCGIClient, err error) {
|
||||
conn, err := net.DialTimeout(network, address, timeout)
|
||||
func DialWithDialerContext(ctx context.Context, network, address string, dialer net.Dialer) (fcgi *FCGIClient, err error) {
|
||||
var conn net.Conn
|
||||
conn, err = dialer.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fcgi = &FCGIClient{conn: conn, keepAlive: false, reqID: 1}
|
||||
fcgi = &FCGIClient{
|
||||
rwc: conn,
|
||||
keepAlive: false,
|
||||
reqID: 1,
|
||||
}
|
||||
|
||||
return fcgi, nil
|
||||
return
|
||||
}
|
||||
|
||||
// Close closes fcgi connnection.
|
||||
func (c *FCGIClient) Close() error {
|
||||
return c.conn.Close()
|
||||
// DialContext is like Dial but passes ctx to dialer.Dial.
|
||||
func DialContext(ctx context.Context, network, address string) (fcgi *FCGIClient, err error) {
|
||||
return DialWithDialerContext(ctx, network, address, net.Dialer{})
|
||||
}
|
||||
|
||||
func (c *FCGIClient) writeRecord(recType uint8, content []byte) error {
|
||||
// Dial connects to the fcgi responder at the specified network address, using default net.Dialer.
|
||||
// See func net.Dial for a description of the network and address parameters.
|
||||
func Dial(network, address string) (fcgi *FCGIClient, err error) {
|
||||
return DialContext(context.Background(), network, address)
|
||||
}
|
||||
|
||||
// Close closes fcgi connection
|
||||
func (c *FCGIClient) Close() {
|
||||
c.rwc.Close()
|
||||
}
|
||||
|
||||
func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.buf.Reset()
|
||||
c.h.init(recType, c.reqID, len(content))
|
||||
|
||||
if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := c.buf.Write(content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.sendTimeout != 0 {
|
||||
if err := c.conn.SetWriteDeadline(time.Now().Add(c.sendTimeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := c.conn.Write(c.buf.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err = c.rwc.Write(c.buf.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *FCGIClient) writeBeginRequest(role uint16, flags uint8) error {
|
||||
@@ -345,14 +350,13 @@ func (w *streamReader) Read(p []byte) (n int, err error) {
|
||||
|
||||
if len(p) > 0 {
|
||||
if len(w.buf) == 0 {
|
||||
|
||||
// filter outputs for error log
|
||||
for {
|
||||
rec := &record{}
|
||||
var buf []byte
|
||||
buf, err = rec.read(w.c.conn)
|
||||
if err == errInvalidHeaderVersion {
|
||||
continue
|
||||
} else if err != nil {
|
||||
buf, err = rec.read(w.c.rwc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// standard error output
|
||||
@@ -376,15 +380,10 @@ func (w *streamReader) Read(p []byte) (n int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// StdErr returns stderr stream
|
||||
func (c *FCGIClient) StdErr() bytes.Buffer {
|
||||
return c.stderr
|
||||
}
|
||||
|
||||
// Do made the request and returns a io.Reader that translates the data read
|
||||
// from fcgi responder out of fcgi packet before returning it.
|
||||
func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
|
||||
err = c.writeBeginRequest(uint16(Responder), FCGIKeepConn)
|
||||
err = c.writeBeginRequest(uint16(Responder), 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -396,7 +395,7 @@ func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err er
|
||||
|
||||
body := newWriter(c, Stdin)
|
||||
if req != nil {
|
||||
io.Copy(body, req)
|
||||
_, _ = io.Copy(body, req)
|
||||
}
|
||||
body.Close()
|
||||
|
||||
@@ -407,11 +406,11 @@ func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err er
|
||||
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
|
||||
// that closes FCGIClient connection.
|
||||
type clientCloser struct {
|
||||
f *FCGIClient
|
||||
*FCGIClient
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (c clientCloser) Close() error { return c.f.Close() }
|
||||
func (f clientCloser) Close() error { return f.rwc.Close() }
|
||||
|
||||
// Request returns a HTTP Response with Header and Body
|
||||
// from fcgi responder
|
||||
@@ -425,12 +424,6 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res
|
||||
tp := textproto.NewReader(rb)
|
||||
resp = new(http.Response)
|
||||
|
||||
if c.readTimeout != 0 {
|
||||
if err = c.conn.SetReadDeadline(time.Now().Add(c.readTimeout)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the response headers.
|
||||
mimeHeader, err := tp.ReadMIMEHeader()
|
||||
if err != nil && err != io.EOF {
|
||||
@@ -465,12 +458,12 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res
|
||||
}
|
||||
|
||||
// Get issues a GET request to the fcgi responder.
|
||||
func (c *FCGIClient) Get(p map[string]string) (resp *http.Response, err error) {
|
||||
func (c *FCGIClient) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) {
|
||||
|
||||
p["REQUEST_METHOD"] = "GET"
|
||||
p["CONTENT_LENGTH"] = "0"
|
||||
p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
|
||||
|
||||
return c.Request(p, nil)
|
||||
return c.Request(p, body)
|
||||
}
|
||||
|
||||
// Head issues a HEAD request to the fcgi responder.
|
||||
@@ -493,7 +486,7 @@ 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, method 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 int64) (resp *http.Response, err error) {
|
||||
if p == nil {
|
||||
p = make(map[string]string)
|
||||
}
|
||||
@@ -504,7 +497,7 @@ func (c *FCGIClient) Post(p map[string]string, method string, bodyType string, b
|
||||
p["REQUEST_METHOD"] = "POST"
|
||||
}
|
||||
|
||||
p["CONTENT_LENGTH"] = strconv.Itoa(l)
|
||||
p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
|
||||
if len(bodyType) > 0 {
|
||||
p["CONTENT_TYPE"] = bodyType
|
||||
} else {
|
||||
@@ -518,7 +511,7 @@ func (c *FCGIClient) Post(p map[string]string, method string, bodyType string, b
|
||||
// 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, "POST", "application/x-www-form-urlencoded", body, body.Len())
|
||||
return c.Post(p, "POST", "application/x-www-form-urlencoded", body, int64(body.Len()))
|
||||
}
|
||||
|
||||
// PostFile issues a POST to the fcgi responder in multipart(RFC 2046) standard,
|
||||
@@ -560,24 +553,26 @@ func (c *FCGIClient) PostFile(p map[string]string, data url.Values, file map[str
|
||||
return
|
||||
}
|
||||
|
||||
return c.Post(p, "POST", bodyType, buf, buf.Len())
|
||||
return c.Post(p, "POST", bodyType, buf, int64(buf.Len()))
|
||||
}
|
||||
|
||||
// SetReadTimeout sets the read timeout for future calls that read from the
|
||||
// fcgi responder. A zero value for t means no timeout will be set.
|
||||
func (c *FCGIClient) SetReadTimeout(t time.Duration) error {
|
||||
c.readTimeout = t
|
||||
if conn, ok := c.rwc.(net.Conn); ok && t != 0 {
|
||||
return conn.SetReadDeadline(time.Now().Add(t))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSendTimeout sets the read timeout for future calls that send data to
|
||||
// the fcgi responder. A zero value for t means no timeout will be set.
|
||||
func (c *FCGIClient) SetSendTimeout(t time.Duration) error {
|
||||
c.sendTimeout = t
|
||||
if conn, ok := c.rwc.(net.Conn); ok && t != 0 {
|
||||
return conn.SetWriteDeadline(time.Now().Add(t))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checks whether chunked is part of the encodings stack
|
||||
func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" }
|
||||
|
||||
var errInvalidHeaderVersion = errors.New("fcgi: invalid header version")
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// NOTE: These tests were adapted from the original
|
||||
// repository from which this package was forked.
|
||||
// The tests are slow (~10s) and in dire need of rewriting.
|
||||
@@ -30,7 +44,7 @@ import (
|
||||
// test fcgi protocol includes:
|
||||
// Get, Post, Post in multipart/form-data, and Post with files
|
||||
// each key should be the md5 of the value or the file uploaded
|
||||
// sepicify remote fcgi responer ip:port to test with php
|
||||
// specify remote fcgi responder ip:port to test with php
|
||||
// test failed if the remote fcgi(script) failed md5 verification
|
||||
// and output "FAILED" in response
|
||||
const (
|
||||
@@ -45,7 +59,9 @@ type FastCGIServer struct{}
|
||||
|
||||
func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
|
||||
req.ParseMultipartForm(100000000)
|
||||
if err := req.ParseMultipartForm(100000000); err != nil {
|
||||
log.Printf("[ERROR] failed to parse: %v", err)
|
||||
}
|
||||
|
||||
stat := "PASSED"
|
||||
fmt.Fprintln(resp, "-")
|
||||
@@ -54,15 +70,15 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
length := 0
|
||||
for k0, v0 := range req.Form {
|
||||
h := md5.New()
|
||||
io.WriteString(h, v0[0])
|
||||
md5 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
_, _ = io.WriteString(h, v0[0])
|
||||
_md5 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
length += len(k0)
|
||||
length += len(v0[0])
|
||||
|
||||
// echo error when key != md5(val)
|
||||
if md5 != k0 {
|
||||
fmt.Fprintln(resp, "server:err ", md5, k0)
|
||||
// echo error when key != _md5(val)
|
||||
if _md5 != k0 {
|
||||
fmt.Fprintln(resp, "server:err ", _md5, k0)
|
||||
stat = "FAILED"
|
||||
}
|
||||
}
|
||||
@@ -103,7 +119,7 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) {
|
||||
fcgi, err := DialTimeout("tcp", ipPort, 0)
|
||||
fcgi, err := Dial("tcp", ipPort)
|
||||
if err != nil {
|
||||
log.Println("err:", err)
|
||||
return
|
||||
@@ -117,7 +133,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
|
||||
if len(data) > 0 {
|
||||
length = len(data)
|
||||
rd := bytes.NewReader(data)
|
||||
resp, err = fcgi.Post(fcgiParams, "", "", rd, rd.Len())
|
||||
resp, err = fcgi.Post(fcgiParams, "", "", rd, int64(rd.Len()))
|
||||
} else if len(posts) > 0 {
|
||||
values := url.Values{}
|
||||
for k, v := range posts {
|
||||
@@ -126,7 +142,8 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
|
||||
}
|
||||
resp, err = fcgi.PostForm(fcgiParams, values)
|
||||
} else {
|
||||
resp, err = fcgi.Get(fcgiParams)
|
||||
rd := bytes.NewReader(data)
|
||||
resp, err = fcgi.Get(fcgiParams, rd, int64(rd.Len()))
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -182,8 +199,12 @@ func generateRandFile(size int) (p string, m string) {
|
||||
for i := 0; i < size/16; i++ {
|
||||
buf := make([]byte, 16)
|
||||
binary.PutVarint(buf, rand.Int63())
|
||||
fo.Write(buf)
|
||||
h.Write(buf)
|
||||
if _, err := fo.Write(buf); err != nil {
|
||||
log.Printf("[ERROR] failed to write buffer: %v\n", err)
|
||||
}
|
||||
if _, err := h.Write(buf); err != nil {
|
||||
log.Printf("[ERROR] failed to write buffer: %v\n", err)
|
||||
}
|
||||
}
|
||||
m = fmt.Sprintf("%x", h.Sum(nil))
|
||||
return
|
||||
@@ -199,12 +220,13 @@ func DisabledTest(t *testing.T) {
|
||||
go func() {
|
||||
listener, err := net.Listen("tcp", ipPort)
|
||||
if err != nil {
|
||||
// handle error
|
||||
log.Println("listener creation failed: ", err)
|
||||
}
|
||||
|
||||
srv := new(FastCGIServer)
|
||||
fcgi.Serve(listener, srv)
|
||||
if err := fcgi.Serve(listener, srv); err != nil {
|
||||
log.Print("[ERROR] failed to start server: ", err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
@@ -229,7 +251,7 @@ func DisabledTest(t *testing.T) {
|
||||
for i := 0x00; i < 0xff; i++ {
|
||||
v0 := strings.Repeat(string(i), 256)
|
||||
h := md5.New()
|
||||
io.WriteString(h, v0)
|
||||
_, _ = io.WriteString(h, v0)
|
||||
k0 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
data += k0 + "=" + url.QueryEscape(v0) + "&"
|
||||
}
|
||||
@@ -246,7 +268,7 @@ func DisabledTest(t *testing.T) {
|
||||
for i := 0x00; i < 0xff; i++ {
|
||||
v0 := strings.Repeat(string(i), 4096)
|
||||
h := md5.New()
|
||||
io.WriteString(h, v0)
|
||||
_, _ = io.WriteString(h, v0)
|
||||
k0 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
p1[k0] = v0
|
||||
}
|
||||
@@ -270,6 +292,10 @@ func DisabledTest(t *testing.T) {
|
||||
delete(f0, "m0")
|
||||
sendFcgi(1, fcgiParams, nil, nil, f0)
|
||||
|
||||
os.Remove(path0)
|
||||
os.Remove(path1)
|
||||
if err := os.Remove(path0); err != nil {
|
||||
log.Println("[ERROR] failed to remove path: ", err)
|
||||
}
|
||||
if err := os.Remove(path1); err != nil {
|
||||
log.Println("[ERROR] failed to remove path: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
+54
-43
@@ -1,17 +1,34 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
var defaultTimeout = 60 * time.Second
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("fastcgi", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
@@ -61,13 +78,20 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
|
||||
}
|
||||
|
||||
rule := Rule{
|
||||
Root: absRoot,
|
||||
Path: args[0],
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
Root: absRoot,
|
||||
Path: args[0],
|
||||
ConnectTimeout: defaultTimeout,
|
||||
ReadTimeout: defaultTimeout,
|
||||
SendTimeout: defaultTimeout,
|
||||
}
|
||||
|
||||
upstreams := []string{args[1]}
|
||||
|
||||
srvUpstream := false
|
||||
if strings.HasPrefix(upstreams[0], "srv://") {
|
||||
srvUpstream = true
|
||||
}
|
||||
|
||||
if len(args) == 3 {
|
||||
if err := fastcgiPreset(args[2], &rule); err != nil {
|
||||
return rules, err
|
||||
@@ -75,10 +99,6 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
|
||||
}
|
||||
|
||||
var err error
|
||||
var pool int
|
||||
var connectTimeout = 60 * time.Second
|
||||
var dialers []dialer
|
||||
var poolSize = -1
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
@@ -106,6 +126,10 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
|
||||
rule.IndexFiles = args
|
||||
|
||||
case "upstream":
|
||||
if srvUpstream {
|
||||
return rules, c.Err("additional upstreams are not supported with SRV upstream")
|
||||
}
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) != 1 {
|
||||
@@ -126,24 +150,11 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
|
||||
}
|
||||
rule.IgnoredSubPaths = ignoredPaths
|
||||
|
||||
case "pool":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
pool, err = strconv.Atoi(c.Val())
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
if pool >= 0 {
|
||||
poolSize = pool
|
||||
} else {
|
||||
return rules, c.Errf("positive integer expected, found %d", pool)
|
||||
}
|
||||
case "connect_timeout":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
connectTimeout, err = time.ParseDuration(c.Val())
|
||||
rule.ConnectTimeout, err = time.ParseDuration(c.Val())
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
@@ -168,32 +179,32 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, rawAddress := range upstreams {
|
||||
network, address := parseAddress(rawAddress)
|
||||
if poolSize >= 0 {
|
||||
dialers = append(dialers, &persistentDialer{
|
||||
size: poolSize,
|
||||
network: network,
|
||||
address: address,
|
||||
timeout: connectTimeout,
|
||||
})
|
||||
} else {
|
||||
dialers = append(dialers, basicDialer{
|
||||
network: network,
|
||||
address: address,
|
||||
timeout: connectTimeout,
|
||||
})
|
||||
if srvUpstream {
|
||||
balancer, err := parseSRV(upstreams[0])
|
||||
if err != nil {
|
||||
return rules, c.Err("malformed service locator string: " + err.Error())
|
||||
}
|
||||
rule.balancer = balancer
|
||||
} else {
|
||||
rule.balancer = &roundRobin{addresses: upstreams, index: -1}
|
||||
}
|
||||
|
||||
rule.dialer = &loadBalancingDialer{dialers: dialers}
|
||||
rule.Address = strings.Join(upstreams, ",")
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func parseSRV(locator string) (*srv, error) {
|
||||
if locator[6:] == "" {
|
||||
return nil, fmt.Errorf("%s does not include the host", locator)
|
||||
}
|
||||
|
||||
return &srv{
|
||||
service: locator[6:],
|
||||
resolver: &net.Resolver{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// fastcgiPreset configures rule according to name. It returns an error if
|
||||
// name is not a recognized preset name.
|
||||
func fastcgiPreset(name string, rule *Rule) error {
|
||||
|
||||
+124
-236
@@ -1,14 +1,28 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
@@ -32,45 +46,29 @@ func TestSetup(t *testing.T) {
|
||||
if myHandler.Rules[0].Path != "/" {
|
||||
t.Errorf("Expected / as the Path")
|
||||
}
|
||||
if myHandler.Rules[0].Address != "127.0.0.1:9000" {
|
||||
addr, err := myHandler.Rules[0].Address()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error in trying to retrieve address: %s", err.Error())
|
||||
}
|
||||
|
||||
if addr != "127.0.0.1:9000" {
|
||||
t.Errorf("Expected 127.0.0.1:9000 as the Address")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (p *persistentDialer) Equals(q *persistentDialer) bool {
|
||||
if p.size != q.size {
|
||||
return false
|
||||
}
|
||||
if p.network != q.network {
|
||||
return false
|
||||
}
|
||||
if p.address != q.address {
|
||||
return false
|
||||
if myHandler.Rules[0].ConnectTimeout != 60*time.Second {
|
||||
t.Errorf("Expected default value of 60 seconds")
|
||||
}
|
||||
|
||||
if len(p.pool) != len(q.pool) {
|
||||
return false
|
||||
if myHandler.Rules[0].ReadTimeout != 60*time.Second {
|
||||
t.Errorf("Expected default value of 60 seconds")
|
||||
}
|
||||
for i, client := range p.pool {
|
||||
if client != q.pool[i] {
|
||||
return false
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].SendTimeout != 60*time.Second {
|
||||
t.Errorf("Expected default value of 60 seconds")
|
||||
}
|
||||
// ignore mutex state
|
||||
return true
|
||||
}
|
||||
|
||||
func TestFastcgiParse(t *testing.T) {
|
||||
rootPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("Can't determine current working directory; got '%v'", err)
|
||||
}
|
||||
|
||||
defaultAddress := "127.0.0.1:9001"
|
||||
network, address := parseAddress(defaultAddress)
|
||||
t.Logf("Address '%v' was parsed to network '%v' and address '%v'", defaultAddress, network, address)
|
||||
|
||||
tests := []struct {
|
||||
inputFastcgiConfig string
|
||||
shouldErr bool
|
||||
@@ -79,193 +77,47 @@ func TestFastcgiParse(t *testing.T) {
|
||||
|
||||
{`fastcgi /blog 127.0.0.1:9000 php`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000",
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9000"}},
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{"index.php"},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi /blog 127.0.0.1:9000 php {
|
||||
root /tmp
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: "/tmp",
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000",
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{"index.php"},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi /blog 127.0.0.1:9000 php {
|
||||
upstream 127.0.0.1:9001
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000,127.0.0.1:9001",
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}, basicDialer{network: "tcp", address: "127.0.0.1:9001", timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{"index.php"},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi /blog 127.0.0.1:9000 {
|
||||
upstream 127.0.0.1:9001
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000,127.0.0.1:9001",
|
||||
Ext: "",
|
||||
SplitPath: "",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}, basicDialer{network: "tcp", address: "127.0.0.1:9001", timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: defaultAddress,
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
except /admin /user
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: "127.0.0.1:9001",
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
IgnoredSubPaths: []string{"/admin", "/user"},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
pool 0
|
||||
}`,
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
send_timeout 30s
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: defaultAddress,
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
|
||||
Ext: "",
|
||||
SplitPath: "",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{&persistentDialer{size: 0, network: network, address: address, timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
SendTimeout: 30 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:8080 {
|
||||
upstream 127.0.0.1:9000
|
||||
pool 5
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: "127.0.0.1:8080,127.0.0.1:9000",
|
||||
Ext: "",
|
||||
SplitPath: "",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{&persistentDialer{size: 5, network: "tcp", address: "127.0.0.1:8080", timeout: 60 * time.Second}, &persistentDialer{size: 5, network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
split .php
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: defaultAddress,
|
||||
Ext: "",
|
||||
SplitPath: ".php",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
connect_timeout 5s
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: defaultAddress,
|
||||
Ext: "",
|
||||
SplitPath: "",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 5 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{
|
||||
`fastcgi / ` + defaultAddress + ` { connect_timeout BADVALUE }`,
|
||||
true,
|
||||
[]Rule{},
|
||||
},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
read_timeout 5s
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: defaultAddress,
|
||||
Ext: "",
|
||||
SplitPath: "",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 5 * time.Second,
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{
|
||||
`fastcgi / ` + defaultAddress + ` { read_timeout BADVALUE }`,
|
||||
true,
|
||||
[]Rule{},
|
||||
},
|
||||
{`fastcgi / ` + defaultAddress + ` {
|
||||
send_timeout 5s
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Root: rootPath,
|
||||
Path: "/",
|
||||
Address: defaultAddress,
|
||||
Ext: "",
|
||||
SplitPath: "",
|
||||
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
|
||||
IndexFiles: []string{},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
SendTimeout: 5 * time.Second,
|
||||
}}},
|
||||
{
|
||||
`fastcgi / ` + defaultAddress + ` { send_timeout BADVALUE }`,
|
||||
true,
|
||||
[]Rule{},
|
||||
},
|
||||
{`fastcgi / {
|
||||
|
||||
}`,
|
||||
true, []Rule{},
|
||||
},
|
||||
}
|
||||
for i, test := range tests {
|
||||
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController("http", test.inputFastcgiConfig))
|
||||
@@ -281,19 +133,24 @@ func TestFastcgiParse(t *testing.T) {
|
||||
}
|
||||
for j, actualFastcgiConfig := range actualFastcgiConfigs {
|
||||
|
||||
if actualFastcgiConfig.Root != test.expectedFastcgiConfig[j].Root {
|
||||
t.Errorf("Test %d expected %dth FastCGI Root to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Root, actualFastcgiConfig.Root)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Path != test.expectedFastcgiConfig[j].Path {
|
||||
t.Errorf("Test %d expected %dth FastCGI Path to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Path, actualFastcgiConfig.Path)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Address != test.expectedFastcgiConfig[j].Address {
|
||||
actualAddr, err := actualFastcgiConfig.Address()
|
||||
if err != nil {
|
||||
t.Errorf("Test %d unexpected error in trying to retrieve %dth actual address: %s", i, j, err.Error())
|
||||
}
|
||||
|
||||
expectedAddr, err := test.expectedFastcgiConfig[j].Address()
|
||||
if err != nil {
|
||||
t.Errorf("Test %d unexpected error in trying to retrieve %dth expected address: %s", i, j, err.Error())
|
||||
}
|
||||
|
||||
if actualAddr != expectedAddr {
|
||||
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Address, actualFastcgiConfig.Address)
|
||||
i, j, expectedAddr, actualAddr)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Ext != test.expectedFastcgiConfig[j].Ext {
|
||||
@@ -306,16 +163,6 @@ func TestFastcgiParse(t *testing.T) {
|
||||
i, j, test.expectedFastcgiConfig[j].SplitPath, actualFastcgiConfig.SplitPath)
|
||||
}
|
||||
|
||||
if reflect.TypeOf(actualFastcgiConfig.dialer) != reflect.TypeOf(test.expectedFastcgiConfig[j].dialer) {
|
||||
t.Errorf("Test %d expected %dth FastCGI dialer to be of type %T, but got %T",
|
||||
i, j, test.expectedFastcgiConfig[j].dialer, actualFastcgiConfig.dialer)
|
||||
} else {
|
||||
if !areDialersEqual(actualFastcgiConfig.dialer, test.expectedFastcgiConfig[j].dialer, t) {
|
||||
t.Errorf("Test %d expected %dth FastCGI dialer to be %v, but got %v",
|
||||
i, j, test.expectedFastcgiConfig[j].dialer, actualFastcgiConfig.dialer)
|
||||
}
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualFastcgiConfig.IndexFiles) != fmt.Sprint(test.expectedFastcgiConfig[j].IndexFiles) {
|
||||
t.Errorf("Test %d expected %dth FastCGI IndexFiles to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].IndexFiles, actualFastcgiConfig.IndexFiles)
|
||||
@@ -326,42 +173,83 @@ func TestFastcgiParse(t *testing.T) {
|
||||
i, j, test.expectedFastcgiConfig[j].IgnoredSubPaths, actualFastcgiConfig.IgnoredSubPaths)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualFastcgiConfig.ReadTimeout) != fmt.Sprint(test.expectedFastcgiConfig[j].ReadTimeout) {
|
||||
t.Errorf("Test %d expected %dth FastCGI ReadTimeout to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].ReadTimeout, actualFastcgiConfig.ReadTimeout)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualFastcgiConfig.SendTimeout) != fmt.Sprint(test.expectedFastcgiConfig[j].SendTimeout) {
|
||||
t.Errorf("Test %d expected %dth FastCGI SendTimeout to be %s , but got %s",
|
||||
if actualFastcgiConfig.SendTimeout != test.expectedFastcgiConfig[j].SendTimeout {
|
||||
t.Errorf("Test %d expected %dth FastCGI SendTimeout to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].SendTimeout, actualFastcgiConfig.SendTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func areDialersEqual(current, expected dialer, t *testing.T) bool {
|
||||
|
||||
switch actual := current.(type) {
|
||||
case *loadBalancingDialer:
|
||||
if expected, ok := expected.(*loadBalancingDialer); ok {
|
||||
for i := 0; i < len(actual.dialers); i++ {
|
||||
if !areDialersEqual(actual.dialers[i], expected.dialers[i], t) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
case basicDialer:
|
||||
return current == expected
|
||||
case *persistentDialer:
|
||||
if expected, ok := expected.(*persistentDialer); ok {
|
||||
return actual.Equals(expected)
|
||||
}
|
||||
|
||||
default:
|
||||
t.Errorf("Unknown dialer type %T", current)
|
||||
func TestFastCGIResolveSRV(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputFastcgiConfig string
|
||||
locator string
|
||||
target string
|
||||
port uint16
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
`fastcgi / srv://fpm.tcp.service.consul {
|
||||
upstream yolo
|
||||
}`,
|
||||
"fpm.tcp.service.consul",
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`fastcgi / srv://fpm.tcp.service.consul`,
|
||||
"fpm.tcp.service.consul",
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
return false
|
||||
for i, test := range tests {
|
||||
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController("http", test.inputFastcgiConfig))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
for _, actualFastcgiConfig := range actualFastcgiConfigs {
|
||||
resolver, ok := (actualFastcgiConfig.balancer).(*srv)
|
||||
if !ok {
|
||||
t.Errorf("Test %d upstream balancer is not srv", i)
|
||||
}
|
||||
resolver.resolver = buildTestResolver(test.target, test.port)
|
||||
|
||||
addr, err := actualFastcgiConfig.Address()
|
||||
if err != nil {
|
||||
t.Errorf("Test %d failed to retrieve upstream address. %s", i, err.Error())
|
||||
}
|
||||
|
||||
expectedAddr := fmt.Sprintf("%s:%d", test.target, test.port)
|
||||
if addr != expectedAddr {
|
||||
t.Errorf("Test %d expected upstream address to be %s, got %s", i, expectedAddr, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildTestResolver(target string, port uint16) srvResolver {
|
||||
return &testSRVResolver{target, port}
|
||||
}
|
||||
|
||||
type testSRVResolver struct {
|
||||
target string
|
||||
port uint16
|
||||
}
|
||||
|
||||
func (r *testSRVResolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error) {
|
||||
return "", []*net.SRV{
|
||||
{Target: r.target,
|
||||
Port: r.port,
|
||||
Priority: 1,
|
||||
Weight: 1}}, nil
|
||||
}
|
||||
|
||||
+67
-55
@@ -1,18 +1,29 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package gzip provides a middleware layer that performs
|
||||
// gzip compression on the response.
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -20,6 +31,8 @@ func init() {
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
|
||||
initWriterPool()
|
||||
}
|
||||
|
||||
// Gzip is a middleware type which gzips HTTP responses. It is
|
||||
@@ -53,22 +66,31 @@ outer:
|
||||
}
|
||||
}
|
||||
|
||||
// gzipWriter modifies underlying writer at init,
|
||||
// use a discard writer instead to leave ResponseWriter in
|
||||
// original form.
|
||||
gzipWriter, err := newWriter(c, ioutil.Discard)
|
||||
if err != nil {
|
||||
// should not happen
|
||||
return http.StatusInternalServerError, err
|
||||
// In order to avoid unused memory allocation, gzip.putWriter only be called when gzip compression happened.
|
||||
// see https://github.com/caddyserver/caddy/issues/2395
|
||||
gz := &gzipResponseWriter{
|
||||
ResponseWriterWrapper: &httpserver.ResponseWriterWrapper{ResponseWriter: w},
|
||||
newWriter: func() io.Writer {
|
||||
// gzipWriter modifies underlying writer at init,
|
||||
// use a discard writer instead to leave ResponseWriter in
|
||||
// original form.
|
||||
return getWriter(c.Level)
|
||||
},
|
||||
}
|
||||
defer gzipWriter.Close()
|
||||
gz := &gzipResponseWriter{Writer: gzipWriter, ResponseWriter: w}
|
||||
|
||||
defer func() {
|
||||
if gzWriter, ok := gz.internalWriter.(*gzip.Writer); ok {
|
||||
putWriter(c.Level, gzWriter)
|
||||
}
|
||||
}()
|
||||
|
||||
var rw http.ResponseWriter
|
||||
// if no response filter is used
|
||||
if len(c.ResponseFilters) == 0 {
|
||||
// replace discard writer with ResponseWriter
|
||||
gzipWriter.Reset(w)
|
||||
if gzWriter, ok := gz.Writer().(*gzip.Writer); ok {
|
||||
gzWriter.Reset(w)
|
||||
}
|
||||
rw = gz
|
||||
} else {
|
||||
// wrap gzip writer with ResponseFilterWriter
|
||||
@@ -92,22 +114,13 @@ outer:
|
||||
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
|
||||
// gzipResponseWriter wraps the underlying Write method
|
||||
// with a gzip.Writer to compress the output.
|
||||
type gzipResponseWriter struct {
|
||||
io.Writer
|
||||
http.ResponseWriter
|
||||
internalWriter io.Writer
|
||||
*httpserver.ResponseWriterWrapper
|
||||
statusCodeWritten bool
|
||||
newWriter func() io.Writer
|
||||
}
|
||||
|
||||
// WriteHeader wraps the underlying WriteHeader method to prevent
|
||||
@@ -117,8 +130,24 @@ type gzipResponseWriter struct {
|
||||
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)
|
||||
varyList, exist := w.Header()["Vary"]
|
||||
shouldAddVary := true
|
||||
if exist {
|
||||
for _, vary := range varyList {
|
||||
if vary == "Accept-Encoding" {
|
||||
shouldAddVary = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if shouldAddVary {
|
||||
w.Header().Add("Vary", "Accept-Encoding")
|
||||
}
|
||||
originalEtag := w.Header().Get("ETag")
|
||||
if originalEtag != "" && !strings.HasPrefix(originalEtag, "W/") {
|
||||
w.Header().Set("ETag", "W/"+originalEtag)
|
||||
}
|
||||
w.ResponseWriterWrapper.WriteHeader(code)
|
||||
w.statusCodeWritten = true
|
||||
}
|
||||
|
||||
@@ -130,34 +159,17 @@ func (w *gzipResponseWriter) Write(b []byte) (int, error) {
|
||||
if !w.statusCodeWritten {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
n, err := w.Writer.Write(b)
|
||||
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()
|
||||
//Writer use a lazy way to initialize Writer
|
||||
func (w *gzipResponseWriter) Writer() io.Writer {
|
||||
if w.internalWriter == nil {
|
||||
w.internalWriter = w.newWriter()
|
||||
}
|
||||
return nil, nil, httpserver.NonHijackerError{Underlying: w.ResponseWriter}
|
||||
return w.internalWriter
|
||||
}
|
||||
|
||||
// Flush implements http.Flusher. It simply wraps the underlying
|
||||
// ResponseWriter's Flush method if there is one, or panics.
|
||||
func (w *gzipResponseWriter) Flush() {
|
||||
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
} else {
|
||||
panic(httpserver.NonFlusherError{Underlying: w.ResponseWriter}) // should be recovered at the beginning of middleware stack
|
||||
}
|
||||
}
|
||||
|
||||
// CloseNotify implements http.CloseNotifier.
|
||||
// It just inherits the underlying ResponseWriter's CloseNotify method.
|
||||
func (w *gzipResponseWriter) CloseNotify() <-chan bool {
|
||||
if cn, ok := w.ResponseWriter.(http.CloseNotifier); ok {
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
panic(httpserver.NonCloseNotifierError{Underlying: w.ResponseWriter})
|
||||
}
|
||||
// Interface guards
|
||||
var _ httpserver.HTTPInterfaces = (*gzipResponseWriter)(nil)
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -8,7 +23,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestGzipHandler(t *testing.T) {
|
||||
@@ -37,6 +52,14 @@ func TestGzipHandler(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
w.Header().Set("ETag", `"2n9cd"`)
|
||||
_, err = gz.ServeHTTP(w, r)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// The second pass, test if the ETag is already weak
|
||||
w.Header().Set("ETag", `W/"2n9cd"`)
|
||||
_, err = gz.ServeHTTP(w, r)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -77,6 +100,22 @@ func TestGzipHandler(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// test all levels
|
||||
w = httptest.NewRecorder()
|
||||
gz.Next = nextFunc(true)
|
||||
for i := 0; i <= gzip.BestCompression; i++ {
|
||||
gz.Configs[0].Level = i
|
||||
r, err := http.NewRequest("GET", "/file.txt", nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
_, err = gz.ServeHTTP(w, r)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nextFunc(shouldGzip bool) httpserver.Handler {
|
||||
@@ -92,10 +131,14 @@ func nextFunc(shouldGzip bool) httpserver.Handler {
|
||||
|
||||
if shouldGzip {
|
||||
if w.Header().Get("Content-Encoding") != "gzip" {
|
||||
return 0, fmt.Errorf("Content-Encoding must be gzip, found %v", r.Header.Get("Content-Encoding"))
|
||||
return 0, fmt.Errorf("Content-Encoding must be gzip, found %v", w.Header().Get("Content-Encoding"))
|
||||
}
|
||||
if w.Header().Get("Vary") != "Accept-Encoding" {
|
||||
return 0, fmt.Errorf("Vary must be Accept-Encoding, found %v", r.Header.Get("Vary"))
|
||||
return 0, fmt.Errorf("Vary must be Accept-Encoding, found %v", w.Header().Get("Vary"))
|
||||
}
|
||||
etag := w.Header().Get("ETag")
|
||||
if etag != "" && etag != `W/"2n9cd"` {
|
||||
return 0, fmt.Errorf("ETag must be converted to weak Etag, found %v", w.Header().Get("ETag"))
|
||||
}
|
||||
if _, ok := w.(*gzipResponseWriter); !ok {
|
||||
return 0, fmt.Errorf("ResponseWriter should be gzipResponseWriter, found %T", w)
|
||||
@@ -117,3 +160,37 @@ func nextFunc(shouldGzip bool) httpserver.Handler {
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkGzip(b *testing.B) {
|
||||
pathFilter := PathFilter{make(Set)}
|
||||
badPaths := []string{"/bad", "/nogzip", "/nongzip"}
|
||||
for _, p := range badPaths {
|
||||
pathFilter.IgnoredPaths.Add(p)
|
||||
}
|
||||
extFilter := ExtFilter{make(Set)}
|
||||
for _, e := range []string{".txt", ".html", ".css", ".md"} {
|
||||
extFilter.Exts.Add(e)
|
||||
}
|
||||
gz := Gzip{Configs: []Config{
|
||||
{
|
||||
RequestFilters: []RequestFilter{pathFilter, extFilter},
|
||||
},
|
||||
}}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
gz.Next = nextFunc(true)
|
||||
url := "/file.txt"
|
||||
r, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err = gz.ServeHTTP(w, r)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// RequestFilter determines if a request should be gzipped.
|
||||
@@ -16,7 +30,7 @@ type RequestFilter interface {
|
||||
|
||||
// defaultExtensions is the list of default extensions for which to enable gzipping.
|
||||
var defaultExtensions = []string{"", ".txt", ".htm", ".html", ".css", ".php", ".js", ".json",
|
||||
".md", ".mdown", ".xml", ".svg", ".go", ".cgi", ".py", ".pl", ".aspx", ".asp"}
|
||||
".md", ".mdown", ".xml", ".svg", ".go", ".cgi", ".py", ".pl", ".aspx", ".asp", ".m3u", ".m3u8", ".wasm"}
|
||||
|
||||
// DefaultExtFilter creates an ExtFilter with default extensions.
|
||||
func DefaultExtFilter() ExtFilter {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
@@ -68,7 +82,7 @@ func (r *ResponseFilterWriter) WriteHeader(code int) {
|
||||
|
||||
if r.shouldCompress {
|
||||
// replace discard writer with ResponseWriter
|
||||
if gzWriter, ok := r.gzipResponseWriter.Writer.(*gzip.Writer); ok {
|
||||
if gzWriter, ok := r.gzipResponseWriter.Writer().(*gzip.Writer); ok {
|
||||
gzWriter.Reset(r.ResponseWriter)
|
||||
}
|
||||
// use gzip WriteHeader to include and delete
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestLengthFilter(t *testing.T) {
|
||||
@@ -33,7 +48,7 @@ func TestLengthFilter(t *testing.T) {
|
||||
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})
|
||||
wWriter := NewResponseFilterWriter([]ResponseFilter{filter}, &gzipResponseWriter{gzip.NewWriter(r), &httpserver.ResponseWriterWrapper{ResponseWriter: r}, false, nil})
|
||||
if filter.ShouldCompress(wWriter) != ts.shouldCompress[j] {
|
||||
t.Errorf("Test %v: Expected %v found %v", i, ts.shouldCompress[j], filter.ShouldCompress(r))
|
||||
}
|
||||
@@ -63,7 +78,9 @@ func TestResponseFilterWriter(t *testing.T) {
|
||||
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))
|
||||
if _, err := w.Write([]byte(ts.body)); err != nil {
|
||||
log.Println("[ERROR] failed to write response: ", err)
|
||||
}
|
||||
return 200, nil
|
||||
})
|
||||
|
||||
@@ -72,7 +89,9 @@ func TestResponseFilterWriter(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(w, r)
|
||||
if _, err := server.ServeHTTP(w, r); err != nil {
|
||||
log.Println("[ERROR] unable to serve a gzipped response: ", err)
|
||||
}
|
||||
|
||||
resp := w.Body.String()
|
||||
|
||||
@@ -95,7 +114,9 @@ func TestResponseGzippedOutput(t *testing.T) {
|
||||
|
||||
server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Write([]byte("gzipped"))
|
||||
if _, err := w.Write([]byte("gzipped")); err != nil {
|
||||
log.Println("[ERROR] failed to write response: ", err)
|
||||
}
|
||||
return 200, nil
|
||||
})
|
||||
|
||||
@@ -103,7 +124,9 @@ func TestResponseGzippedOutput(t *testing.T) {
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
server.ServeHTTP(w, r)
|
||||
if _, err := server.ServeHTTP(w, r); err != nil {
|
||||
log.Println("[ERROR] unable to serve a gzipped response: ", err)
|
||||
}
|
||||
resp := w.Body.String()
|
||||
|
||||
if resp != "gzipped" {
|
||||
|
||||
+64
-2
@@ -1,12 +1,29 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// setup configures a new gzip middleware instance.
|
||||
@@ -119,3 +136,48 @@ func gzipParse(c *caddy.Controller) ([]Config, error) {
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// pool gzip.Writer according to compress level
|
||||
// so we can reuse allocations over time
|
||||
var (
|
||||
writerPool = map[int]*sync.Pool{}
|
||||
defaultWriterPoolIndex int
|
||||
)
|
||||
|
||||
func initWriterPool() {
|
||||
var i int
|
||||
newWriterPool := func(level int) *sync.Pool {
|
||||
return &sync.Pool{
|
||||
New: func() interface{} {
|
||||
w, _ := gzip.NewWriterLevel(ioutil.Discard, level)
|
||||
return w
|
||||
},
|
||||
}
|
||||
}
|
||||
for i = gzip.BestSpeed; i <= gzip.BestCompression; i++ {
|
||||
writerPool[i] = newWriterPool(i)
|
||||
}
|
||||
|
||||
// add default writer pool
|
||||
defaultWriterPoolIndex = i
|
||||
writerPool[defaultWriterPoolIndex] = newWriterPool(gzip.DefaultCompression)
|
||||
}
|
||||
|
||||
func getWriter(level int) *gzip.Writer {
|
||||
index := defaultWriterPoolIndex
|
||||
if level >= gzip.BestSpeed && level <= gzip.BestCompression {
|
||||
index = level
|
||||
}
|
||||
w := writerPool[index].Get().(*gzip.Writer)
|
||||
w.Reset(ioutil.Discard)
|
||||
return w
|
||||
}
|
||||
|
||||
func putWriter(level int, w *gzip.Writer) {
|
||||
index := defaultWriterPoolIndex
|
||||
if level >= gzip.BestSpeed && level <= gzip.BestCompression {
|
||||
index = level
|
||||
}
|
||||
w.Close()
|
||||
writerPool[index].Put(w)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
|
||||
+24
-36
@@ -1,15 +1,27 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package header provides middleware that appends headers to
|
||||
// requests based on a set of configuration rules that define
|
||||
// which routes receive which headers.
|
||||
package header
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Headers is middleware that adds headers to the responses
|
||||
@@ -23,7 +35,9 @@ type Headers struct {
|
||||
// setting headers on the response according to the configured rules.
|
||||
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
replacer := httpserver.NewReplacer(r, nil, "")
|
||||
rww := &responseWriterWrapper{w: w}
|
||||
rww := &responseWriterWrapper{
|
||||
ResponseWriterWrapper: &httpserver.ResponseWriterWrapper{ResponseWriter: w},
|
||||
}
|
||||
for _, rule := range h.Rules {
|
||||
if httpserver.Path(r.URL.Path).Matches(rule.Path) {
|
||||
for name := range rule.Headers {
|
||||
@@ -62,20 +76,20 @@ type headerOperation func(http.Header)
|
||||
// responseWriterWrapper wraps the real ResponseWriter.
|
||||
// It defers header operations until writeHeader
|
||||
type responseWriterWrapper struct {
|
||||
w http.ResponseWriter
|
||||
*httpserver.ResponseWriterWrapper
|
||||
ops []headerOperation
|
||||
wroteHeader bool
|
||||
}
|
||||
|
||||
func (rww *responseWriterWrapper) Header() http.Header {
|
||||
return rww.w.Header()
|
||||
return rww.ResponseWriterWrapper.Header()
|
||||
}
|
||||
|
||||
func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
|
||||
if !rww.wroteHeader {
|
||||
rww.WriteHeader(http.StatusOK)
|
||||
}
|
||||
return rww.w.Write(d)
|
||||
return rww.ResponseWriterWrapper.Write(d)
|
||||
}
|
||||
|
||||
func (rww *responseWriterWrapper) WriteHeader(status int) {
|
||||
@@ -91,7 +105,7 @@ func (rww *responseWriterWrapper) WriteHeader(status int) {
|
||||
op(h)
|
||||
}
|
||||
|
||||
rww.w.WriteHeader(status)
|
||||
rww.ResponseWriterWrapper.WriteHeader(status)
|
||||
}
|
||||
|
||||
// delHeader deletes the existing header according to the key
|
||||
@@ -106,31 +120,5 @@ func (rww *responseWriterWrapper) delHeader(key string) {
|
||||
})
|
||||
}
|
||||
|
||||
// Hijack implements http.Hijacker. It simply wraps the underlying
|
||||
// ResponseWriter's Hijack method if there is one, or returns an error.
|
||||
func (rww *responseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := rww.w.(http.Hijacker); ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
return nil, nil, httpserver.NonHijackerError{Underlying: rww.w}
|
||||
}
|
||||
|
||||
// Flush implements http.Flusher. It simply wraps the underlying
|
||||
// ResponseWriter's Flush method if there is one, or panics.
|
||||
func (rww *responseWriterWrapper) Flush() {
|
||||
if f, ok := rww.w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
} else {
|
||||
panic(httpserver.NonFlusherError{Underlying: rww.w}) // should be recovered at the beginning of middleware stack
|
||||
}
|
||||
}
|
||||
|
||||
// CloseNotify implements http.CloseNotifier.
|
||||
// It just inherits the underlying ResponseWriter's CloseNotify method.
|
||||
// It panics if the underlying ResponseWriter is not a CloseNotifier.
|
||||
func (rww *responseWriterWrapper) CloseNotify() <-chan bool {
|
||||
if cn, ok := rww.w.(http.CloseNotifier); ok {
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
panic(httpserver.NonCloseNotifierError{Underlying: rww.w})
|
||||
}
|
||||
// Interface guards
|
||||
var _ httpserver.HTTPInterfaces = (*responseWriterWrapper)(nil)
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package header
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -9,7 +24,7 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestHeader(t *testing.T) {
|
||||
@@ -55,7 +70,9 @@ func TestHeader(t *testing.T) {
|
||||
// preset header
|
||||
rec.Header().Set("Server", "Caddy")
|
||||
|
||||
he.ServeHTTP(rec, req)
|
||||
if _, err := he.ServeHTTP(rec, req); err != nil {
|
||||
log.Println("[ERROR] ServeHTTP failed: ", err)
|
||||
}
|
||||
|
||||
if got := rec.Header().Get(test.name); got != test.value {
|
||||
t.Errorf("Test %d: Expected %s header to be %q but was %q",
|
||||
@@ -67,7 +84,9 @@ func TestHeader(t *testing.T) {
|
||||
func TestMultipleHeaders(t *testing.T) {
|
||||
he := Headers{
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
fmt.Fprint(w, "This is a test")
|
||||
if _, err := fmt.Fprint(w, "This is a test"); err != nil {
|
||||
log.Println("[ERROR] Fprint failed: ", err)
|
||||
}
|
||||
return 0, nil
|
||||
}),
|
||||
Rules: []Rule{
|
||||
@@ -83,7 +102,9 @@ func TestMultipleHeaders(t *testing.T) {
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
he.ServeHTTP(rec, req)
|
||||
if _, err := he.ServeHTTP(rec, req); err != nil {
|
||||
log.Println("[ERROR] ServeHTTP failed: ", err)
|
||||
}
|
||||
|
||||
desiredHeaders := []string{"</css/main.css>; rel=preload", "</images/image.png>; rel=preload"}
|
||||
actualHeaders := rec.HeaderMap[http.CanonicalHeaderKey("Link")]
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package header
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package header
|
||||
|
||||
import (
|
||||
@@ -6,8 +20,8 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -6,7 +20,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
// SetupIfMatcher parses `if` or `if_op` in the current dispenser block.
|
||||
@@ -26,6 +40,7 @@ func SetupIfMatcher(controller *caddy.Controller) (RequestMatcher, error) {
|
||||
return matcher, err
|
||||
}
|
||||
matcher.ifs = append(matcher.ifs, ifc)
|
||||
matcher.Enabled = true
|
||||
case "if_op":
|
||||
if !c.NextArg() {
|
||||
return matcher, c.ArgErr()
|
||||
@@ -48,121 +63,102 @@ 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 ifFunc func(a, b string) bool
|
||||
|
||||
// ifCond is statement for a IfMatcher condition.
|
||||
type ifCond struct {
|
||||
a string
|
||||
op string
|
||||
b string
|
||||
neg bool
|
||||
rex *regexp.Regexp
|
||||
f ifFunc
|
||||
}
|
||||
|
||||
// ifCondition is a 'if' condition.
|
||||
type ifCondition func(string, string) bool
|
||||
// newIfCond creates a new If condition.
|
||||
func newIfCond(a, op, b string) (ifCond, error) {
|
||||
i := ifCond{a: a, op: op, b: b}
|
||||
if strings.HasPrefix(op, "not_") {
|
||||
i.neg = true
|
||||
i.op = op[4:]
|
||||
}
|
||||
|
||||
var ifConditions = map[string]ifCondition{
|
||||
isOp: isFunc,
|
||||
notOp: notFunc,
|
||||
hasOp: hasFunc,
|
||||
notHasOp: notHasFunc,
|
||||
startsWithOp: startsWithFunc,
|
||||
endsWithOp: endsWithFunc,
|
||||
matchOp: matchFunc,
|
||||
notMatchOp: notMatchFunc,
|
||||
switch i.op {
|
||||
case isOp:
|
||||
// It checks for equality.
|
||||
i.f = i.isFunc
|
||||
case notOp:
|
||||
// It checks for inequality.
|
||||
i.f = i.notFunc
|
||||
case hasOp:
|
||||
// It checks if b is a substring of a.
|
||||
i.f = strings.Contains
|
||||
case startsWithOp:
|
||||
// It checks if b is a prefix of a.
|
||||
i.f = strings.HasPrefix
|
||||
case endsWithOp:
|
||||
// It checks if b is a suffix of a.
|
||||
i.f = strings.HasSuffix
|
||||
case matchOp:
|
||||
// It does regexp matching of a against pattern in b and returns if they match.
|
||||
var err error
|
||||
if i.rex, err = regexp.Compile(i.b); err != nil {
|
||||
return ifCond{}, fmt.Errorf("Invalid regular expression: '%s', %v", i.b, err)
|
||||
}
|
||||
i.f = i.matchFunc
|
||||
default:
|
||||
return ifCond{}, fmt.Errorf("Invalid operator %v", i.op)
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// isFunc is condition for Is operator.
|
||||
// It checks for equality.
|
||||
func isFunc(a, b string) bool {
|
||||
func (i ifCond) isFunc(a, b string) bool {
|
||||
return a == b
|
||||
}
|
||||
|
||||
// notFunc is condition for Not operator.
|
||||
// It checks for inequality.
|
||||
func notFunc(a, b string) bool {
|
||||
func (i ifCond) 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
|
||||
func (i ifCond) matchFunc(a, b string) bool {
|
||||
return i.rex.MatchString(a)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if i.f != nil {
|
||||
a, b := i.a, i.b
|
||||
if r != nil {
|
||||
replacer := NewReplacer(r, nil, "")
|
||||
a = replacer.Replace(i.a)
|
||||
b = replacer.Replace(i.b)
|
||||
if i.op != matchOp {
|
||||
b = replacer.Replace(i.b)
|
||||
}
|
||||
}
|
||||
return c(a, b)
|
||||
if i.neg {
|
||||
return !i.f(a, b)
|
||||
}
|
||||
return i.f(a, b)
|
||||
}
|
||||
return false
|
||||
return i.neg // false if not negated, true otherwise
|
||||
}
|
||||
|
||||
// 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'
|
||||
Enabled bool // if true, matcher has been configured; otherwise it's no-op
|
||||
ifs []ifCond // list of If
|
||||
isOr bool // if true, conditions are 'or' instead of 'and'
|
||||
}
|
||||
|
||||
// Match satisfies RequestMatcher interface.
|
||||
@@ -195,7 +191,7 @@ func (m IfMatcher) Or(r *http.Request) bool {
|
||||
}
|
||||
|
||||
// IfMatcherKeyword checks if the next value in the dispenser is a keyword for 'if' config block.
|
||||
// If true, remaining arguments in the dispinser are cleard to keep the dispenser valid for use.
|
||||
// If true, remaining arguments in the dispenser are cleared to keep the dispenser valid for use.
|
||||
func IfMatcherKeyword(c *caddy.Controller) bool {
|
||||
if c.Val() == "if" || c.Val() == "if_op" {
|
||||
// clear remaining args
|
||||
|
||||
@@ -1,72 +1,99 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
func TestConditions(t *testing.T) {
|
||||
tests := []struct {
|
||||
condition string
|
||||
isTrue bool
|
||||
shouldErr 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},
|
||||
{"a is b", false, false},
|
||||
{"a is a", true, false},
|
||||
{"a not b", true, false},
|
||||
{"a not a", false, false},
|
||||
{"a has a", true, false},
|
||||
{"a has b", false, false},
|
||||
{"ba has b", true, false},
|
||||
{"bab has b", true, false},
|
||||
{"bab has bb", false, false},
|
||||
{"a not_has a", false, false},
|
||||
{"a not_has b", true, false},
|
||||
{"ba not_has b", false, false},
|
||||
{"bab not_has b", false, false},
|
||||
{"bab not_has bb", true, false},
|
||||
{"bab starts_with bb", false, false},
|
||||
{"bab starts_with ba", true, false},
|
||||
{"bab starts_with bab", true, false},
|
||||
{"bab not_starts_with bb", true, false},
|
||||
{"bab not_starts_with ba", false, false},
|
||||
{"bab not_starts_with bab", false, false},
|
||||
{"bab ends_with bb", false, false},
|
||||
{"bab ends_with bab", true, false},
|
||||
{"bab ends_with ab", true, false},
|
||||
{"bab not_ends_with bb", true, false},
|
||||
{"bab not_ends_with ab", false, false},
|
||||
{"bab not_ends_with bab", false, false},
|
||||
{"a match *", false, true},
|
||||
{"a match a", true, false},
|
||||
{"a match .*", true, false},
|
||||
{"a match a.*", true, false},
|
||||
{"a match b.*", false, false},
|
||||
{"ba match b.*", true, false},
|
||||
{"ba match b[a-z]", true, false},
|
||||
{"b0 match b[a-z]", false, false},
|
||||
{"b0a match b[a-z]", false, false},
|
||||
{"b0a match b[a-z]+", false, false},
|
||||
{"b0a match b[a-z0-9]+", true, false},
|
||||
{"bac match b[a-z]{2}", true, false},
|
||||
{"a not_match *", false, true},
|
||||
{"a not_match a", false, false},
|
||||
{"a not_match .*", false, false},
|
||||
{"a not_match a.*", false, false},
|
||||
{"a not_match b.*", true, false},
|
||||
{"ba not_match b.*", false, false},
|
||||
{"ba not_match b[a-z]", false, false},
|
||||
{"b0 not_match b[a-z]", true, false},
|
||||
{"b0a not_match b[a-z]", true, false},
|
||||
{"b0a not_match b[a-z]+", true, false},
|
||||
{"b0a not_match b[a-z0-9]+", false, false},
|
||||
{"bac not_match b[a-z]{2}", false, 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)
|
||||
if !test.shouldErr {
|
||||
t.Error(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
isTrue := ifCond.True(nil)
|
||||
if isTrue != test.isTrue {
|
||||
t.Errorf("Test %d: expected %v found %v", i, test.isTrue, isTrue)
|
||||
t.Errorf("Test %d: '%s' expected %v found %v", i, test.condition, test.isTrue, isTrue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,16 +121,21 @@ func TestConditions(t *testing.T) {
|
||||
for i, test := range replaceTests {
|
||||
r, err := http.NewRequest("GET", test.url, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
t.Errorf("Test %d: failed to create request: %v", i, err)
|
||||
continue
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), OriginalURLCtxKey, *r.URL)
|
||||
r = r.WithContext(ctx)
|
||||
str := strings.Fields(test.condition)
|
||||
ifCond, err := newIfCond(str[0], str[1], str[2])
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
t.Errorf("Test %d: failed to create 'if' condition %v", i, err)
|
||||
continue
|
||||
}
|
||||
isTrue := ifCond.True(r)
|
||||
if isTrue != test.isTrue {
|
||||
t.Errorf("Test %v: expected %v found %v", i, test.isTrue, isTrue)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,6 +212,7 @@ func TestIfMatcher(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSetupIfMatcher(t *testing.T) {
|
||||
rex_b, _ := regexp.Compile("b")
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
@@ -189,7 +222,7 @@ func TestSetupIfMatcher(t *testing.T) {
|
||||
if a match b
|
||||
}`, false, IfMatcher{
|
||||
ifs: []ifCond{
|
||||
{a: "a", op: "match", b: "b"},
|
||||
{a: "a", op: "match", b: "b", neg: false, rex: rex_b},
|
||||
},
|
||||
}},
|
||||
{`test {
|
||||
@@ -197,7 +230,7 @@ func TestSetupIfMatcher(t *testing.T) {
|
||||
if_op or
|
||||
}`, false, IfMatcher{
|
||||
ifs: []ifCond{
|
||||
{a: "a", op: "match", b: "b"},
|
||||
{a: "a", op: "match", b: "b", neg: false, rex: rex_b},
|
||||
},
|
||||
isOr: true,
|
||||
}},
|
||||
@@ -215,26 +248,26 @@ func TestSetupIfMatcher(t *testing.T) {
|
||||
},
|
||||
{`test {
|
||||
if goal has go
|
||||
if cook not_has go
|
||||
if cook not_has go
|
||||
}`, false, IfMatcher{
|
||||
ifs: []ifCond{
|
||||
{a: "goal", op: "has", b: "go"},
|
||||
{a: "cook", op: "not_has", b: "go"},
|
||||
{a: "goal", op: "has", b: "go", neg: false},
|
||||
{a: "cook", op: "has", b: "go", neg: true},
|
||||
},
|
||||
}},
|
||||
{`test {
|
||||
if goal has go
|
||||
if cook not_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"},
|
||||
{a: "goal", op: "has", b: "go", neg: false},
|
||||
{a: "cook", op: "has", b: "go", neg: true},
|
||||
},
|
||||
}},
|
||||
{`test {
|
||||
if goal has go
|
||||
if cook not_has go
|
||||
if cook not_has go
|
||||
if_op not
|
||||
}`, true, IfMatcher{},
|
||||
},
|
||||
@@ -243,6 +276,7 @@ func TestSetupIfMatcher(t *testing.T) {
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("http", test.input)
|
||||
c.Next()
|
||||
|
||||
matcher, err := SetupIfMatcher(c)
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
@@ -251,15 +285,60 @@ func TestSetupIfMatcher(t *testing.T) {
|
||||
} else if err != nil && test.shouldErr {
|
||||
continue
|
||||
}
|
||||
if _, ok := matcher.(IfMatcher); !ok {
|
||||
|
||||
test_if, ok := matcher.(IfMatcher)
|
||||
if !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))
|
||||
|
||||
if len(test_if.ifs) != len(test.expected.ifs) {
|
||||
t.Errorf("Test %d: Expected %d ifConditions, found %v", i,
|
||||
len(test.expected.ifs), len(test_if.ifs))
|
||||
}
|
||||
|
||||
for j, if_c := range test_if.ifs {
|
||||
expected_c := test.expected.ifs[j]
|
||||
|
||||
if if_c.a != expected_c.a {
|
||||
t.Errorf("Test %d, ifCond %d: Expected A=%s, got %s",
|
||||
i, j, if_c.a, expected_c.a)
|
||||
}
|
||||
|
||||
if if_c.op != expected_c.op {
|
||||
t.Errorf("Test %d, ifCond %d: Expected Op=%s, got %s",
|
||||
i, j, if_c.op, expected_c.op)
|
||||
}
|
||||
|
||||
if if_c.b != expected_c.b {
|
||||
t.Errorf("Test %d, ifCond %d: Expected B=%s, got %s",
|
||||
i, j, if_c.b, expected_c.b)
|
||||
}
|
||||
|
||||
if if_c.neg != expected_c.neg {
|
||||
t.Errorf("Test %d, ifCond %d: Expected Neg=%v, got %v",
|
||||
i, j, if_c.neg, expected_c.neg)
|
||||
}
|
||||
|
||||
if expected_c.rex != nil && if_c.rex == nil {
|
||||
t.Errorf("Test %d, ifCond %d: Expected Rex=%v, got <nil>",
|
||||
i, j, expected_c.rex)
|
||||
}
|
||||
|
||||
if expected_c.rex == nil && if_c.rex != nil {
|
||||
t.Errorf("Test %d, ifCond %d: Expected Rex=<nil>, got %v",
|
||||
i, j, if_c.rex)
|
||||
}
|
||||
|
||||
if expected_c.rex != nil && if_c.rex != nil {
|
||||
if if_c.rex.String() != expected_c.rex.String() {
|
||||
t.Errorf("Test %d, ifCond %d: Expected Rex=%v, got %v",
|
||||
i, j, if_c.rex, expected_c.rex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -8,6 +22,7 @@ var (
|
||||
_ error = NonHijackerError{}
|
||||
_ error = NonFlusherError{}
|
||||
_ error = NonCloseNotifierError{}
|
||||
_ error = NonPusherError{}
|
||||
)
|
||||
|
||||
// NonHijackerError is more descriptive error caused by a non hijacker
|
||||
@@ -42,3 +57,14 @@ type NonCloseNotifierError struct {
|
||||
func (c NonCloseNotifierError) Error() string {
|
||||
return fmt.Sprintf("%T is not a closeNotifier", c.Underlying)
|
||||
}
|
||||
|
||||
// NonPusherError is more descriptive error caused by a non pusher
|
||||
type NonPusherError struct {
|
||||
// underlying type which doesn't implement pusher
|
||||
Underlying interface{}
|
||||
}
|
||||
|
||||
// Implement Error
|
||||
func (c NonPusherError) Error() string {
|
||||
return fmt.Sprintf("%T is not a pusher", c.Underlying)
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,19 +1,35 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/mholt/certmagic"
|
||||
)
|
||||
|
||||
func activateHTTPS(cctx caddy.Context) error {
|
||||
operatorPresent := !caddy.Started()
|
||||
|
||||
if !caddy.Quiet && operatorPresent {
|
||||
fmt.Print("Activating privacy features...")
|
||||
fmt.Print("Activating privacy features... ")
|
||||
}
|
||||
|
||||
ctx := cctx.(*httpContext)
|
||||
@@ -23,7 +39,13 @@ func activateHTTPS(cctx caddy.Context) error {
|
||||
|
||||
// place certificates and keys on disk
|
||||
for _, c := range ctx.siteConfigs {
|
||||
err := c.TLS.ObtainCert(c.TLS.Hostname, operatorPresent)
|
||||
if !c.TLS.Managed {
|
||||
continue
|
||||
}
|
||||
if c.TLS.Manager.OnDemand != nil {
|
||||
continue // obtain these certificates on-demand instead
|
||||
}
|
||||
err := c.TLS.Manager.ObtainCert(c.TLS.Hostname, operatorPresent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -41,13 +63,23 @@ func activateHTTPS(cctx caddy.Context) error {
|
||||
// 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
|
||||
// (skip if upgrading, because the parent process is likely already listening
|
||||
// on the ports we'd need to do ACME before we finish starting; parent process
|
||||
// already running renewal ticker, so renewal won't be missed anyway.)
|
||||
if !caddy.IsUpgrade() {
|
||||
ctx.instance.StorageMu.RLock()
|
||||
certCache, ok := ctx.instance.Storage[caddytls.CertCacheInstStorageKey].(*certmagic.Cache)
|
||||
ctx.instance.StorageMu.RUnlock()
|
||||
if ok && certCache != nil {
|
||||
err = certCache.RenewManagedCertificates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !caddy.Quiet && operatorPresent {
|
||||
fmt.Println(" done.")
|
||||
fmt.Println("done.")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -65,21 +97,22 @@ func markQualifiedForAutoHTTPS(configs []*SiteConfig) {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// It will only change configs that are marked as managed but not on-demand, 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 {
|
||||
if cfg == nil || cfg.TLS == nil || !cfg.TLS.Managed ||
|
||||
cfg.TLS.Manager == nil || cfg.TLS.Manager.OnDemand != nil {
|
||||
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 loadCertificates && certmagic.HostQualifies(cfg.TLS.Hostname) {
|
||||
_, err := cfg.TLS.Manager.CacheManagedCertificate(cfg.TLS.Hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -91,9 +124,9 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
|
||||
// Set default port of 443 if not explicitly set
|
||||
if cfg.Addr.Port == "" &&
|
||||
cfg.TLS.Enabled &&
|
||||
(!cfg.TLS.Manual || cfg.TLS.OnDemand) &&
|
||||
(!cfg.TLS.Manual || cfg.TLS.Manager.OnDemand != nil) &&
|
||||
cfg.Addr.Host != "localhost" {
|
||||
cfg.Addr.Port = "443"
|
||||
cfg.Addr.Port = strconv.Itoa(certmagic.HTTPSPort)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -106,10 +139,12 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
|
||||
// only set up redirects for configs that qualify. It returns the updated list of
|
||||
// all configs.
|
||||
func makePlaintextRedirects(allConfigs []*SiteConfig) []*SiteConfig {
|
||||
httpPort := strconv.Itoa(certmagic.HTTPPort)
|
||||
httpsPort := strconv.Itoa(certmagic.HTTPSPort)
|
||||
for i, cfg := range allConfigs {
|
||||
if cfg.TLS.Managed &&
|
||||
!hostHasOtherPort(allConfigs, i, "80") &&
|
||||
(cfg.Addr.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) {
|
||||
!hostHasOtherPort(allConfigs, i, httpPort) &&
|
||||
(cfg.Addr.Port == httpsPort || !hostHasOtherPort(allConfigs, i, httpsPort)) {
|
||||
allConfigs = append(allConfigs, redirPlaintextHost(cfg))
|
||||
}
|
||||
}
|
||||
@@ -135,32 +170,57 @@ func hostHasOtherPort(allConfigs []*SiteConfig, thisConfigIdx int, otherPort str
|
||||
// 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.
|
||||
// to listen on certmagic.HTTPPort. 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
|
||||
if redirPort == strconv.Itoa(certmagic.HTTPSPort) {
|
||||
// By default, HTTPSPort should be DefaultHTTPSPort,
|
||||
// which of course doesn't need to be explicitly stated
|
||||
// in the Location header. Even if HTTPSPort is changed
|
||||
// so that it is no longer DefaultHTTPSPort, we shouldn't
|
||||
// append it to the URL in the Location because changing
|
||||
// the HTTPS port is assumed to be an internal-only change
|
||||
// (in other words, we assume port forwarding is going on);
|
||||
// but redirects go back to a presumably-external client.
|
||||
// (If redirect clients are also internal, that is more
|
||||
// advanced, and the user should configure HTTP->HTTPS
|
||||
// redirects themselves.)
|
||||
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
|
||||
// Construct the URL to which to redirect. Note that the Host in a
|
||||
// request might contain a port, but we just need the hostname from
|
||||
// it; and we'll set the port if needed.
|
||||
toURL := "https://"
|
||||
requestHost, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
requestHost = r.Host // Host did not contain a port, so use the whole value
|
||||
}
|
||||
if redirPort == "" {
|
||||
toURL += requestHost
|
||||
} else {
|
||||
toURL += net.JoinHostPort(requestHost, redirPort)
|
||||
}
|
||||
|
||||
toURL += r.URL.RequestURI()
|
||||
|
||||
w.Header().Set("Connection", "close")
|
||||
http.Redirect(w, r, toURL, http.StatusMovedPermanently)
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
|
||||
host := cfg.Addr.Host
|
||||
port := "80"
|
||||
port := strconv.Itoa(certmagic.HTTPPort)
|
||||
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},
|
||||
TLS: &caddytls.Config{Manager: cfg.TLS.Manager},
|
||||
Timeouts: cfg.Timeouts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +1,134 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/mholt/certmagic"
|
||||
)
|
||||
|
||||
func TestRedirPlaintextHost(t *testing.T) {
|
||||
cfg := redirPlaintextHost(&SiteConfig{
|
||||
Addr: Address{
|
||||
Host: "example.com",
|
||||
for i, testcase := range []struct {
|
||||
Host string // used for the site config
|
||||
Port string
|
||||
ListenHost string
|
||||
RequestHost string // if different from Host
|
||||
}{
|
||||
{
|
||||
Host: "foohost",
|
||||
},
|
||||
{
|
||||
Host: "foohost",
|
||||
Port: "80",
|
||||
},
|
||||
{
|
||||
Host: "foohost",
|
||||
Port: "1234",
|
||||
},
|
||||
ListenHost: "93.184.216.34",
|
||||
TLS: new(caddytls.Config),
|
||||
})
|
||||
{
|
||||
Host: "foohost",
|
||||
ListenHost: "93.184.216.34",
|
||||
},
|
||||
{
|
||||
Host: "foohost",
|
||||
Port: "1234",
|
||||
ListenHost: "93.184.216.34",
|
||||
},
|
||||
{
|
||||
Host: "foohost",
|
||||
Port: strconv.Itoa(certmagic.HTTPSPort), // since this is the 'default' HTTPS port, should not be included in Location value
|
||||
},
|
||||
{
|
||||
Host: "*.example.com",
|
||||
RequestHost: "foo.example.com",
|
||||
},
|
||||
{
|
||||
Host: "*.example.com",
|
||||
Port: "1234",
|
||||
RequestHost: "foo.example.com:1234",
|
||||
},
|
||||
} {
|
||||
cfg := redirPlaintextHost(&SiteConfig{
|
||||
Addr: Address{
|
||||
Host: testcase.Host,
|
||||
Port: testcase.Port,
|
||||
},
|
||||
ListenHost: testcase.ListenHost,
|
||||
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)
|
||||
}
|
||||
// Check host and port
|
||||
if actual, expected := cfg.Addr.Host, testcase.Host; actual != expected {
|
||||
t.Errorf("Test %d: Expected redir config to have host %s but got %s", i, expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.ListenHost, testcase.ListenHost; actual != expected {
|
||||
t.Errorf("Test %d: Expected redir config to have bindhost %s but got %s", i, expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.Addr.Port, strconv.Itoa(certmagic.HTTPPort); actual != expected {
|
||||
t.Errorf("Test %d: Expected redir config to have port '%s' but got '%s'", i, 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)
|
||||
}
|
||||
// Make sure redirect handler is set up properly
|
||||
if cfg.middleware == nil || len(cfg.middleware) != 1 {
|
||||
t.Fatalf("Test %d: Redir config middleware not set up properly; got: %#v", i, cfg.middleware)
|
||||
}
|
||||
|
||||
handler := cfg.middleware[0](nil)
|
||||
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)
|
||||
}
|
||||
// Check redirect for correctness, first by inspecting error and status code
|
||||
requestHost := testcase.Host // hostname of request might be different than in config (e.g. wildcards)
|
||||
if testcase.RequestHost != "" {
|
||||
requestHost = testcase.RequestHost
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://"+requestHost+"/bar?q=1", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: %v", i, err)
|
||||
}
|
||||
status, err := handler.ServeHTTP(rec, req)
|
||||
if status != 0 {
|
||||
t.Errorf("Test %d: Expected status return to be 0, but was %d", i, status)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: Expected returned error to be nil, but was %v", i, err)
|
||||
}
|
||||
if rec.Code != http.StatusMovedPermanently {
|
||||
t.Errorf("Test %d: Expected status %d but got %d", http.StatusMovedPermanently, i, rec.Code)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// Now check the Location value. It should mirror the hostname and port of the request
|
||||
// unless the port is redundant, in which case it should be dropped.
|
||||
locationHost, _, err := net.SplitHostPort(requestHost)
|
||||
if err != nil {
|
||||
locationHost = requestHost
|
||||
}
|
||||
expectedLoc := fmt.Sprintf("https://%s/bar?q=1", locationHost)
|
||||
if testcase.Port != "" && testcase.Port != DefaultHTTPSPort {
|
||||
expectedLoc = fmt.Sprintf("https://%s:%s/bar?q=1", locationHost, testcase.Port)
|
||||
}
|
||||
if got, want := rec.Header().Get("Location"), expectedLoc; got != want {
|
||||
t.Errorf("Test %d: Expected Location: '%s' but got '%s'", i, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,11 +178,13 @@ func TestMakePlaintextRedirects(t *testing.T) {
|
||||
|
||||
func TestEnableAutoHTTPS(t *testing.T) {
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true, Manager: &certmagic.Config{}}},
|
||||
{}, // not managed - no changes!
|
||||
}
|
||||
|
||||
enableAutoHTTPS(configs, false)
|
||||
if err := enableAutoHTTPS(configs, false); err != nil {
|
||||
log.Println("[ERROR] enableAutoHTTPS failed: ", err)
|
||||
}
|
||||
|
||||
if !configs[0].TLS.Enabled {
|
||||
t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false")
|
||||
@@ -148,18 +201,18 @@ func TestEnableAutoHTTPS(t *testing.T) {
|
||||
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: ""}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "localhost"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "123.44.3.21"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com"}, TLS: newManagedConfig()},
|
||||
{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)},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "foo@bar.com", Manager: &certmagic.Config{}}},
|
||||
{Addr: Address{Host: "example.com", Scheme: "http"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com", Port: "80"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com", Port: "1234"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com", Scheme: "https"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com", Port: "80", Scheme: "https"}, TLS: newManagedConfig()},
|
||||
}
|
||||
expectedManagedCount := 4
|
||||
|
||||
@@ -176,3 +229,7 @@ func TestMarkQualifiedForAutoHTTPS(t *testing.T) {
|
||||
t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
|
||||
}
|
||||
}
|
||||
|
||||
func newManagedConfig() *caddytls.Config {
|
||||
return &caddytls.Config{Manager: &certmagic.Config{}}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
gsyslog "github.com/hashicorp/go-syslog"
|
||||
)
|
||||
|
||||
var remoteSyslogPrefixes = map[string]string{
|
||||
"syslog+tcp://": "tcp",
|
||||
"syslog+udp://": "udp",
|
||||
"syslog://": "udp",
|
||||
}
|
||||
|
||||
// Logger is shared between errors and log plugins and supports both logging to
|
||||
// a file (with an optional file roller), local and remote syslog servers.
|
||||
type Logger struct {
|
||||
Output string
|
||||
*log.Logger
|
||||
Roller *LogRoller
|
||||
writer io.Writer
|
||||
fileMu *sync.RWMutex
|
||||
V4ipMask net.IPMask
|
||||
V6ipMask net.IPMask
|
||||
IPMaskExists bool
|
||||
Exceptions []string
|
||||
}
|
||||
|
||||
// NewTestLogger creates logger suitable for testing purposes
|
||||
func NewTestLogger(buffer *bytes.Buffer) *Logger {
|
||||
return &Logger{
|
||||
Logger: log.New(buffer, "", 0),
|
||||
fileMu: new(sync.RWMutex),
|
||||
}
|
||||
}
|
||||
|
||||
// Println wraps underlying logger with mutex
|
||||
func (l Logger) Println(args ...interface{}) {
|
||||
l.fileMu.RLock()
|
||||
l.Logger.Println(args...)
|
||||
l.fileMu.RUnlock()
|
||||
}
|
||||
|
||||
// Printf wraps underlying logger with mutex
|
||||
func (l Logger) Printf(format string, args ...interface{}) {
|
||||
l.fileMu.RLock()
|
||||
l.Logger.Printf(format, args...)
|
||||
l.fileMu.RUnlock()
|
||||
}
|
||||
|
||||
func (l Logger) MaskIP(ip string) string {
|
||||
var reqIP net.IP
|
||||
// If unable to parse, simply return IP as provided.
|
||||
reqIP = net.ParseIP(ip)
|
||||
if reqIP == nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
if reqIP.To4() != nil {
|
||||
return reqIP.Mask(l.V4ipMask).String()
|
||||
}
|
||||
return reqIP.Mask(l.V6ipMask).String()
|
||||
}
|
||||
|
||||
// ShouldLog returns true if the path is not exempted from
|
||||
// being logged (i.e. it is not found in l.Exceptions).
|
||||
func (l Logger) ShouldLog(path string) bool {
|
||||
for _, exc := range l.Exceptions {
|
||||
if Path(path).Matches(exc) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Attach binds logger Start and Close functions to
|
||||
// controller's OnStartup and OnShutdown hooks.
|
||||
func (l *Logger) Attach(controller *caddy.Controller) {
|
||||
if controller != nil {
|
||||
// Opens file or connect to local/remote syslog
|
||||
controller.OnStartup(l.Start)
|
||||
|
||||
// Closes file or disconnects from local/remote syslog
|
||||
controller.OnShutdown(l.Close)
|
||||
}
|
||||
}
|
||||
|
||||
type syslogAddress struct {
|
||||
network string
|
||||
address string
|
||||
}
|
||||
|
||||
func parseSyslogAddress(location string) *syslogAddress {
|
||||
for prefix, network := range remoteSyslogPrefixes {
|
||||
if strings.HasPrefix(location, prefix) {
|
||||
return &syslogAddress{
|
||||
network: network,
|
||||
address: strings.TrimPrefix(location, prefix),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start initializes logger opening files or local/remote syslog connections
|
||||
func (l *Logger) Start() error {
|
||||
// initialize mutex on start
|
||||
l.fileMu = new(sync.RWMutex)
|
||||
|
||||
var err error
|
||||
|
||||
selectwriter:
|
||||
switch l.Output {
|
||||
case "", "stderr":
|
||||
l.writer = os.Stderr
|
||||
case "stdout":
|
||||
l.writer = os.Stdout
|
||||
case "syslog":
|
||||
l.writer, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if address := parseSyslogAddress(l.Output); address != nil {
|
||||
l.writer, err = gsyslog.DialLogger(address.network, address.address, gsyslog.LOG_ERR, "LOCAL0", "caddy")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break selectwriter
|
||||
}
|
||||
|
||||
var file *os.File
|
||||
|
||||
file, err = os.OpenFile(l.Output, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if l.Roller != nil && !l.Roller.Disabled {
|
||||
file.Close()
|
||||
l.Roller.Filename = l.Output
|
||||
l.writer = l.Roller.GetLogWriter()
|
||||
} else {
|
||||
l.writer = file
|
||||
}
|
||||
}
|
||||
|
||||
l.Logger = log.New(l.writer, "", 0)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Close closes open log files or connections to syslog.
|
||||
func (l *Logger) Close() error {
|
||||
// don't close stdout or stderr
|
||||
if l.writer == os.Stdout || l.writer == os.Stderr {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Will close local/remote syslog connections too :)
|
||||
if closer, ok := l.writer.(io.WriteCloser); ok {
|
||||
l.fileMu.Lock()
|
||||
err := closer.Close()
|
||||
l.fileMu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//+build linux darwin
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
syslog "gopkg.in/mcuadros/go-syslog.v2"
|
||||
"gopkg.in/mcuadros/go-syslog.v2/format"
|
||||
)
|
||||
|
||||
func TestLoggingToStdout(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Output string
|
||||
ExpectedOutput string
|
||||
}{
|
||||
{
|
||||
Output: "stdout",
|
||||
ExpectedOutput: "Hello world logged to stdout",
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
output := captureStdout(func() {
|
||||
logger := Logger{Output: testCase.Output, fileMu: new(sync.RWMutex)}
|
||||
|
||||
if err := logger.Start(); err != nil {
|
||||
t.Fatalf("Got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
logger.Println(testCase.ExpectedOutput)
|
||||
})
|
||||
|
||||
if !strings.Contains(output, testCase.ExpectedOutput) {
|
||||
t.Fatalf("Test #%d: Expected output to contain: %s, got: %s", i, testCase.ExpectedOutput, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggingToStderr(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
Output string
|
||||
ExpectedOutput string
|
||||
}{
|
||||
{
|
||||
Output: "stderr",
|
||||
ExpectedOutput: "Hello world logged to stderr",
|
||||
},
|
||||
{
|
||||
Output: "",
|
||||
ExpectedOutput: "Hello world logged to stderr #2",
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
output := captureStderr(func() {
|
||||
logger := Logger{Output: testCase.Output, fileMu: new(sync.RWMutex)}
|
||||
|
||||
if err := logger.Start(); err != nil {
|
||||
t.Fatalf("Got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
logger.Println(testCase.ExpectedOutput)
|
||||
})
|
||||
|
||||
if !strings.Contains(output, testCase.ExpectedOutput) {
|
||||
t.Fatalf("Test #%d: Expected output to contain: %s, got: %s", i, testCase.ExpectedOutput, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggingToFile(t *testing.T) {
|
||||
file := filepath.Join(os.TempDir(), "access.log")
|
||||
expectedOutput := "Hello world written to file"
|
||||
|
||||
logger := Logger{Output: file}
|
||||
|
||||
if err := logger.Start(); err != nil {
|
||||
t.Fatalf("Got unexpected error during logger start: %v", err)
|
||||
}
|
||||
|
||||
logger.Print(expectedOutput)
|
||||
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read log file content: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Contains(content, []byte(expectedOutput)) {
|
||||
t.Fatalf("Expected log file to contain: %s, got: %s", expectedOutput, string(content))
|
||||
}
|
||||
|
||||
os.Remove(file)
|
||||
}
|
||||
|
||||
func TestLoggingToSyslog(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
Output string
|
||||
ExpectedOutput string
|
||||
}{
|
||||
{
|
||||
Output: "syslog://127.0.0.1:5660",
|
||||
ExpectedOutput: "Hello world! Test #1 over tcp",
|
||||
},
|
||||
{
|
||||
Output: "syslog+tcp://127.0.0.1:5661",
|
||||
ExpectedOutput: "Hello world! Test #2 over tcp",
|
||||
},
|
||||
{
|
||||
Output: "syslog+udp://127.0.0.1:5662",
|
||||
ExpectedOutput: "Hello world! Test #3 over udp",
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
|
||||
ch := make(chan format.LogParts, 256)
|
||||
server, err := bootServer(testCase.Output, ch)
|
||||
defer server.Kill()
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Test #%d: expected no error during syslog server boot, got: %v", i, err)
|
||||
}
|
||||
|
||||
logger := Logger{Output: testCase.Output, fileMu: new(sync.RWMutex)}
|
||||
|
||||
if err := logger.Start(); err != nil {
|
||||
t.Errorf("Test #%d: expected no error during logger start, got: %v", i, err)
|
||||
}
|
||||
|
||||
defer logger.Close()
|
||||
|
||||
logger.Print(testCase.ExpectedOutput)
|
||||
|
||||
actual := <-ch
|
||||
|
||||
if content, ok := actual["content"].(string); ok {
|
||||
if !strings.Contains(content, testCase.ExpectedOutput) {
|
||||
t.Errorf("Test #%d: expected server to capture content: %s, but got: %s", i, testCase.ExpectedOutput, content)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Test #%d: expected server to capture content but got: %v", i, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func bootServer(location string, ch chan format.LogParts) (*syslog.Server, error) {
|
||||
address := parseSyslogAddress(location)
|
||||
|
||||
if address == nil {
|
||||
return nil, fmt.Errorf("Could not parse syslog address: %s", location)
|
||||
}
|
||||
|
||||
server := syslog.NewServer()
|
||||
server.SetFormat(syslog.Automatic)
|
||||
|
||||
switch address.network {
|
||||
case "tcp":
|
||||
if err := server.ListenTCP(address.address); err != nil {
|
||||
log.Println("[ERROR] server failed to listen on TCP address: ", err)
|
||||
}
|
||||
case "udp":
|
||||
if err := server.ListenUDP(address.address); err != nil {
|
||||
log.Println("[ERROR] server failed to listen on UDP address: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
server.SetHandler(syslog.NewChannelHandler(ch))
|
||||
|
||||
if err := server.Boot(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func captureStdout(f func()) string {
|
||||
original := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
|
||||
os.Stdout = w
|
||||
|
||||
f()
|
||||
|
||||
w.Close()
|
||||
|
||||
written, _ := ioutil.ReadAll(r)
|
||||
os.Stdout = original
|
||||
|
||||
return string(written)
|
||||
}
|
||||
|
||||
func captureStderr(f func()) string {
|
||||
original := os.Stderr
|
||||
r, w, _ := os.Pipe()
|
||||
|
||||
os.Stderr = w
|
||||
|
||||
f()
|
||||
|
||||
w.Close()
|
||||
|
||||
written, _ := ioutil.ReadAll(r)
|
||||
os.Stderr = original
|
||||
|
||||
return string(written)
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -6,6 +20,8 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -18,6 +34,10 @@ type (
|
||||
// passed the next Handler in the chain.
|
||||
Middleware func(Handler) Handler
|
||||
|
||||
// ListenerMiddleware is similar to the Middleware type, except it
|
||||
// chains one net.Listener to the next.
|
||||
ListenerMiddleware func(caddy.Listener) caddy.Listener
|
||||
|
||||
// Handler is like http.Handler except ServeHTTP may return a status
|
||||
// code and/or error.
|
||||
//
|
||||
@@ -97,6 +117,10 @@ func (c ConfigSelector) Select(r *http.Request) (config HandlerConfig) {
|
||||
// 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 len(fpath) == 0 {
|
||||
// https://caddy.community/t/panic-runtime-error-index-out-of-range/5781
|
||||
fpath = "/"
|
||||
}
|
||||
if fpath[len(fpath)-1] != '/' || root == nil {
|
||||
return "", false
|
||||
}
|
||||
@@ -138,7 +162,7 @@ func SetLastModifiedHeader(w http.ResponseWriter, modTime time.Time) {
|
||||
|
||||
// CaseSensitivePath determines if paths should be case sensitive.
|
||||
// This is configurable via CASE_SENSITIVE_PATH environment variable.
|
||||
var CaseSensitivePath = true
|
||||
var CaseSensitivePath = false
|
||||
|
||||
const caseSensitivePathEnv = "CASE_SENSITIVE_PATH"
|
||||
|
||||
@@ -147,10 +171,10 @@ const caseSensitivePathEnv = "CASE_SENSITIVE_PATH"
|
||||
// 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:
|
||||
case "1", "true":
|
||||
CaseSensitivePath = true
|
||||
default:
|
||||
CaseSensitivePath = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,3 +215,18 @@ var EmptyNext = HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, e
|
||||
func SameNext(next1, next2 Handler) bool {
|
||||
return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2)
|
||||
}
|
||||
|
||||
// Context key constants.
|
||||
const (
|
||||
// ReplacerCtxKey is the context key for a per-request replacer.
|
||||
ReplacerCtxKey caddy.CtxKey = "replacer"
|
||||
|
||||
// RemoteUserCtxKey is the key for the remote user of the request, if any (basicauth).
|
||||
RemoteUserCtxKey caddy.CtxKey = "remote_user"
|
||||
|
||||
// MitmCtxKey is the key for the result of MITM detection
|
||||
MitmCtxKey caddy.CtxKey = "mitm"
|
||||
|
||||
// RequestIDCtxKey is the key for the U4 UUID value
|
||||
RequestIDCtxKey caddy.CtxKey = "request_id"
|
||||
)
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -45,7 +59,7 @@ func TestPathCaseSensitiveEnv(t *testing.T) {
|
||||
{"0", false},
|
||||
{"false", false},
|
||||
{"true", true},
|
||||
{"", true},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
||||
@@ -0,0 +1,780 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy/telemetry"
|
||||
)
|
||||
|
||||
// tlsHandler is a http.Handler that will inject a value
|
||||
// into the request context indicating if the TLS
|
||||
// connection is likely being intercepted.
|
||||
type tlsHandler struct {
|
||||
next http.Handler
|
||||
listener *tlsHelloListener
|
||||
closeOnMITM bool // whether to close connection on MITM; TODO: expose through new directive
|
||||
}
|
||||
|
||||
// ServeHTTP checks the User-Agent. For the four main browsers (Chrome,
|
||||
// Edge, Firefox, and Safari) indicated by the User-Agent, the properties
|
||||
// of the TLS Client Hello will be compared. The context value "mitm" will
|
||||
// be set to a value indicating if it is likely that the underlying TLS
|
||||
// connection is being intercepted.
|
||||
//
|
||||
// Note that due to Microsoft's decision to intentionally make IE/Edge
|
||||
// user agents obscure (and look like other browsers), this may offer
|
||||
// less accuracy for IE/Edge clients.
|
||||
//
|
||||
// This MITM detection capability is based on research done by Durumeric,
|
||||
// Halderman, et. al. in "The Security Impact of HTTPS Interception" (NDSS '17):
|
||||
// https://jhalderm.com/pub/papers/interception-ndss17.pdf
|
||||
func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: one request per connection, we should report UA in connection with
|
||||
// handshake (reported in caddytls package) and our MITM assessment
|
||||
|
||||
if h.listener == nil {
|
||||
h.next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
h.listener.helloInfosMu.RLock()
|
||||
info := h.listener.helloInfos[r.RemoteAddr]
|
||||
h.listener.helloInfosMu.RUnlock()
|
||||
|
||||
ua := r.Header.Get("User-Agent")
|
||||
uaHash := telemetry.FastHash([]byte(ua))
|
||||
|
||||
// report this request's UA in connection with this ClientHello
|
||||
go telemetry.AppendUnique("tls_client_hello_ua:"+caddytls.ClientHelloInfo(info).Key(), uaHash)
|
||||
|
||||
var checked, mitm bool
|
||||
if r.Header.Get("X-BlueCoat-Via") != "" || // Blue Coat (masks User-Agent header to generic values)
|
||||
r.Header.Get("X-FCCKV2") != "" || // Fortinet
|
||||
info.advertisesHeartbeatSupport() { // no major browsers have ever implemented Heartbeat
|
||||
// TODO: Move the heartbeat check into each "looksLike" function...
|
||||
checked = true
|
||||
mitm = true
|
||||
} else if strings.Contains(ua, "Edge") || strings.Contains(ua, "MSIE") ||
|
||||
strings.Contains(ua, "Trident") {
|
||||
checked = true
|
||||
mitm = !info.looksLikeEdge()
|
||||
} else if strings.Contains(ua, "Chrome") {
|
||||
checked = true
|
||||
mitm = !info.looksLikeChrome()
|
||||
} else if strings.Contains(ua, "CriOS") {
|
||||
// Chrome on iOS sometimes uses iOS-provided TLS stack (which looks exactly like Safari)
|
||||
// but for connections that don't render a web page (favicon, etc.) it uses its own...
|
||||
checked = true
|
||||
mitm = !info.looksLikeChrome() && !info.looksLikeSafari()
|
||||
} else if strings.Contains(ua, "Firefox") {
|
||||
checked = true
|
||||
if strings.Contains(ua, "Windows") {
|
||||
ver := getVersion(ua, "Firefox")
|
||||
if ver == 45.0 || ver == 52.0 {
|
||||
mitm = !info.looksLikeTor()
|
||||
} else {
|
||||
mitm = !info.looksLikeFirefox()
|
||||
}
|
||||
} else {
|
||||
mitm = !info.looksLikeFirefox()
|
||||
}
|
||||
} else if strings.Contains(ua, "Safari") {
|
||||
checked = true
|
||||
mitm = !info.looksLikeSafari()
|
||||
}
|
||||
|
||||
if checked {
|
||||
r = r.WithContext(context.WithValue(r.Context(), MitmCtxKey, mitm))
|
||||
if mitm {
|
||||
go telemetry.AppendUnique("http_mitm", "likely")
|
||||
} else {
|
||||
go telemetry.AppendUnique("http_mitm", "unlikely")
|
||||
}
|
||||
} else {
|
||||
go telemetry.AppendUnique("http_mitm", "unknown")
|
||||
}
|
||||
|
||||
if mitm && h.closeOnMITM {
|
||||
// TODO: This termination might need to happen later in the middleware
|
||||
// chain in order to be picked up by the log directive, in case the site
|
||||
// owner still wants to log this event. It'll probably require a new
|
||||
// directive. If this feature is useful, we can finish implementing this.
|
||||
r.Close = true
|
||||
return
|
||||
}
|
||||
|
||||
h.next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// getVersion returns a (possibly simplified) representation of the version string
|
||||
// from a UserAgent string. It returns a float, so it can represent major and minor
|
||||
// versions; the rest of the version is just tacked on behind the decimal point.
|
||||
// The purpose of this is to stay simple while allowing for basic, fast comparisons.
|
||||
// If the version for softwareName is not found in ua, -1 is returned.
|
||||
func getVersion(ua, softwareName string) float64 {
|
||||
search := softwareName + "/"
|
||||
start := strings.Index(ua, search)
|
||||
if start < 0 {
|
||||
return -1
|
||||
}
|
||||
start += len(search)
|
||||
end := strings.Index(ua[start:], " ")
|
||||
if end < 0 {
|
||||
end = len(ua)
|
||||
} else {
|
||||
end += start
|
||||
}
|
||||
strVer := strings.Replace(ua[start:end], "-", "", -1)
|
||||
firstDot := strings.Index(strVer, ".")
|
||||
if firstDot >= 0 {
|
||||
strVer = strVer[:firstDot+1] + strings.Replace(strVer[firstDot+1:], ".", "", -1)
|
||||
}
|
||||
ver, err := strconv.ParseFloat(strVer, 64)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return ver
|
||||
}
|
||||
|
||||
// clientHelloConn reads the ClientHello
|
||||
// and stores it in the attached listener.
|
||||
type clientHelloConn struct {
|
||||
net.Conn
|
||||
listener *tlsHelloListener
|
||||
readHello bool // whether ClientHello has been read
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
// Read reads from c.Conn (by letting the standard library
|
||||
// do the reading off the wire), with the exception of
|
||||
// getting a copy of the ClientHello so it can parse it.
|
||||
func (c *clientHelloConn) Read(b []byte) (n int, err error) {
|
||||
// if we've already read the ClientHello, pass thru
|
||||
if c.readHello {
|
||||
return c.Conn.Read(b)
|
||||
}
|
||||
|
||||
// we let the standard lib read off the wire for us, and
|
||||
// tee that into our buffer so we can read the ClientHello
|
||||
tee := io.TeeReader(c.Conn, c.buf)
|
||||
n, err = tee.Read(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if c.buf.Len() < 5 {
|
||||
return // need to read more bytes for header
|
||||
}
|
||||
|
||||
// read the header bytes
|
||||
hdr := make([]byte, 5)
|
||||
_, err = io.ReadFull(c.buf, hdr)
|
||||
if err != nil {
|
||||
return // this would be highly unusual and sad
|
||||
}
|
||||
|
||||
// get length of the ClientHello message and read it
|
||||
length := int(uint16(hdr[3])<<8 | uint16(hdr[4]))
|
||||
if c.buf.Len() < length {
|
||||
return // need to read more bytes
|
||||
}
|
||||
hello := make([]byte, length)
|
||||
_, err = io.ReadFull(c.buf, hello)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bufpool.Put(c.buf) // buffer no longer needed
|
||||
|
||||
// parse the ClientHello and store it in the map
|
||||
rawParsed := parseRawClientHello(hello)
|
||||
c.listener.helloInfosMu.Lock()
|
||||
c.listener.helloInfos[c.Conn.RemoteAddr().String()] = rawParsed
|
||||
c.listener.helloInfosMu.Unlock()
|
||||
|
||||
// report this ClientHello to telemetry
|
||||
chKey := caddytls.ClientHelloInfo(rawParsed).Key()
|
||||
go telemetry.SetNested("tls_client_hello", chKey, rawParsed)
|
||||
go telemetry.AppendUnique("tls_client_hello_count", chKey)
|
||||
|
||||
c.readHello = true
|
||||
return
|
||||
}
|
||||
|
||||
// parseRawClientHello parses data which contains the raw
|
||||
// TLS Client Hello message. It extracts relevant information
|
||||
// into info. Any error reading the Client Hello (such as
|
||||
// insufficient length or invalid length values) results in
|
||||
// a silent error and an incomplete info struct, since there
|
||||
// is no good way to handle an error like this during Accept().
|
||||
// The data is expected to contain the whole ClientHello and
|
||||
// ONLY the ClientHello.
|
||||
//
|
||||
// The majority of this code is borrowed from the Go standard
|
||||
// library, which is (c) The Go Authors. It has been modified
|
||||
// to fit this use case.
|
||||
func parseRawClientHello(data []byte) (info rawHelloInfo) {
|
||||
if len(data) < 42 {
|
||||
return
|
||||
}
|
||||
info.Version = uint16(data[4])<<8 | uint16(data[5])
|
||||
sessionIDLen := int(data[38])
|
||||
if sessionIDLen > 32 || len(data) < 39+sessionIDLen {
|
||||
return
|
||||
}
|
||||
data = data[39+sessionIDLen:]
|
||||
if len(data) < 2 {
|
||||
return
|
||||
}
|
||||
// cipherSuiteLen is the number of bytes of cipher suite numbers. Since
|
||||
// they are uint16s, the number must be even.
|
||||
cipherSuiteLen := int(data[0])<<8 | int(data[1])
|
||||
if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen {
|
||||
return
|
||||
}
|
||||
numCipherSuites := cipherSuiteLen / 2
|
||||
// read in the cipher suites
|
||||
info.CipherSuites = make([]uint16, numCipherSuites)
|
||||
for i := 0; i < numCipherSuites; i++ {
|
||||
info.CipherSuites[i] = uint16(data[2+2*i])<<8 | uint16(data[3+2*i])
|
||||
}
|
||||
data = data[2+cipherSuiteLen:]
|
||||
if len(data) < 1 {
|
||||
return
|
||||
}
|
||||
// read in the compression methods
|
||||
compressionMethodsLen := int(data[0])
|
||||
if len(data) < 1+compressionMethodsLen {
|
||||
return
|
||||
}
|
||||
info.CompressionMethods = data[1 : 1+compressionMethodsLen]
|
||||
|
||||
data = data[1+compressionMethodsLen:]
|
||||
|
||||
// ClientHello is optionally followed by extension data
|
||||
if len(data) < 2 {
|
||||
return
|
||||
}
|
||||
extensionsLength := int(data[0])<<8 | int(data[1])
|
||||
data = data[2:]
|
||||
if extensionsLength != len(data) {
|
||||
return
|
||||
}
|
||||
|
||||
// read in each extension, and extract any relevant information
|
||||
// from extensions we care about
|
||||
for len(data) != 0 {
|
||||
if len(data) < 4 {
|
||||
return
|
||||
}
|
||||
extension := uint16(data[0])<<8 | uint16(data[1])
|
||||
length := int(data[2])<<8 | int(data[3])
|
||||
data = data[4:]
|
||||
if len(data) < length {
|
||||
return
|
||||
}
|
||||
|
||||
// record that the client advertised support for this extension
|
||||
info.Extensions = append(info.Extensions, extension)
|
||||
|
||||
switch extension {
|
||||
case extensionSupportedCurves:
|
||||
// http://tools.ietf.org/html/rfc4492#section-5.5.1
|
||||
if length < 2 {
|
||||
return
|
||||
}
|
||||
l := int(data[0])<<8 | int(data[1])
|
||||
if l%2 == 1 || length != l+2 {
|
||||
return
|
||||
}
|
||||
numCurves := l / 2
|
||||
info.Curves = make([]tls.CurveID, numCurves)
|
||||
d := data[2:]
|
||||
for i := 0; i < numCurves; i++ {
|
||||
info.Curves[i] = tls.CurveID(d[0])<<8 | tls.CurveID(d[1])
|
||||
d = d[2:]
|
||||
}
|
||||
case extensionSupportedPoints:
|
||||
// http://tools.ietf.org/html/rfc4492#section-5.5.2
|
||||
if length < 1 {
|
||||
return
|
||||
}
|
||||
l := int(data[0])
|
||||
if length != l+1 {
|
||||
return
|
||||
}
|
||||
info.Points = make([]uint8, l)
|
||||
copy(info.Points, data[1:])
|
||||
}
|
||||
|
||||
data = data[length:]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// newTLSListener returns a new tlsHelloListener that wraps ln.
|
||||
func newTLSListener(ln net.Listener, config *tls.Config) *tlsHelloListener {
|
||||
return &tlsHelloListener{
|
||||
Listener: ln,
|
||||
config: config,
|
||||
helloInfos: make(map[string]rawHelloInfo),
|
||||
}
|
||||
}
|
||||
|
||||
// tlsHelloListener is a TLS listener that is specially designed
|
||||
// to read the ClientHello manually so we can extract necessary
|
||||
// information from it. Each ClientHello message is mapped by
|
||||
// the remote address of the client, which must be removed when
|
||||
// the connection is closed (use ConnState).
|
||||
type tlsHelloListener struct {
|
||||
net.Listener
|
||||
config *tls.Config
|
||||
helloInfos map[string]rawHelloInfo
|
||||
helloInfosMu sync.RWMutex
|
||||
}
|
||||
|
||||
// Accept waits for and returns the next connection to the listener.
|
||||
// After it accepts the underlying connection, it reads the
|
||||
// ClientHello message and stores the parsed data into a map on l.
|
||||
func (l *tlsHelloListener) Accept() (net.Conn, error) {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := bufpool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
helloConn := &clientHelloConn{Conn: conn, listener: l, buf: buf}
|
||||
return tls.Server(helloConn, l.config), nil
|
||||
}
|
||||
|
||||
// rawHelloInfo contains the "raw" data parsed from the TLS
|
||||
// Client Hello. No interpretation is done on the raw data.
|
||||
//
|
||||
// The methods on this type implement heuristics described
|
||||
// by Durumeric, Halderman, et. al. in
|
||||
// "The Security Impact of HTTPS Interception":
|
||||
// https://jhalderm.com/pub/papers/interception-ndss17.pdf
|
||||
type rawHelloInfo caddytls.ClientHelloInfo
|
||||
|
||||
// advertisesHeartbeatSupport returns true if info indicates
|
||||
// that the client supports the Heartbeat extension.
|
||||
func (info rawHelloInfo) advertisesHeartbeatSupport() bool {
|
||||
for _, ext := range info.Extensions {
|
||||
if ext == extensionHeartbeat {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// looksLikeFirefox returns true if info looks like a handshake
|
||||
// from a modern version of Firefox.
|
||||
func (info rawHelloInfo) looksLikeFirefox() bool {
|
||||
// "To determine whether a Firefox session has been
|
||||
// intercepted, we check for the presence and order
|
||||
// of extensions, cipher suites, elliptic curves,
|
||||
// EC point formats, and handshake compression methods." (early 2016)
|
||||
|
||||
// We check for the presence and order of the extensions.
|
||||
// Note: Sometimes 0x15 (21, padding) is present, sometimes not.
|
||||
// Note: Firefox 51+ does not advertise 0x3374 (13172, NPN).
|
||||
// Note: Firefox doesn't advertise 0x0 (0, SNI) when connecting to IP addresses.
|
||||
// Note: Firefox 55+ doesn't appear to advertise 0xFF03 (65283, short headers). It used to be between 5 and 13.
|
||||
// Note: Firefox on Fedora (or RedHat) doesn't include ECC suites because of patent liability.
|
||||
requiredExtensionsOrder := []uint16{23, 65281, 10, 11, 35, 16, 5, 13}
|
||||
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.Extensions, true) {
|
||||
return false
|
||||
}
|
||||
|
||||
// We check for both presence of curves and their ordering.
|
||||
requiredCurves := []tls.CurveID{29, 23, 24, 25}
|
||||
if len(info.Curves) < len(requiredCurves) {
|
||||
return false
|
||||
}
|
||||
for i := range requiredCurves {
|
||||
if info.Curves[i] != requiredCurves[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(info.Curves) > len(requiredCurves) {
|
||||
// newer Firefox (55 Nightly?) may have additional curves at end of list
|
||||
allowedCurves := []tls.CurveID{256, 257}
|
||||
for i := range allowedCurves {
|
||||
if info.Curves[len(requiredCurves)+i] != allowedCurves[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasGreaseCiphers(info.CipherSuites) {
|
||||
return false
|
||||
}
|
||||
|
||||
// We check for order of cipher suites but not presence, since
|
||||
// according to the paper, cipher suites may be not be added
|
||||
// or reordered by the user, but they may be disabled.
|
||||
expectedCipherSuiteOrder := []uint16{
|
||||
TLS_AES_128_GCM_SHA256, // 0x1301
|
||||
TLS_CHACHA20_POLY1305_SHA256, // 0x1303
|
||||
TLS_AES_256_GCM_SHA384, // 0x1302
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // 0xc02b
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // 0xc02f
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, // 0xcca9
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, // 0xcca8
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, // 0xc02c
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, // 0xc030
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // 0xc00a
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // 0xc009
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // 0xc013
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // 0xc014
|
||||
TLS_DHE_RSA_WITH_AES_128_CBC_SHA, // 0x33
|
||||
TLS_DHE_RSA_WITH_AES_256_CBC_SHA, // 0x39
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA, // 0x2f
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // 0xa
|
||||
}
|
||||
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.CipherSuites, false)
|
||||
}
|
||||
|
||||
// looksLikeChrome returns true if info looks like a handshake
|
||||
// from a modern version of Chrome.
|
||||
func (info rawHelloInfo) looksLikeChrome() bool {
|
||||
// "We check for ciphers and extensions that Chrome is known
|
||||
// to not support, but do not check for the inclusion of
|
||||
// specific ciphers or extensions, nor do we validate their
|
||||
// order. When appropriate, we check the presence and order
|
||||
// of elliptic curves, compression methods, and EC point formats." (early 2016)
|
||||
|
||||
// Not in Chrome 56, but present in Safari 10 (Feb. 2017):
|
||||
// TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 (0xc024)
|
||||
// TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 (0xc023)
|
||||
// TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
|
||||
// TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
|
||||
// TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (0xc028)
|
||||
// TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (0xc027)
|
||||
// TLS_RSA_WITH_AES_256_CBC_SHA256 (0x3d)
|
||||
// TLS_RSA_WITH_AES_128_CBC_SHA256 (0x3c)
|
||||
|
||||
// Not in Chrome 56, but present in Firefox 51 (Feb. 2017):
|
||||
// TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
|
||||
// TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
|
||||
// TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x33)
|
||||
// TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x39)
|
||||
|
||||
// Selected ciphers present in Chrome mobile (Feb. 2017):
|
||||
// 0xc00a, 0xc014, 0xc009, 0x9c, 0x9d, 0x2f, 0x35, 0xa
|
||||
|
||||
chromeCipherExclusions := map[uint16]struct{}{
|
||||
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384: {}, // 0xc024
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256: {}, // 0xc023
|
||||
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384: {}, // 0xc028
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256: {}, // 0xc027
|
||||
TLS_RSA_WITH_AES_256_CBC_SHA256: {}, // 0x3d
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA256: {}, // 0x3c
|
||||
TLS_DHE_RSA_WITH_AES_128_CBC_SHA: {}, // 0x33
|
||||
TLS_DHE_RSA_WITH_AES_256_CBC_SHA: {}, // 0x39
|
||||
}
|
||||
for _, ext := range info.CipherSuites {
|
||||
if _, ok := chromeCipherExclusions[ext]; ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome does not include curve 25 (CurveP521) (as of Chrome 56, Feb. 2017).
|
||||
for _, curve := range info.Curves {
|
||||
if curve == 25 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if !hasGreaseCiphers(info.CipherSuites) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// looksLikeEdge returns true if info looks like a handshake
|
||||
// from a modern version of MS Edge.
|
||||
func (info rawHelloInfo) looksLikeEdge() bool {
|
||||
// "SChannel connections can by uniquely identified because SChannel
|
||||
// is the only TLS library we tested that includes the OCSP status
|
||||
// request extension before the supported groups and EC point formats
|
||||
// extensions." (early 2016)
|
||||
//
|
||||
// More specifically, the OCSP status request extension appears
|
||||
// *directly* before the other two extensions, which occur in that
|
||||
// order. (I contacted the authors for clarification and verified it.)
|
||||
for i, ext := range info.Extensions {
|
||||
if ext == extensionOCSPStatusRequest {
|
||||
if len(info.Extensions) <= i+2 {
|
||||
return false
|
||||
}
|
||||
if info.Extensions[i+1] != extensionSupportedCurves ||
|
||||
info.Extensions[i+2] != extensionSupportedPoints {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, cs := range info.CipherSuites {
|
||||
// As of Feb. 2017, Edge does not have 0xff, but Avast adds it
|
||||
if cs == scsvRenegotiation {
|
||||
return false
|
||||
}
|
||||
// Edge and modern IE do not have 0x4 or 0x5, but Blue Coat does
|
||||
if cs == TLS_RSA_WITH_RC4_128_MD5 || cs == tls.TLS_RSA_WITH_RC4_128_SHA {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if hasGreaseCiphers(info.CipherSuites) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// looksLikeSafari returns true if info looks like a handshake
|
||||
// from a modern version of MS Safari.
|
||||
func (info rawHelloInfo) looksLikeSafari() bool {
|
||||
// "One unique aspect of Secure Transport is that it includes
|
||||
// the TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0xff) cipher first,
|
||||
// whereas the other libraries we investigated include the
|
||||
// cipher last. Similar to Microsoft, Apple has changed
|
||||
// TLS behavior in minor OS updates, which are not indicated
|
||||
// in the HTTP User-Agent header. We allow for any of the
|
||||
// updates when validating handshakes, and we check for the
|
||||
// presence and ordering of ciphers, extensions, elliptic
|
||||
// curves, and compression methods." (early 2016)
|
||||
|
||||
// Note that any C lib (e.g. curl) compiled on macOS
|
||||
// will probably use Secure Transport which will also
|
||||
// share the TLS handshake characteristics of Safari.
|
||||
|
||||
// We check for the presence and order of the extensions.
|
||||
requiredExtensionsOrder := []uint16{10, 11, 13, 13172, 16, 5, 18, 23}
|
||||
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.Extensions, true) {
|
||||
// Safari on iOS 11 (beta) uses different set/ordering of extensions
|
||||
requiredExtensionsOrderiOS11 := []uint16{65281, 0, 23, 13, 5, 13172, 18, 16, 11, 10}
|
||||
if !assertPresenceAndOrdering(requiredExtensionsOrderiOS11, info.Extensions, true) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// For these versions of Safari, expect TLS_EMPTY_RENEGOTIATION_INFO_SCSV first.
|
||||
if len(info.CipherSuites) < 1 {
|
||||
return false
|
||||
}
|
||||
if info.CipherSuites[0] != scsvRenegotiation {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if hasGreaseCiphers(info.CipherSuites) {
|
||||
return false
|
||||
}
|
||||
|
||||
// We check for order and presence of cipher suites
|
||||
expectedCipherSuiteOrder := []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, // 0xc02c
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // 0xc02b
|
||||
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, // 0xc024
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, // 0xc023
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // 0xc00a
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // 0xc009
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, // 0xc030
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // 0xc02f
|
||||
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, // 0xc028
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, // 0xc027
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // 0xc014
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // 0xc013
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // 0x9d
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // 0x9c
|
||||
TLS_RSA_WITH_AES_256_CBC_SHA256, // 0x3d
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA256, // 0x3c
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA, // 0x2f
|
||||
}
|
||||
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.CipherSuites, true)
|
||||
}
|
||||
|
||||
// looksLikeTor returns true if the info looks like a ClientHello from Tor browser
|
||||
// (based on Firefox).
|
||||
func (info rawHelloInfo) looksLikeTor() bool {
|
||||
requiredExtensionsOrder := []uint16{10, 11, 16, 5, 13}
|
||||
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.Extensions, true) {
|
||||
return false
|
||||
}
|
||||
|
||||
// check for session tickets support; Tor doesn't support them to prevent tracking
|
||||
for _, ext := range info.Extensions {
|
||||
if ext == 35 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// We check for both presence of curves and their ordering, including
|
||||
// an optional curve at the beginning (for Tor based on Firefox 52)
|
||||
infoCurves := info.Curves
|
||||
if len(info.Curves) == 4 {
|
||||
if info.Curves[0] != 29 {
|
||||
return false
|
||||
}
|
||||
infoCurves = info.Curves[1:]
|
||||
}
|
||||
requiredCurves := []tls.CurveID{23, 24, 25}
|
||||
if len(infoCurves) < len(requiredCurves) {
|
||||
return false
|
||||
}
|
||||
for i := range requiredCurves {
|
||||
if infoCurves[i] != requiredCurves[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if hasGreaseCiphers(info.CipherSuites) {
|
||||
return false
|
||||
}
|
||||
|
||||
// We check for order of cipher suites but not presence, since
|
||||
// according to the paper, cipher suites may be not be added
|
||||
// or reordered by the user, but they may be disabled.
|
||||
expectedCipherSuiteOrder := []uint16{
|
||||
TLS_AES_128_GCM_SHA256, // 0x1301
|
||||
TLS_CHACHA20_POLY1305_SHA256, // 0x1303
|
||||
TLS_AES_256_GCM_SHA384, // 0x1302
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // 0xc02b
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // 0xc02f
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, // 0xcca9
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, // 0xcca8
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, // 0xc02c
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, // 0xc030
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // 0xc00a
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // 0xc009
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // 0xc013
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // 0xc014
|
||||
TLS_DHE_RSA_WITH_AES_128_CBC_SHA, // 0x33
|
||||
TLS_DHE_RSA_WITH_AES_256_CBC_SHA, // 0x39
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA, // 0x2f
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // 0xa
|
||||
}
|
||||
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.CipherSuites, false)
|
||||
}
|
||||
|
||||
// assertPresenceAndOrdering will return true if candidateList contains
|
||||
// the items in requiredItems in the same order as requiredItems.
|
||||
//
|
||||
// If requiredIsSubset is true, then all items in requiredItems must be
|
||||
// present in candidateList. If requiredIsSubset is false, then requiredItems
|
||||
// may contain items that are not in candidateList.
|
||||
//
|
||||
// In all cases, the order of requiredItems is enforced.
|
||||
func assertPresenceAndOrdering(requiredItems, candidateList []uint16, requiredIsSubset bool) bool {
|
||||
superset := requiredItems
|
||||
subset := candidateList
|
||||
if requiredIsSubset {
|
||||
superset = candidateList
|
||||
subset = requiredItems
|
||||
}
|
||||
|
||||
var j int
|
||||
for _, item := range subset {
|
||||
var found bool
|
||||
for j < len(superset) {
|
||||
if superset[j] == item {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
j++
|
||||
}
|
||||
if j == len(superset) && !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func hasGreaseCiphers(cipherSuites []uint16) bool {
|
||||
for _, cipher := range cipherSuites {
|
||||
if _, ok := greaseCiphers[cipher]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pool buffers so we can reuse allocations over time
|
||||
var bufpool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
var greaseCiphers = map[uint16]struct{}{
|
||||
0x0A0A: {},
|
||||
0x1A1A: {},
|
||||
0x2A2A: {},
|
||||
0x3A3A: {},
|
||||
0x4A4A: {},
|
||||
0x5A5A: {},
|
||||
0x6A6A: {},
|
||||
0x7A7A: {},
|
||||
0x8A8A: {},
|
||||
0x9A9A: {},
|
||||
0xAAAA: {},
|
||||
0xBABA: {},
|
||||
0xCACA: {},
|
||||
0xDADA: {},
|
||||
0xEAEA: {},
|
||||
0xFAFA: {},
|
||||
}
|
||||
|
||||
// Define variables used for TLS communication
|
||||
const (
|
||||
extensionOCSPStatusRequest = 5
|
||||
extensionSupportedCurves = 10 // also called "SupportedGroups"
|
||||
extensionSupportedPoints = 11
|
||||
extensionHeartbeat = 15
|
||||
|
||||
scsvRenegotiation = 0xff
|
||||
|
||||
// cipher suites missing from the crypto/tls package,
|
||||
// in no particular order here
|
||||
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = 0xc024
|
||||
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 = 0xc028
|
||||
TLS_RSA_WITH_AES_256_CBC_SHA256 = 0x3d
|
||||
TLS_DHE_RSA_WITH_AES_128_CBC_SHA = 0x33
|
||||
TLS_DHE_RSA_WITH_AES_256_CBC_SHA = 0x39
|
||||
TLS_RSA_WITH_RC4_128_MD5 = 0x4
|
||||
|
||||
// new PSK ciphers introduced by TLS 1.3, not (yet) in crypto/tls
|
||||
// https://tlswg.github.io/tls13-spec/#rfc.appendix.A.4)
|
||||
TLS_AES_128_GCM_SHA256 = 0x1301
|
||||
TLS_AES_256_GCM_SHA384 = 0x1302
|
||||
TLS_CHACHA20_POLY1305_SHA256 = 0x1303
|
||||
TLS_AES_128_CCM_SHA256 = 0x1304
|
||||
TLS_AES_128_CCM_8_SHA256 = 0x1305
|
||||
)
|
||||
@@ -0,0 +1,424 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseClientHello(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
inputHex string
|
||||
expected rawHelloInfo
|
||||
}{
|
||||
{
|
||||
// curl 7.51.0 (x86_64-apple-darwin16.0) libcurl/7.51.0 SecureTransport zlib/1.2.8
|
||||
inputHex: `010000a6030358a28c73a71bdfc1f09dee13fecdc58805dcce42ac44254df548f14645f7dc2c00004400ffc02cc02bc024c023c00ac009c008c030c02fc028c027c014c013c012009f009e006b0067003900330016009d009c003d003c0035002f000a00af00ae008d008c008b01000039000a00080006001700180019000b00020100000d00120010040102010501060104030203050306030005000501000000000012000000170000`,
|
||||
expected: rawHelloInfo{
|
||||
Version: 0x303,
|
||||
CipherSuites: []uint16{255, 49196, 49195, 49188, 49187, 49162, 49161, 49160, 49200, 49199, 49192, 49191, 49172, 49171, 49170, 159, 158, 107, 103, 57, 51, 22, 157, 156, 61, 60, 53, 47, 10, 175, 174, 141, 140, 139},
|
||||
Extensions: []uint16{10, 11, 13, 5, 18, 23},
|
||||
CompressionMethods: []byte{0},
|
||||
Curves: []tls.CurveID{23, 24, 25},
|
||||
Points: []uint8{0},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Chrome 56
|
||||
inputHex: `010000c003031dae75222dae1433a5a283ddcde8ddabaefbf16d84f250eee6fdff48cdfff8a00000201a1ac02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a010000777a7a0000ff010001000000000e000c0000096c6f63616c686f73740017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a0008aaaa001d001700182a2a000100`,
|
||||
expected: rawHelloInfo{
|
||||
Version: 0x303,
|
||||
CipherSuites: []uint16{6682, 49195, 49199, 49196, 49200, 52393, 52392, 52244, 52243, 49171, 49172, 156, 157, 47, 53, 10},
|
||||
Extensions: []uint16{31354, 65281, 0, 23, 35, 13, 5, 18, 16, 30032, 11, 10, 10794},
|
||||
CompressionMethods: []byte{0},
|
||||
Curves: []tls.CurveID{43690, 29, 23, 24},
|
||||
Points: []uint8{0},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Firefox 51
|
||||
inputHex: `010000bd030375f9022fc3a6562467f3540d68013b2d0b961979de6129e944efe0b35531323500001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a010000760000000e000c0000096c6f63616c686f737400170000ff01000100000a000a0008001d001700180019000b00020100002300000010000e000c02683208687474702f312e31000500050100000000ff030000000d0020001e040305030603020308040805080604010501060102010402050206020202`,
|
||||
expected: rawHelloInfo{
|
||||
Version: 0x303,
|
||||
CipherSuites: []uint16{49195, 49199, 52393, 52392, 49196, 49200, 49162, 49161, 49171, 49172, 51, 57, 47, 53, 10},
|
||||
Extensions: []uint16{0, 23, 65281, 10, 11, 35, 16, 5, 65283, 13},
|
||||
CompressionMethods: []byte{0},
|
||||
Curves: []tls.CurveID{29, 23, 24, 25},
|
||||
Points: []uint8{0},
|
||||
},
|
||||
},
|
||||
{
|
||||
// openssl s_client (OpenSSL 0.9.8zh 14 Jan 2016)
|
||||
inputHex: `0100012b03035d385236b8ca7b7946fa0336f164e76bf821ed90e8de26d97cc677671b6f36380000acc030c02cc028c024c014c00a00a500a300a1009f006b006a0069006800390038003700360088008700860085c032c02ec02ac026c00fc005009d003d00350084c02fc02bc027c023c013c00900a400a200a0009e00670040003f003e0033003200310030009a0099009800970045004400430042c031c02dc029c025c00ec004009c003c002f009600410007c011c007c00cc00200050004c012c008001600130010000dc00dc003000a00ff0201000055000b000403000102000a001c001a00170019001c001b0018001a0016000e000d000b000c0009000a00230000000d0020001e060106020603050105020503040104020403030103020303020102020203000f000101`,
|
||||
expected: rawHelloInfo{
|
||||
Version: 0x303,
|
||||
CipherSuites: []uint16{49200, 49196, 49192, 49188, 49172, 49162, 165, 163, 161, 159, 107, 106, 105, 104, 57, 56, 55, 54, 136, 135, 134, 133, 49202, 49198, 49194, 49190, 49167, 49157, 157, 61, 53, 132, 49199, 49195, 49191, 49187, 49171, 49161, 164, 162, 160, 158, 103, 64, 63, 62, 51, 50, 49, 48, 154, 153, 152, 151, 69, 68, 67, 66, 49201, 49197, 49193, 49189, 49166, 49156, 156, 60, 47, 150, 65, 7, 49169, 49159, 49164, 49154, 5, 4, 49170, 49160, 22, 19, 16, 13, 49165, 49155, 10, 255},
|
||||
Extensions: []uint16{11, 10, 35, 13, 15},
|
||||
CompressionMethods: []byte{1, 0},
|
||||
Curves: []tls.CurveID{23, 25, 28, 27, 24, 26, 22, 14, 13, 11, 12, 9, 10},
|
||||
Points: []uint8{0, 1, 2},
|
||||
},
|
||||
},
|
||||
} {
|
||||
data, err := hex.DecodeString(test.inputHex)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not decode hex data: %v", i, err)
|
||||
}
|
||||
actual := parseRawClientHello(data)
|
||||
if !reflect.DeepEqual(test.expected, actual) {
|
||||
t.Errorf("Test %d: Expected %+v; got %+v", i, test.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeuristicFunctionsAndHandler(t *testing.T) {
|
||||
// To test the heuristics, we assemble a collection of real
|
||||
// ClientHello messages from various TLS clients, both genuine
|
||||
// and intercepted. Please be sure to hex-encode them and
|
||||
// document the User-Agent associated with the connection
|
||||
// as well as any intercepting proxy as thoroughly as possible.
|
||||
//
|
||||
// If the TLS client used is not an HTTP client (e.g. s_client),
|
||||
// you can leave the userAgent blank, but please use a comment
|
||||
// to document crucial missing information such as client name,
|
||||
// version, and platform, maybe even the date you collected
|
||||
// the sample! Please group similar clients together, ordered
|
||||
// by version for convenience.
|
||||
|
||||
// clientHello pairs a User-Agent string to its ClientHello message.
|
||||
type clientHello struct {
|
||||
userAgent string
|
||||
helloHex string // do NOT include the header, just the ClientHello message
|
||||
interception bool // if test case shows an interception, set to true
|
||||
reqHeaders http.Header // if the request should set any headers to imitate a browser or proxy
|
||||
}
|
||||
|
||||
// clientHellos groups samples of true (real) ClientHellos by the
|
||||
// name of the browser that produced them. We limit the set of
|
||||
// browsers to those we are programmed to protect, as well as a
|
||||
// category for "Other" which contains real ClientHello messages
|
||||
// from clients that we do not recognize, which may be used to
|
||||
// test or imitate interception scenarios.
|
||||
//
|
||||
// Please group similar clients and order by version for convenience
|
||||
// when adding to the test cases.
|
||||
clientHellos := map[string][]clientHello{
|
||||
"Chrome": {
|
||||
{
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
||||
helloHex: `010000c003031dae75222dae1433a5a283ddcde8ddabaefbf16d84f250eee6fdff48cdfff8a00000201a1ac02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a010000777a7a0000ff010001000000000e000c0000096c6f63616c686f73740017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a0008aaaa001d001700182a2a000100`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
// Chrome on iOS will use iOS' TLS stack for requests that load
|
||||
// the web page (apparently required by the dev ToS) but will use its
|
||||
// own TLS stack for everything else, it seems.
|
||||
|
||||
// Chrome on iOS
|
||||
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.79 Mobile/14A456 Safari/602.1",
|
||||
helloHex: `010000de030358b062c509b21410a6496b5a82bfec74436cdecebe8ea1da29799939bbd3c17200002c00ffc02cc02bc024c023c00ac009c008c030c02fc028c027c014c013c012009d009c003d003c0035002f000a0100008900000014001200000f66696e6572706978656c732e636f6d000a00080006001700180019000b00020100000d00120010040102010501060104030203050306033374000000100030002e0268320568322d31360568322d31350568322d313408737064792f332e3106737064792f3308687474702f312e310005000501000000000012000000170000`,
|
||||
},
|
||||
{
|
||||
// Chrome on iOS (requesting favicon)
|
||||
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.79 Mobile/14A456 Safari/602.1",
|
||||
helloHex: `010000c20303863eb64788e3b9638c261300318411cbdd8f09576d58eec1e744b6ce944f574f0000208a8acca9cca8cc14cc13c02bc02fc02cc030c013c014009c009d002f0035000a01000079baba0000ff0100010000000014001200000f66696e6572706978656c732e636f6d0017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e31000b00020100000a000a00083a3a001d001700184a4a000100`,
|
||||
},
|
||||
{
|
||||
userAgent: "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
||||
helloHex: `010000c603036f717a88212c3e9e41940f82c42acb3473e0e4a64e8f52d9af33d34e972e08a30000206a6ac02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a0100007d7a7a0000ff0100010000000014001200000f66696e6572706978656c732e636f6d0017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a00087a7a001d001700188a8a000100`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
userAgent: "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
||||
helloHex: `010001fc030383141d213d1bf069171843489faf808028d282c9828e1ba87637c863833c730720a67e76e152f4b704523b72317ef4587e231f02e2395e0ecac6be9f28c35e6ce600208a8ac02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a010001931a1a0000ff0100010000000014001200000f66696e6572706978656c732e636f6d00170000002300785e85429bf1764f33111cd3ad5d1c56d765976fd962b49dbecbb6f7865e2a8d8536ad854f1fa99a8bbbf998814fee54a63a0bf162869d2bba37e9778304e7c4140825718e191b574c6246a0611de6447bdd80417f83ff9d9b7124069a9f74b90394ecb89bec5f6a1a67c1b89e50b8674782f53dd51807651a000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a00081a1a001d001700182a2a0001000015009a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
|
||||
helloHex: `010000c203034166c97e2016046e0c88ad867c410d0aee470f4d9b4ec8fe41a751d2a6348e3100001c4a4ac02bc02fc02cc030cca9cca8c013c014009c009d002f0035000a0100007dcaca0000ff0100010000000014001200000f66696e6572706978656c732e636f6d0017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a00086a6a001d001700187a7a000100`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
|
||||
helloHex: `010000c203037741795e73cd5b4949f79a0dc9cccc8b006e4c0ec324f965c6fe9f0833909f0100001c7a7ac02bc02fc02cc030cca9cca8c013c014009c009d002f0035000a0100007d7a7a0000ff0100010000000014001200000f66696e6572706978656c732e636f6d0017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a00084a4a001d001700185a5a000100`,
|
||||
interception: false,
|
||||
},
|
||||
},
|
||||
"Firefox": {
|
||||
{
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:51.0) Gecko/20100101 Firefox/51.0",
|
||||
helloHex: `010000bd030375f9022fc3a6562467f3540d68013b2d0b961979de6129e944efe0b35531323500001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a010000760000000e000c0000096c6f63616c686f737400170000ff01000100000a000a0008001d001700180019000b00020100002300000010000e000c02683208687474702f312e31000500050100000000ff030000000d0020001e040305030603020308040805080604010501060102010402050206020202`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) Gecko/20100101 Firefox/53.0",
|
||||
helloHex: `010001fc0303c99d54ae0628bbb9fea3833a4244c6a712cac9d7738f4930b8b9d8e2f6bd578220f7936cedb48907981c9292fb08ceee6f59bd6fddb3d4271ccd7c12380c5038ab001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a01000195001500af000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000c0000096c6f63616c686f737400170000ff01000100000a000a0008001d001700180019000b000201000023007886da2d41843ff42131b856982c19a545837b70e604325423a817d925e9d95bd084737682cea6b804dfb7cbe336a3b27b8d520d57520c29cfe5f4f3d3236183b84b05c18f0ca30bf598111e390086fea00d9631f1f78527277eb7838b86e73c4e5d15b55d086b1a4a8aa29f12a55126c6274bcd499bbeb23a0010000e000c02683208687474702f312e31000500050100000000000d0018001604030503060308040805080604010501060102030201`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) Gecko/20100101 Firefox/53.0",
|
||||
helloHex: `010000b1030365d899820b999245d571c2f7d6b850f63ad931d3c68ceb9cf5a508421a871dc500001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100006a0000000e000c0000096c6f63616c686f737400170000ff01000100000a000a0008001d001700180019000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0018001604030503060308040805080604010501060102030201`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
// this was a Nightly release at the time
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:55.0) Gecko/20100101 Firefox/55.0",
|
||||
helloHex: `010001fc030331e380b7d12018e1202ef3327607203df5c5732b4fa5ab5abaf0b60034c2fb662070c836b9b89123e37f4f1074d152df438fa8ee8a0f89b036fd952f4fcc0b994f001c130113031302c02bc02fcca9cca8c02cc030c013c014002f0035000a0100019700000014001200000f63616464797365727665722e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b0002010000230078c97e7716a041e2ea824571bef26a3dff2bf50a883cd15d904ab2d17deb514f6e0a079ee7c212c000178387ffafc2e530b6df6662f570aae134330f13c458a0eaad5a96a9696f572110918740b15db1143d19aaaa706942030b433a7e6150f62b443c0564e5b8f7ee9577bf3bf7faec8c67425b648ab54d880010000e000c02683208687474702f312e310005000501000000000028006b0069001d0020aee6e596155ee6f79f943e81ceabe0979d27fbbb8b9189ccb2ebc75226351f32001700410421875a44e510decac11ef1d7cfddd4dfe105d5cd3a2d42fba03ebde23e51e8ce65bda1b48be82d4848d1db2bfce68e94092e925a9ce0dbf5df35479558108489002b0009087f12030303020301000d0018001604030503060308040805080604010501060102030201002d000201010015002500000000000000000000000000000000000000000000000000000000000000000000000000`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
// Firefox on Fedora (RedHat) doesn't include ECC ciphers because of patent liabilities
|
||||
userAgent: "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0",
|
||||
helloHex: `010000b70303f5280b74d617d42e39fd77b78a2b537b1d7787ce4fcbcf3604c9fbcd677c6c5500001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100007000000014001200000f66696e6572706978656c732e636f6d00170000ff01000100000a000a0008001d001700180019000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0018001604030503060308040805080604010501060102030201`,
|
||||
interception: false,
|
||||
},
|
||||
},
|
||||
"Edge": {
|
||||
{
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
|
||||
helloHex: `010000bd030358a3c9bf05f734842e189fb6ce653b67b846e990bc1fc5fb8c397874d06020f1000038c02cc02bc030c02f009f009ec024c023c028c027c00ac009c014c01300390033009d009c003d003c0035002f000a006a00400038003200130100005c000500050100000000000a00080006001d00170018000b00020100000d00140012040105010201040305030203020206010603002300000010000e000c02683208687474702f312e310017000055000006000100020002ff01000100`,
|
||||
interception: false,
|
||||
},
|
||||
},
|
||||
"Safari": {
|
||||
{
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8",
|
||||
helloHex: `010000d2030358a295b513c8140c6ff880f4a8a73cc830ed2dab2c4f2068eb365228d828732e00002600ffc02cc02bc024c023c00ac009c030c02fc028c027c014c013009d009c003d003c0035002f010000830000000e000c0000096c6f63616c686f7374000a00080006001700180019000b00020100000d00120010040102010501060104030203050306033374000000100030002e0268320568322d31360568322d31350568322d313408737064792f332e3106737064792f3308687474702f312e310005000501000000000012000000170000`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
// I think this was iOS 11 beta
|
||||
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.28 (KHTML, like Gecko) Version/11.0 Mobile/15A5318g Safari/604.1",
|
||||
helloHex: `010000e10303be294e11847ba01301e0bb6129f4a0d66344602141a8f0a1ab0750a1db145755000028c02cc02bc024c023cca9c00ac009c030c02fc028c027cca8c014c013009d009c003d003c0035002f01000090ff0100010000000014001200000f66696e6572706978656c732e636f6d00170000000d00140012040308040401050308050501080606010201000500050100000000337400000012000000100030002e0268320568322d31360568322d31350568322d313408737064792f332e3106737064792f3308687474702f312e31000b00020100000a00080006001d00170018`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
// iOS 11 stable
|
||||
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
|
||||
helloHex: `010000dc030327fafb16708fcbe489fda332260d32b1a22bea6672a72b5e61d7b9963df1b10d000028c02cc02bc024c023c00ac009cca9c030c02fc028c027c014c013cca8009d009c003d003c0035002f0100008bff010001000000000f000d00000a6d69746d2e776174636800170000000d00140012040308040401050308050501080606010201000500050100000000337400000012000000100030002e0268320568322d31360568322d31350568322d313408737064792f332e3106737064792f3308687474702f312e31000b00020100000a00080006001d00170018`,
|
||||
interception: false,
|
||||
},
|
||||
},
|
||||
"Tor": {
|
||||
{
|
||||
userAgent: "Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0",
|
||||
helloHex: `010000a40303137f05d4151f2d9095aee4254416d9dce73d6a1d857e8097ea20d021c04a7a81000016c02bc02fc00ac009c013c01400330039002f0035000a0100006500000014001200000f66696e6572706978656c732e636f6dff01000100000a00080006001700180019000b00020100337400000010000b000908687474702f312e31000500050100000000000d001600140401050106010201040305030603020304020202`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
userAgent: "Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0",
|
||||
helloHex: `010000b4030322e1f3aff4c37caba303c2ce53ba1689b3e70117a46f413d44f70a74cb6a496100001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100006d00000014001200000f66696e6572706978656c732e636f6d00170000ff01000100000a000a0008001d001700180019000b000201000010000b000908687474702f312e31000500050100000000ff030000000d0018001604030503060308040805080604010501060102030201`,
|
||||
interception: false,
|
||||
},
|
||||
},
|
||||
"Other": { // these are either non-browser clients or intercepted client hellos
|
||||
{
|
||||
// openssl s_client (OpenSSL 0.9.8zh 14 Jan 2016) - NOT an interception, but not a browser either
|
||||
helloHex: `0100012b03035d385236b8ca7b7946fa0336f164e76bf821ed90e8de26d97cc677671b6f36380000acc030c02cc028c024c014c00a00a500a300a1009f006b006a0069006800390038003700360088008700860085c032c02ec02ac026c00fc005009d003d00350084c02fc02bc027c023c013c00900a400a200a0009e00670040003f003e0033003200310030009a0099009800970045004400430042c031c02dc029c025c00ec004009c003c002f009600410007c011c007c00cc00200050004c012c008001600130010000dc00dc003000a00ff0201000055000b000403000102000a001c001a00170019001c001b0018001a0016000e000d000b000c0009000a00230000000d0020001e060106020603050105020503040104020403030103020303020102020203000f000101`,
|
||||
// NOTE: This test case is not actually an interception, but s_client is not a browser
|
||||
// or any client we support MITM checking for, either. Since it advertises heartbeat,
|
||||
// our heuristics still flag it as a MITM.
|
||||
interception: true,
|
||||
},
|
||||
{
|
||||
// curl 7.51.0 (x86_64-apple-darwin16.0) libcurl/7.51.0 SecureTransport zlib/1.2.8
|
||||
userAgent: "curl/7.51.0",
|
||||
helloHex: `010000a6030358a28c73a71bdfc1f09dee13fecdc58805dcce42ac44254df548f14645f7dc2c00004400ffc02cc02bc024c023c00ac009c008c030c02fc028c027c014c013c012009f009e006b0067003900330016009d009c003d003c0035002f000a00af00ae008d008c008b01000039000a00080006001700180019000b00020100000d00120010040102010501060104030203050306030005000501000000000012000000170000`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
// Avast 17.1.2286 (Feb. 2017) on Windows 10 x64 build 14393, intercepting Edge
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
|
||||
helloHex: `010000ce0303b418fdc4b6cf6436a5e2bfb06b96ed5faa7285c20c7b49341a78be962a9dc40000003ac02cc02bc030c02f009f009ec024c023c028c027c00ac009c014c01300390033009d009c003d003c0035002f000a006a004000380032001300ff0100006b00000014001200000f66696e6572706978656c732e636f6d000b000403000102000a00080006001d0017001800230000000d001400120401050102010403050302030202060106030005000501000000000010000e000c02683208687474702f312e310016000000170000`,
|
||||
interception: true,
|
||||
},
|
||||
{
|
||||
// Kaspersky Internet Security 17.0.0.611 on Windows 10 x64 build 14393, intercepting Edge
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
|
||||
helloHex: `010000eb030361ce302bf4b0d5adf1ff30b2cf433c4a4b68f33e07b2651695e7ae6ec3cf126400003ac02cc02bc030c02f009f009ec024c023c028c027c00ac009c014c01300390033009d009c003d003c0035002f000a006a004000380032001300ff0100008800000014001200000f66696e6572706978656c732e636f6d000b000403000102000a001c001a00170019001c001b0018001a0016000e000d000b000c0009000a00230000000d0020001e060106020603050105020503040104020403030103020303020102020203000500050100000000000f0001010010000e000c02683208687474702f312e31`,
|
||||
interception: true,
|
||||
},
|
||||
{
|
||||
// Kaspersky Internet Security 17.0.0.611 on Windows 10 x64 build 14393, intercepting Firefox 51
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0",
|
||||
helloHex: `010001fc0303768e3f9ea75194c7cb03d23e8e6371b95fb696d339b797be57a634309ec98a42200f2a7554098364b7f05d21a8c7f43f31a893a4fc5670051020408c8e4dc234dd001cc02bc02fc02cc030c00ac009c013c01400330039002f0035000a00ff0100019700000014001200000f66696e6572706978656c732e636f6d000b000403000102000a001c001a00170019001c001b0018001a0016000e000d000b000c0009000a00230078bf4e244d4de3d53c6331edda9672dfc4a17aae92b671e86da1368b1b5ae5324372817d8f3b7ffe1a7a1537a5049b86cd7c44863978c1e615b005942755da20fc3a4e34a16f78034aa3b1cffcef95f81a0995c522a53b0e95a4f98db84c43359d93d8647b2de2a69f3ebdcfc6bca452730cbd00179226dedf000d0020001e060106020603050105020503040104020403030103020303020102020203000500050100000000000f0001010010000e000c02683208687474702f312e3100150093000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000`,
|
||||
interception: true,
|
||||
},
|
||||
{
|
||||
// Kaspersky Internet Security 17.0.0.611 on Windows 10 x64 build 14393, intercepting Chrome 56
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
||||
helloHex: `010000c903033481e7af24e647ba5a79ec97e9264c1a1f990cf842f50effe22be52130d5af82000018c02bc02fc02cc030c013c014009c009d002f0035000a00ff0100008800000014001200000f66696e6572706978656c732e636f6d000b000403000102000a001c001a00170019001c001b0018001a0016000e000d000b000c0009000a00230000000d0020001e060106020603050105020503040104020403030103020303020102020203000500050100000000000f0001010010000e000c02683208687474702f312e31`,
|
||||
interception: true,
|
||||
},
|
||||
{
|
||||
// AVG 17.1.3006 (build 17.1.3354.20) on Windows 10 x64 build 14393, intercepting Edge
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
|
||||
helloHex: `010000ca0303fd83091207161eca6b4887db50587109c50e463beb190362736b1fcf9e05f807000036c02cc02bc030c02f009f009ec024c023c028c027c00ac009c014c01300390033009d009c003d003c0035002f006a00400038003200ff0100006b00000014001200000f66696e6572706978656c732e636f6d000b000403000102000a00080006001d0017001800230000000d001400120401050102010403050302030202060106030005000501000000000010000e000c02683208687474702f312e310016000000170000`,
|
||||
interception: true,
|
||||
},
|
||||
{
|
||||
// IE 11 on Windows 7, this connection was intercepted by Blue Coat
|
||||
// no sensible User-Agent value, since Blue Coat changes it to something super generic
|
||||
// By the way, here's another reason we hate Blue Coat: they break TLS 1.3:
|
||||
// https://twitter.com/FiloSottile/status/835269932929667072
|
||||
helloHex: `010000b1030358a3f3bae627f464da8cb35976b88e9119640032d41e62a107d608ed8d3e62b9000034c028c027c014c013009f009e009d009cc02cc02bc024c023c00ac009003d003c0035002f006a004000380032000a0013000500040100005400000014001200000f66696e6572706978656c732e636f6d000500050100000000000a00080006001700180019000b00020100000d0014001206010603040105010201040305030203020200170000ff01000100`,
|
||||
interception: true,
|
||||
reqHeaders: http.Header{"X-Bluecoat-Via": {"66808702E9A2CF4"}}, // actual field name would be "X-BlueCoat-Via" but Go canonicalizes field names
|
||||
},
|
||||
{
|
||||
// Firefox 51.0.1 being intercepted by burp 1.7.17
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:51.0) Gecko/20100101 Firefox/51.0",
|
||||
helloHex: `010000d8030358a92f4daca95acc2f6a10a9c50d736135eae39406d3090238464540d482677600003ac023c027003cc025c02900670040c009c013002fc004c00e00330032c02bc02f009cc02dc031009e00a2c008c012000ac003c00d0016001300ff01000075000a0034003200170001000300130015000600070009000a0018000b000c0019000d000e000f001000110002001200040005001400080016000b00020100000d00180016060306010503050104030401040202030201020201010000001700150000126a61677561722e6b796877616e612e6f7267`,
|
||||
interception: true,
|
||||
},
|
||||
{
|
||||
// Chrome 56 on Windows 10 being intercepted by Fortigate (on some public school network); note: I had to enable TLS 1.0 for this test (proxy was issuing a SHA-1 cert to client)
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
||||
helloHex: `010000e5030158ac612125c83bae95282113b2a4c572cf613c160d234350fb6d0ddce879ffec000064003300320039003800160013c013c009c014c00ac012c008002f0035000a00150012003d003c00670040006b006ac011c0070096009a009900410084004500440088008700ba00be00bd00c000c400c3c03cc044c042c03dc045c04300090005000400ff01000058000a003600340000000100020003000400050006000700080009000a000b000c000d000e000f0010001100120013001400150016001700180019000b0002010000000014001200000f66696e6572706978656c732e636f6d`,
|
||||
interception: true,
|
||||
},
|
||||
{
|
||||
// IE 11 on Windows 10, intercepted by Fortigate (same firewall as above)
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko",
|
||||
helloHex: `010000e5030158ac634c5278d7b17421f23a64cc91d68c470c6b247322fe867ba035b373d05c000064003300320039003800160013c013c009c014c00ac012c008002f0035000a00150012003d003c00670040006b006ac011c0070096009a009900410084004500440088008700ba00be00bd00c000c400c3c03cc044c042c03dc045c04300090005000400ff01000058000a003600340000000100020003000400050006000700080009000a000b000c000d000e000f0010001100120013001400150016001700180019000b0002010000000014001200000f66696e6572706978656c732e636f6d`,
|
||||
interception: true,
|
||||
},
|
||||
{
|
||||
// Edge 38.14393.0.0 on Windows 10, intercepted by Fortigate (same as above)
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
|
||||
helloHex: `010000e5030158ac6421a45794b8ade6a0ac6c910cde0f99c49bb1ba737b88638ec8dcf0d077000064003300320039003800160013c013c009c014c00ac012c008002f0035000a00150012003d003c00670040006b006ac011c0070096009a009900410084004500440088008700ba00be00bd00c000c400c3c03cc044c042c03dc045c04300090005000400ff01000058000a003600340000000100020003000400050006000700080009000a000b000c000d000e000f0010001100120013001400150016001700180019000b0002010000000014001200000f66696e6572706978656c732e636f6d`,
|
||||
interception: true,
|
||||
},
|
||||
{
|
||||
// Firefox 50.0.1 on Windows 10, intercepted by Fortigate (same as above)
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0",
|
||||
helloHex: `010000e5030158ac64e40495e77b7baf2031281451620bfe354b0c37521ebc0a40f5dc0c0cb6000064003300320039003800160013c013c009c014c00ac012c008002f0035000a00150012003d003c00670040006b006ac011c0070096009a009900410084004500440088008700ba00be00bd00c000c400c3c03cc044c042c03dc045c04300090005000400ff01000058000a003600340000000100020003000400050006000700080009000a000b000c000d000e000f0010001100120013001400150016001700180019000b0002010000000014001200000f66696e6572706978656c732e636f6d`,
|
||||
interception: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for client, chs := range clientHellos {
|
||||
for i, ch := range chs {
|
||||
hello, err := hex.DecodeString(ch.helloHex)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Test %d: Error decoding ClientHello: %v", client, i, err)
|
||||
continue
|
||||
}
|
||||
parsed := parseRawClientHello(hello)
|
||||
|
||||
isChrome := parsed.looksLikeChrome()
|
||||
isFirefox := parsed.looksLikeFirefox()
|
||||
isSafari := parsed.looksLikeSafari()
|
||||
isEdge := parsed.looksLikeEdge()
|
||||
isTor := parsed.looksLikeTor()
|
||||
|
||||
// we want each of the heuristic functions to be as
|
||||
// exclusive but as low-maintenance as possible;
|
||||
// in other words, if one returns true, the others
|
||||
// should return false, with as little logic as possible,
|
||||
// but with enough logic to force TLS proxies to do a
|
||||
// good job preserving characteristics of the handshake.
|
||||
if (isChrome && (isFirefox || isSafari || isEdge || isTor)) ||
|
||||
(isFirefox && (isChrome || isSafari || isEdge || isTor)) ||
|
||||
(isSafari && (isChrome || isFirefox || isEdge || isTor)) ||
|
||||
(isEdge && (isChrome || isFirefox || isSafari || isTor)) ||
|
||||
(isTor && (isChrome || isFirefox || isSafari || isEdge)) {
|
||||
t.Errorf("[%s] Test %d: Multiple fingerprinting functions matched: "+
|
||||
"Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n",
|
||||
client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed)
|
||||
}
|
||||
|
||||
// test the handler and detection results
|
||||
var got, checked bool
|
||||
want := ch.interception
|
||||
handler := &tlsHandler{
|
||||
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
got, checked = r.Context().Value(MitmCtxKey).(bool)
|
||||
}),
|
||||
listener: newTLSListener(nil, nil),
|
||||
}
|
||||
handler.listener.helloInfos[""] = parsed
|
||||
w := httptest.NewRecorder()
|
||||
r, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Header.Set("User-Agent", ch.userAgent)
|
||||
if ch.reqHeaders != nil {
|
||||
for field, values := range ch.reqHeaders {
|
||||
r.Header[field] = values // NOTE: field names not standardized when setting directly like this!
|
||||
}
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
if got != want {
|
||||
t.Errorf("[%s] Test %d: Expected MITM=%v but got %v (type assertion OK (checked)=%v)",
|
||||
client, i, want, got, checked)
|
||||
t.Errorf("[%s] Test %d: Looks like Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n",
|
||||
client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVersion(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
UserAgent string
|
||||
SoftwareName string
|
||||
Version float64
|
||||
}{
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0",
|
||||
SoftwareName: "Firefox",
|
||||
Version: 45.0,
|
||||
},
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0 more_stuff_here",
|
||||
SoftwareName: "Firefox",
|
||||
Version: 45.0,
|
||||
},
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
|
||||
SoftwareName: "Safari",
|
||||
Version: 537.36,
|
||||
},
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
|
||||
SoftwareName: "Chrome",
|
||||
Version: 51.0270479,
|
||||
},
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
|
||||
SoftwareName: "Mozilla",
|
||||
Version: 5.0,
|
||||
},
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
|
||||
SoftwareName: "curl",
|
||||
Version: -1,
|
||||
},
|
||||
} {
|
||||
actual := getVersion(test.UserAgent, test.SoftwareName)
|
||||
if actual != test.Version {
|
||||
t.Errorf("Test [%d]: Expected version=%f, got version=%f for %s in '%s'",
|
||||
i, test.Version, actual, test.SoftwareName, test.UserAgent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,61 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Path represents a URI path.
|
||||
// Path represents a URI path. It should usually be
|
||||
// set to the value of a request path.
|
||||
type Path string
|
||||
|
||||
// Matches checks to see if other matches p.
|
||||
// Matches checks to see if base matches p. The correct
|
||||
// usage of this method sets p as the request path, and
|
||||
// base as a Caddyfile (user-defined) rule path.
|
||||
//
|
||||
// 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)
|
||||
//
|
||||
// Multiple slashes are collapsed/merged. See issue #1859.
|
||||
func (p Path) Matches(base string) bool {
|
||||
if base == "/" || base == "" {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(other))
|
||||
|
||||
// sanitize the paths for comparison, very important
|
||||
// (slightly lossy if the base path requires multiple
|
||||
// consecutive forward slashes, since those will be merged)
|
||||
pHasTrailingSlash := strings.HasSuffix(string(p), "/")
|
||||
baseHasTrailingSlash := strings.HasSuffix(base, "/")
|
||||
p = Path(path.Clean(string(p)))
|
||||
base = path.Clean(base)
|
||||
if pHasTrailingSlash {
|
||||
p += "/"
|
||||
}
|
||||
if baseHasTrailingSlash {
|
||||
base += "/"
|
||||
}
|
||||
|
||||
if CaseSensitivePath {
|
||||
return strings.HasPrefix(string(p), base)
|
||||
}
|
||||
return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(base))
|
||||
}
|
||||
|
||||
// PathMatcher is a Path RequestMatcher.
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPathMatches(t *testing.T) {
|
||||
for i, testcase := range []struct {
|
||||
reqPath Path
|
||||
rulePath string // or "base path" as in Caddyfile docs
|
||||
shouldMatch bool
|
||||
caseInsensitive bool
|
||||
}{
|
||||
{
|
||||
reqPath: "/",
|
||||
rulePath: "/",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "/foo/bar",
|
||||
rulePath: "/foo",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "/foobar",
|
||||
rulePath: "/foo/",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
reqPath: "/foobar",
|
||||
rulePath: "/foo/bar",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
reqPath: "/foo/",
|
||||
rulePath: "/foo/",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "/Foobar",
|
||||
rulePath: "/Foo",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
|
||||
reqPath: "/FooBar",
|
||||
rulePath: "/Foo",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "/foobar",
|
||||
rulePath: "/FooBar",
|
||||
shouldMatch: true,
|
||||
caseInsensitive: true,
|
||||
},
|
||||
{
|
||||
reqPath: "",
|
||||
rulePath: "/", // a lone forward slash means to match all requests (see issue #1645) - many future test cases related to this issue
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "foobar.php",
|
||||
rulePath: "/",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "",
|
||||
rulePath: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "/foo/bar",
|
||||
rulePath: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "/foo/bar",
|
||||
rulePath: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "no/leading/slash",
|
||||
rulePath: "/",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "no/leading/slash",
|
||||
rulePath: "/no/leading/slash",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
reqPath: "no/leading/slash",
|
||||
rulePath: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
// see issue #1859
|
||||
reqPath: "//double-slash",
|
||||
rulePath: "/double-slash",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "/double//slash",
|
||||
rulePath: "/double/slash",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "//more/double//slashes",
|
||||
rulePath: "/more/double/slashes",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "/path/../traversal",
|
||||
rulePath: "/traversal",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
reqPath: "/path/../traversal",
|
||||
rulePath: "/path",
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
reqPath: "/keep-slashes/http://something/foo/bar",
|
||||
rulePath: "/keep-slashes/http://something",
|
||||
shouldMatch: true,
|
||||
},
|
||||
} {
|
||||
CaseSensitivePath = !testcase.caseInsensitive
|
||||
if got, want := testcase.reqPath.Matches(testcase.rulePath), testcase.shouldMatch; got != want {
|
||||
t.Errorf("Test %d: For request path '%s' and base path '%s': expected %v, got %v",
|
||||
i, testcase.reqPath, testcase.rulePath, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CleanMaskedPath prevents one or more of the path cleanup operations:
|
||||
// - collapse multiple slashes into one
|
||||
// - eliminate "/." (current directory)
|
||||
// - eliminate "<parent_directory>/.."
|
||||
// by masking certain patterns in the path with a temporary random string.
|
||||
// This could be helpful when certain patterns in the path are desired to be preserved
|
||||
// that would otherwise be changed by path.Clean().
|
||||
// One such use case is the presence of the double slashes as protocol separator
|
||||
// (e.g., /api/endpoint/http://example.com).
|
||||
// This is a common pattern in many applications to allow passing URIs as path argument.
|
||||
func CleanMaskedPath(reqPath string, masks ...string) string {
|
||||
var replacerVal string
|
||||
maskMap := make(map[string]string)
|
||||
|
||||
// Iterate over supplied masks and create temporary replacement strings
|
||||
// only for the masks that are present in the path, then replace all occurrences
|
||||
for _, mask := range masks {
|
||||
if strings.Index(reqPath, mask) >= 0 {
|
||||
replacerVal = "/_caddy" + generateRandomString() + "__"
|
||||
maskMap[mask] = replacerVal
|
||||
reqPath = strings.Replace(reqPath, mask, replacerVal, -1)
|
||||
}
|
||||
}
|
||||
|
||||
reqPath = path.Clean(reqPath)
|
||||
|
||||
// Revert the replaced masks after path cleanup
|
||||
for mask, replacerVal := range maskMap {
|
||||
reqPath = strings.Replace(reqPath, replacerVal, mask, -1)
|
||||
}
|
||||
return reqPath
|
||||
}
|
||||
|
||||
// CleanPath calls CleanMaskedPath() with the default mask of "://"
|
||||
// to preserve double slashes of protocols
|
||||
// such as "http://", "https://", and "ftp://" etc.
|
||||
func CleanPath(reqPath string) string {
|
||||
return CleanMaskedPath(reqPath, "://")
|
||||
}
|
||||
|
||||
// An efficient and fast method for random string generation.
|
||||
// Inspired by http://stackoverflow.com/a/31832326.
|
||||
const randomStringLength = 4
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
const (
|
||||
letterIdxBits = 6
|
||||
letterIdxMask = 1<<letterIdxBits - 1
|
||||
letterIdxMax = 63 / letterIdxBits
|
||||
)
|
||||
|
||||
var src = rand.NewSource(time.Now().UnixNano())
|
||||
|
||||
func generateRandomString() string {
|
||||
b := make([]byte, randomStringLength)
|
||||
for i, cache, remain := randomStringLength-1, src.Int63(), letterIdxMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = src.Int63(), letterIdxMax
|
||||
}
|
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||
b[i] = letterBytes[idx]
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var paths = map[string]map[string]string{
|
||||
"/../a/b/../././/c": {
|
||||
"preserve_all": "/../a/b/../././/c",
|
||||
"preserve_protocol": "/a/c",
|
||||
"preserve_slashes": "/a//c",
|
||||
"preserve_dots": "/../a/b/../././c",
|
||||
"clean_all": "/a/c",
|
||||
},
|
||||
"/path/https://www.google.com": {
|
||||
"preserve_all": "/path/https://www.google.com",
|
||||
"preserve_protocol": "/path/https://www.google.com",
|
||||
"preserve_slashes": "/path/https://www.google.com",
|
||||
"preserve_dots": "/path/https:/www.google.com",
|
||||
"clean_all": "/path/https:/www.google.com",
|
||||
},
|
||||
"/a/b/../././/c/http://example.com/foo//bar/../blah": {
|
||||
"preserve_all": "/a/b/../././/c/http://example.com/foo//bar/../blah",
|
||||
"preserve_protocol": "/a/c/http://example.com/foo/blah",
|
||||
"preserve_slashes": "/a//c/http://example.com/foo/blah",
|
||||
"preserve_dots": "/a/b/../././c/http:/example.com/foo/bar/../blah",
|
||||
"clean_all": "/a/c/http:/example.com/foo/blah",
|
||||
},
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, expected, received string) {
|
||||
if expected != received {
|
||||
t.Errorf("\tExpected: %s\n\t\t\tRecieved: %s", expected, received)
|
||||
}
|
||||
}
|
||||
|
||||
func maskedTestRunner(t *testing.T, variation string, masks ...string) {
|
||||
for reqPath, transformation := range paths {
|
||||
assertEqual(t, transformation[variation], CleanMaskedPath(reqPath, masks...))
|
||||
}
|
||||
}
|
||||
|
||||
// No need to test the built-in path.Clean() function.
|
||||
// However, it could be useful to cross-examine the test dataset.
|
||||
func TestPathClean(t *testing.T) {
|
||||
for reqPath, transformation := range paths {
|
||||
assertEqual(t, transformation["clean_all"], path.Clean(reqPath))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanAll(t *testing.T) {
|
||||
maskedTestRunner(t, "clean_all")
|
||||
}
|
||||
|
||||
func TestPreserveAll(t *testing.T) {
|
||||
maskedTestRunner(t, "preserve_all", "//", "/..", "/.")
|
||||
}
|
||||
|
||||
func TestPreserveProtocol(t *testing.T) {
|
||||
maskedTestRunner(t, "preserve_protocol", "://")
|
||||
}
|
||||
|
||||
func TestPreserveSlashes(t *testing.T) {
|
||||
maskedTestRunner(t, "preserve_slashes", "//")
|
||||
}
|
||||
|
||||
func TestPreserveDots(t *testing.T) {
|
||||
maskedTestRunner(t, "preserve_dots", "/..", "/.")
|
||||
}
|
||||
|
||||
func TestDefaultMask(t *testing.T) {
|
||||
for reqPath, transformation := range paths {
|
||||
assertEqual(t, transformation["preserve_protocol"], CleanPath(reqPath))
|
||||
}
|
||||
}
|
||||
|
||||
func maskedBenchmarkRunner(b *testing.B, masks ...string) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
for reqPath := range paths {
|
||||
CleanMaskedPath(reqPath, masks...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPathClean(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
for reqPath := range paths {
|
||||
path.Clean(reqPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCleanAll(b *testing.B) {
|
||||
maskedBenchmarkRunner(b)
|
||||
}
|
||||
|
||||
func BenchmarkPreserveAll(b *testing.B) {
|
||||
maskedBenchmarkRunner(b, "//", "/..", "/.")
|
||||
}
|
||||
|
||||
func BenchmarkPreserveProtocol(b *testing.B) {
|
||||
maskedBenchmarkRunner(b, "://")
|
||||
}
|
||||
|
||||
func BenchmarkPreserveSlashes(b *testing.B) {
|
||||
maskedBenchmarkRunner(b, "//")
|
||||
}
|
||||
|
||||
func BenchmarkPreserveDots(b *testing.B) {
|
||||
maskedBenchmarkRunner(b, "/..", "/.")
|
||||
}
|
||||
|
||||
func BenchmarkDefaultMask(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
for reqPath := range paths {
|
||||
CleanPath(reqPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
+295
-59
@@ -1,6 +1,21 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -8,17 +23,23 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyfile"
|
||||
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy/telemetry"
|
||||
"github.com/mholt/certmagic"
|
||||
)
|
||||
|
||||
const serverType = "http"
|
||||
|
||||
func init() {
|
||||
flag.IntVar(&certmagic.HTTPPort, "http-port", certmagic.HTTPPort, "Default port to use for HTTP")
|
||||
flag.IntVar(&certmagic.HTTPSPort, "https-port", certmagic.HTTPSPort, "Default port to use for HTTPS")
|
||||
flag.StringVar(&Host, "host", DefaultHost, "Default host")
|
||||
flag.StringVar(&Port, "port", DefaultPort, "Default port")
|
||||
flag.StringVar(&Root, "root", DefaultRoot, "Root path of default site")
|
||||
@@ -48,6 +69,12 @@ func init() {
|
||||
caddy.RegisterParsingCallback(serverType, "root", hideCaddyfile)
|
||||
caddy.RegisterParsingCallback(serverType, "tls", activateHTTPS)
|
||||
caddytls.RegisterConfigGetter(serverType, func(c *caddy.Controller) *caddytls.Config { return GetConfig(c).TLS })
|
||||
|
||||
// disable the caddytls package reporting ClientHellos
|
||||
// to telemetry, since our MITM detector does this but
|
||||
// with more information than the standard lib provides
|
||||
// (as of May 2018)
|
||||
caddytls.ClientHelloTelemetry = false
|
||||
}
|
||||
|
||||
// hideCaddyfile hides the source/origin Caddyfile if it is within the
|
||||
@@ -55,6 +82,10 @@ func init() {
|
||||
func hideCaddyfile(cctx caddy.Context) error {
|
||||
ctx := cctx.(*httpContext)
|
||||
for _, cfg := range ctx.siteConfigs {
|
||||
// if no Caddyfile exists exit.
|
||||
if cfg.originCaddyfile == "" {
|
||||
return nil
|
||||
}
|
||||
absRoot, err := filepath.Abs(cfg.Root)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -64,17 +95,19 @@ func hideCaddyfile(cctx caddy.Context) error {
|
||||
return err
|
||||
}
|
||||
if strings.HasPrefix(absOriginCaddyfile, absRoot) {
|
||||
cfg.HiddenFiles = append(cfg.HiddenFiles, strings.TrimPrefix(absOriginCaddyfile, absRoot))
|
||||
cfg.HiddenFiles = append(cfg.HiddenFiles, filepath.ToSlash(strings.TrimPrefix(absOriginCaddyfile, absRoot)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newContext() caddy.Context {
|
||||
return &httpContext{keysToSiteConfigs: make(map[string]*SiteConfig)}
|
||||
func newContext(inst *caddy.Instance) caddy.Context {
|
||||
return &httpContext{instance: inst, keysToSiteConfigs: make(map[string]*SiteConfig)}
|
||||
}
|
||||
|
||||
type httpContext struct {
|
||||
instance *caddy.Instance
|
||||
|
||||
// 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
|
||||
@@ -94,18 +127,24 @@ func (h *httpContext) saveConfig(key string, cfg *SiteConfig) {
|
||||
// executing directives and otherwise prepares the directives to
|
||||
// be parsed and executed.
|
||||
func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
|
||||
siteAddrs := make(map[string]string)
|
||||
httpPort := strconv.Itoa(certmagic.HTTPPort)
|
||||
httpsPort := strconv.Itoa(certmagic.HTTPSPort)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
addr = addr.Normalize()
|
||||
key = addr.Key()
|
||||
if _, dup := h.keysToSiteConfigs[key]; dup {
|
||||
return serverBlocks, fmt.Errorf("duplicate site key: %s", key)
|
||||
}
|
||||
|
||||
// Fill in address components from command line so that middleware
|
||||
// have access to the correct information during setup
|
||||
if addr.Host == "" && Host != DefaultHost {
|
||||
@@ -115,12 +154,59 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd
|
||||
addr.Port = Port
|
||||
}
|
||||
|
||||
// Make sure the adjusted site address is distinct
|
||||
addrCopy := addr // make copy so we don't disturb the original, carefully-parsed address struct
|
||||
if addrCopy.Port == "" && Port == DefaultPort {
|
||||
addrCopy.Port = Port
|
||||
}
|
||||
addrStr := addrCopy.String()
|
||||
if otherSiteKey, dup := siteAddrs[addrStr]; dup {
|
||||
err := fmt.Errorf("duplicate site address: %s", addrStr)
|
||||
if (addrCopy.Host == Host && Host != DefaultHost) ||
|
||||
(addrCopy.Port == Port && Port != DefaultPort) {
|
||||
err = fmt.Errorf("site defined as %s is a duplicate of %s because of modified "+
|
||||
"default host and/or port values (usually via -host or -port flags)", key, otherSiteKey)
|
||||
}
|
||||
return serverBlocks, err
|
||||
}
|
||||
siteAddrs[addrStr] = key
|
||||
|
||||
// If default HTTP or HTTPS ports have been customized,
|
||||
// make sure the ACME challenge ports match
|
||||
var altHTTPPort, altTLSALPNPort int
|
||||
if httpPort != DefaultHTTPPort {
|
||||
portInt, err := strconv.Atoi(httpPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
altHTTPPort = portInt
|
||||
}
|
||||
if httpsPort != DefaultHTTPSPort {
|
||||
portInt, err := strconv.Atoi(httpsPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
altTLSALPNPort = portInt
|
||||
}
|
||||
|
||||
// Make our caddytls.Config, which has a pointer to the
|
||||
// instance's certificate cache and enough information
|
||||
// to use automatic HTTPS when the time comes
|
||||
caddytlsConfig, err := caddytls.NewConfig(h.instance)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating new caddytls configuration: %v", err)
|
||||
}
|
||||
caddytlsConfig.Hostname = addr.Host
|
||||
caddytlsConfig.Manager.AltHTTPPort = altHTTPPort
|
||||
caddytlsConfig.Manager.AltTLSALPNPort = altTLSALPNPort
|
||||
|
||||
// Save the config to our master list, and key it for lookups
|
||||
cfg := &SiteConfig{
|
||||
Addr: addr,
|
||||
Root: Root,
|
||||
TLS: &caddytls.Config{Hostname: addr.Host},
|
||||
TLS: caddytlsConfig,
|
||||
originCaddyfile: sourceFile,
|
||||
IndexPages: staticfiles.DefaultIndexPages,
|
||||
}
|
||||
h.saveConfig(key, cfg)
|
||||
}
|
||||
@@ -144,13 +230,47 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd
|
||||
// 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)
|
||||
httpPort := strconv.Itoa(certmagic.HTTPPort)
|
||||
httpsPort := strconv.Itoa(certmagic.HTTPSPort)
|
||||
|
||||
// make a rough estimate as to whether we're in a "production
|
||||
// environment/system" - start by assuming that most production
|
||||
// servers will set their default CA endpoint to a public,
|
||||
// trusted CA (obviously not a perfect heuristic)
|
||||
var looksLikeProductionCA bool
|
||||
for _, publicCAEndpoint := range caddytls.KnownACMECAs {
|
||||
if strings.Contains(certmagic.Default.CA, publicCAEndpoint) {
|
||||
looksLikeProductionCA = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate each site configuration and make sure that:
|
||||
// 1) TLS is disabled for explicitly-HTTP sites (necessary
|
||||
// when an HTTP address shares a block containing tls)
|
||||
// 2) if QUIC is enabled, TLS ClientAuth is not, because
|
||||
// currently, QUIC does not support ClientAuth (TODO:
|
||||
// revisit this when our QUIC implementation supports it)
|
||||
var atLeastOneSiteLooksLikeProduction bool
|
||||
for _, cfg := range h.siteConfigs {
|
||||
// see if all the addresses (both sites and
|
||||
// listeners) are loopback to help us determine
|
||||
// if this is a "production" instance or not
|
||||
if !atLeastOneSiteLooksLikeProduction {
|
||||
if !caddy.IsLoopback(cfg.Addr.Host) &&
|
||||
!caddy.IsLoopback(cfg.ListenHost) &&
|
||||
(caddytls.QualifiesForManagedTLS(cfg) ||
|
||||
certmagic.HostQualifies(cfg.Addr.Host)) {
|
||||
atLeastOneSiteLooksLikeProduction = true
|
||||
}
|
||||
}
|
||||
|
||||
// make sure TLS is disabled for explicitly-HTTP sites
|
||||
// (necessary when HTTP address shares a block containing tls)
|
||||
if !cfg.TLS.Enabled {
|
||||
continue
|
||||
}
|
||||
if cfg.Addr.Port == "80" || cfg.Addr.Scheme == "http" {
|
||||
if cfg.Addr.Port == httpPort || cfg.Addr.Scheme == "http" {
|
||||
cfg.TLS.Enabled = false
|
||||
log.Printf("[WARNING] TLS disabled for %s", cfg.Addr)
|
||||
} else if cfg.Addr.Scheme == "" {
|
||||
@@ -161,11 +281,16 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
|
||||
// is incorrect for this site.
|
||||
cfg.Addr.Scheme = "https"
|
||||
}
|
||||
if cfg.Addr.Port == "" && ((!cfg.TLS.Manual && !cfg.TLS.SelfSigned) || cfg.TLS.OnDemand) {
|
||||
if cfg.Addr.Port == "" && ((!cfg.TLS.Manual && !cfg.TLS.SelfSigned) || cfg.TLS.Manager.OnDemand != nil) {
|
||||
// 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"
|
||||
cfg.Addr.Port = httpsPort
|
||||
}
|
||||
if cfg.TLS.ClientAuth != tls.NoClientCert {
|
||||
if QUIC {
|
||||
return nil, fmt.Errorf("cannot enable TLS client authentication with QUIC, because QUIC does not yet support it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,22 +310,48 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
|
||||
servers = append(servers, s)
|
||||
}
|
||||
|
||||
// NOTE: This value is only a "good guess". Quite often, development
|
||||
// environments will use internal DNS or a local hosts file to serve
|
||||
// real-looking domains in local development. We can't easily tell
|
||||
// which without doing a DNS lookup, so this guess is definitely naive,
|
||||
// and if we ever want a better guess, we will have to do DNS lookups.
|
||||
deploymentGuess := "dev"
|
||||
if looksLikeProductionCA && atLeastOneSiteLooksLikeProduction {
|
||||
deploymentGuess = "prod"
|
||||
}
|
||||
telemetry.Set("http_deployment_guess", deploymentGuess)
|
||||
telemetry.Set("http_num_sites", len(h.siteConfigs))
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// normalizedKey returns "normalized" key representation:
|
||||
// scheme and host names are lowered, everything else stays the same
|
||||
func normalizedKey(key string) string {
|
||||
addr, err := standardizeAddress(key)
|
||||
if err != nil {
|
||||
return key
|
||||
}
|
||||
return addr.Normalize().Key()
|
||||
}
|
||||
|
||||
// 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)
|
||||
key := strings.ToLower(c.Key)
|
||||
key := normalizedKey(c.Key)
|
||||
if cfg, ok := ctx.keysToSiteConfigs[key]; ok {
|
||||
return cfg
|
||||
}
|
||||
// we should only get here during tests because directive
|
||||
// actions typically skip the server blocks where we make
|
||||
// the configs
|
||||
cfg := &SiteConfig{Root: Root, TLS: new(caddytls.Config)}
|
||||
cfg := &SiteConfig{
|
||||
Root: Root,
|
||||
TLS: &caddytls.Config{Manager: certmagic.NewDefault()},
|
||||
IndexPages: staticfiles.DefaultIndexPages,
|
||||
}
|
||||
ctx.saveConfig(key, cfg)
|
||||
return cfg
|
||||
}
|
||||
@@ -234,7 +385,7 @@ func groupSiteConfigsByListenAddr(configs []*SiteConfig) (map[string][]*SiteConf
|
||||
// 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
|
||||
// https://caddy.community/t/wildcard-virtual-domains-with-wildcard-roots/221/5?u=matt
|
||||
|
||||
if conf.Addr.Port == "" {
|
||||
conf.Addr.Port = Port
|
||||
@@ -255,6 +406,8 @@ func groupSiteConfigsByListenAddr(configs []*SiteConfig) (map[string][]*SiteConf
|
||||
// parts of an address. The component parts may be
|
||||
// updated to the correct values as setup proceeds,
|
||||
// but the original value should never be changed.
|
||||
//
|
||||
// The Host field must be in a normalized form.
|
||||
type Address struct {
|
||||
Original, Scheme, Host, Port, Path string
|
||||
}
|
||||
@@ -266,7 +419,7 @@ func (a Address) String() string {
|
||||
}
|
||||
scheme := a.Scheme
|
||||
if scheme == "" {
|
||||
if a.Port == "443" {
|
||||
if a.Port == strconv.Itoa(certmagic.HTTPSPort) {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
@@ -276,11 +429,12 @@ func (a Address) String() string {
|
||||
if s != "" {
|
||||
s += "://"
|
||||
}
|
||||
s += a.Host
|
||||
if a.Port != "" &&
|
||||
((scheme == "https" && a.Port != "443") ||
|
||||
(scheme == "http" && a.Port != "80")) {
|
||||
s += ":" + a.Port
|
||||
((scheme == "https" && a.Port != DefaultHTTPSPort) ||
|
||||
(scheme == "http" && a.Port != DefaultHTTPPort)) {
|
||||
s += net.JoinHostPort(a.Host, a.Port)
|
||||
} else {
|
||||
s += a.Host
|
||||
}
|
||||
if a.Path != "" {
|
||||
s += a.Path
|
||||
@@ -297,11 +451,66 @@ func (a Address) VHost() string {
|
||||
return a.Original
|
||||
}
|
||||
|
||||
// Normalize normalizes URL: turn scheme and host names into lower case
|
||||
func (a Address) Normalize() Address {
|
||||
path := a.Path
|
||||
if !CaseSensitivePath {
|
||||
path = strings.ToLower(path)
|
||||
}
|
||||
|
||||
// ensure host is normalized if it's an IP address
|
||||
host := a.Host
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
host = ip.String()
|
||||
}
|
||||
|
||||
return Address{
|
||||
Original: a.Original,
|
||||
Scheme: strings.ToLower(a.Scheme),
|
||||
Host: strings.ToLower(host),
|
||||
Port: a.Port,
|
||||
Path: path,
|
||||
}
|
||||
}
|
||||
|
||||
// Key is similar to String, just replaces scheme and host values with modified values.
|
||||
// Unlike String it doesn't add anything default (scheme, port, etc)
|
||||
func (a Address) Key() string {
|
||||
res := ""
|
||||
if a.Scheme != "" {
|
||||
res += a.Scheme + "://"
|
||||
}
|
||||
if a.Host != "" {
|
||||
res += a.Host
|
||||
}
|
||||
// insert port only if the original has its own explicit port
|
||||
if a.Port != "" && len(a.Original) >= len(res) &&
|
||||
strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
|
||||
res += ":" + a.Port
|
||||
}
|
||||
if a.Path != "" {
|
||||
res += a.Path
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
httpPort := strconv.Itoa(certmagic.HTTPPort)
|
||||
httpsPort := strconv.Itoa(certmagic.HTTPSPort)
|
||||
|
||||
// As of Go 1.12.8 (Aug 2019), ports that are service names such
|
||||
// as ":http" and ":https" are no longer parsed as they were
|
||||
// before, which is a breaking change for us. Attempt to smooth
|
||||
// this over for now by replacing those strings with their port
|
||||
// equivalents. See
|
||||
// https://github.com/golang/go/commit/3226f2d492963d361af9dfc6714ef141ba606713
|
||||
str = strings.Replace(str, ":https", ":"+httpsPort, 1)
|
||||
str = strings.Replace(str, ":http", ":"+httpPort, 1)
|
||||
|
||||
// Split input into components (prepend with // to assert host by default)
|
||||
if !strings.Contains(str, "//") && !strings.HasPrefix(str, "/") {
|
||||
str = "//" + str
|
||||
@@ -323,32 +532,28 @@ func standardizeAddress(str string) (Address, error) {
|
||||
// see if we can set port based off scheme
|
||||
if port == "" {
|
||||
if u.Scheme == "http" {
|
||||
port = "80"
|
||||
port = httpPort
|
||||
} else if u.Scheme == "https" {
|
||||
port = "443"
|
||||
port = httpsPort
|
||||
}
|
||||
}
|
||||
|
||||
// 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") {
|
||||
if (u.Scheme == "http" && port == httpsPort) || (u.Scheme == "https" && port == httpPort) {
|
||||
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"
|
||||
// (this behavior changed in Go 1.12.8)
|
||||
if u.Scheme == "" {
|
||||
if port == httpPort {
|
||||
u.Scheme = "http"
|
||||
} else if port == httpsPort {
|
||||
u.Scheme = "https"
|
||||
}
|
||||
}
|
||||
|
||||
return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err
|
||||
return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, nil
|
||||
}
|
||||
|
||||
// RegisterDevDirective splices name into the list of directives
|
||||
@@ -414,54 +619,81 @@ func RegisterDevDirective(name, before string) {
|
||||
var directives = []string{
|
||||
// primitive actions that set up the fundamental vitals of each config
|
||||
"root",
|
||||
"index",
|
||||
"bind",
|
||||
"maxrequestbody", // TODO: 'limits'
|
||||
"limits",
|
||||
"timeouts",
|
||||
"tls",
|
||||
|
||||
// services/utilities, or other directives that don't necessarily inject handlers
|
||||
"startup",
|
||||
"shutdown",
|
||||
"startup", // TODO: Deprecate this directive
|
||||
"shutdown", // TODO: Deprecate this directive
|
||||
"on",
|
||||
"supervisor", // github.com/lucaslorentz/caddy-supervisor
|
||||
"request_id",
|
||||
"realip", // github.com/captncraig/caddy-realip
|
||||
"git", // github.com/abiosoft/caddy-git
|
||||
|
||||
// directives that add listener middleware to the stack
|
||||
"proxyprotocol", // github.com/mastercactapus/caddy-proxyprotocol
|
||||
|
||||
// directives that add middleware to the stack
|
||||
"locale", // github.com/simia-tech/caddy-locale
|
||||
"log",
|
||||
"cache", // github.com/nicolasazrak/caddy-cache
|
||||
"rewrite",
|
||||
"ext",
|
||||
"minify", // github.com/hacdias/caddy-minify
|
||||
"gzip",
|
||||
"header",
|
||||
"geoip", // github.com/kodnaplakal/caddy-geoip
|
||||
"errors",
|
||||
"filter", // github.com/echocat/caddy-filter
|
||||
"minify", // github.com/hacdias/caddy-minify
|
||||
"ipfilter", // github.com/pyed/ipfilter
|
||||
"ratelimit", // github.com/xuqingfeng/caddy-rate-limit
|
||||
"search", // github.com/pedronasser/caddy-search
|
||||
"expires", // github.com/epicagency/caddy-expires
|
||||
"authz", // github.com/casbin/caddy-authz
|
||||
"filter", // github.com/echocat/caddy-filter
|
||||
"ipfilter", // github.com/pyed/ipfilter
|
||||
"ratelimit", // github.com/xuqingfeng/caddy-rate-limit
|
||||
"recaptcha", // github.com/defund/caddy-recaptcha
|
||||
"expires", // github.com/epicagency/caddy-expires
|
||||
"forwardproxy", // github.com/caddyserver/forwardproxy
|
||||
"basicauth",
|
||||
"redir",
|
||||
"status",
|
||||
"cors", // github.com/captncraig/cors/caddy
|
||||
"cors", // github.com/captncraig/cors/caddy
|
||||
"s3browser", // github.com/techknowlogick/caddy-s3browser
|
||||
"nobots", // github.com/Xumeiquer/nobots
|
||||
"mime",
|
||||
"jwt", // github.com/BTBurke/caddy-jwt
|
||||
"jsonp", // github.com/pschlump/caddy-jsonp
|
||||
"upload", // blitznote.com/src/caddy.upload
|
||||
"multipass", // github.com/namsral/multipass/caddy
|
||||
"login", // github.com/tarent/loginsrv/caddy
|
||||
"reauth", // github.com/freman/caddy-reauth
|
||||
"extauth", // github.com/BTBurke/caddy-extauth
|
||||
"jwt", // github.com/BTBurke/caddy-jwt
|
||||
"permission", // github.com/dhaavi/caddy-permission
|
||||
"jsonp", // github.com/pschlump/caddy-jsonp
|
||||
"upload", // blitznote.com/src/caddy.upload
|
||||
"multipass", // github.com/namsral/multipass/caddy
|
||||
"internal",
|
||||
"pprof",
|
||||
"expvar",
|
||||
"push",
|
||||
"datadog", // github.com/payintech/caddy-datadog
|
||||
"prometheus", // github.com/miekg/caddy-prometheus
|
||||
"proxy",
|
||||
"fastcgi",
|
||||
"websocket",
|
||||
"filemanager", // github.com/hacdias/caddy-filemanager
|
||||
"markdown",
|
||||
"templates",
|
||||
"proxy",
|
||||
"pubsub", // github.com/jung-kurt/caddy-pubsub
|
||||
"fastcgi",
|
||||
"cgi", // github.com/jung-kurt/caddy-cgi
|
||||
"websocket",
|
||||
"filebrowser", // github.com/filebrowser/caddy
|
||||
"webdav", // github.com/hacdias/caddy-webdav
|
||||
"markdown",
|
||||
"browse",
|
||||
"hugo", // github.com/hacdias/caddy-hugo
|
||||
"mailout", // github.com/SchumacherFM/mailout
|
||||
"awses", // github.com/miquella/caddy-awses
|
||||
"awslambda", // github.com/coopernurse/caddy-awslambda
|
||||
"grpc", // github.com/pieterlouw/caddy-grpc
|
||||
"gopkg", // github.com/zikes/gopkg
|
||||
"restic", // github.com/restic/caddy
|
||||
"wkd", // github.com/emersion/caddy-wkd
|
||||
"dyndns", // github.com/linkonoid/caddy-dyndns
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -471,6 +703,10 @@ const (
|
||||
DefaultPort = "2015"
|
||||
// DefaultRoot is the default root folder.
|
||||
DefaultRoot = "."
|
||||
// DefaultHTTPPort is the default port for HTTP.
|
||||
DefaultHTTPPort = "80"
|
||||
// DefaultHTTPSPort is the default port for HTTPS.
|
||||
DefaultHTTPSPort = "443"
|
||||
)
|
||||
|
||||
// These "soft defaults" are configurable by
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
"sort"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyfile"
|
||||
)
|
||||
|
||||
func TestStandardizeAddress(t *testing.T) {
|
||||
@@ -25,12 +43,12 @@ func TestStandardizeAddress(t *testing.T) {
|
||||
{`:`, "", "", "", "", 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`, "http", "", "80", "", false}, // as of Go 1.12.8, service name in port is no longer supported
|
||||
{`:https`, "https", "", "443", "", false}, // as of Go 1.12.8, service name in port is no longer supported
|
||||
{`http://localhost:https`, "", "", "", "", true}, // conflict
|
||||
{`http://localhost:http`, "http", "localhost", "80", "", false}, // repeated scheme -- test adjusted for Go 1.12.8 (expect no error)
|
||||
{`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},
|
||||
@@ -40,8 +58,8 @@ func TestStandardizeAddress(t *testing.T) {
|
||||
{`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},
|
||||
{`::1`, "", "::1", "", "", false}, // test adjusted for Go 1.12.8 (expect no error)
|
||||
{`localhost::`, "", "localhost::", "", "", false}, // test adjusted for Go 1.12.8 (expect no error)
|
||||
{`#$%@`, "", "", "", "", true},
|
||||
{`host/path`, "", "host", "", "/path", false},
|
||||
{`http://host/`, "http", "host", "80", "/", false},
|
||||
@@ -49,7 +67,7 @@ func TestStandardizeAddress(t *testing.T) {
|
||||
{`: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:80/path`, "http", "host", "80", "/path", false}, // test adjusted for Go 1.12.8 (expect "http" scheme)
|
||||
{`host:https/path`, "https", "host", "443", "/path", false},
|
||||
{`/path`, "", "", "", "/path", false},
|
||||
} {
|
||||
@@ -123,7 +141,7 @@ func TestAddressString(t *testing.T) {
|
||||
func TestInspectServerBlocksWithCustomDefaultPort(t *testing.T) {
|
||||
Port = "9999"
|
||||
filename := "Testfile"
|
||||
ctx := newContext().(*httpContext)
|
||||
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
|
||||
input := strings.NewReader(`localhost`)
|
||||
sblocks, err := caddyfile.Parse(filename, input, nil)
|
||||
if err != nil {
|
||||
@@ -133,15 +151,45 @@ func TestInspectServerBlocksWithCustomDefaultPort(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Didn't expect an error, but got: %v", err)
|
||||
}
|
||||
addr := ctx.keysToSiteConfigs["localhost"].Addr
|
||||
localhostKey := "localhost"
|
||||
item, ok := ctx.keysToSiteConfigs[localhostKey]
|
||||
if !ok {
|
||||
availableKeys := make(sort.StringSlice, len(ctx.keysToSiteConfigs))
|
||||
i := 0
|
||||
for key := range ctx.keysToSiteConfigs {
|
||||
availableKeys[i] = fmt.Sprintf("'%s'", key)
|
||||
i++
|
||||
}
|
||||
availableKeys.Sort()
|
||||
t.Errorf("`%s` not found within registered keys, only these are available: %s", localhostKey, strings.Join(availableKeys, ", "))
|
||||
return
|
||||
}
|
||||
addr := item.Addr
|
||||
if addr.Port != Port {
|
||||
t.Errorf("Expected the port on the address to be set, but got: %#v", addr)
|
||||
}
|
||||
}
|
||||
|
||||
// See discussion on PR #2015
|
||||
func TestInspectServerBlocksWithAdjustedAddress(t *testing.T) {
|
||||
Port = DefaultPort
|
||||
Host = "example.com"
|
||||
filename := "Testfile"
|
||||
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
|
||||
input := strings.NewReader("example.com {\n}\n:2015 {\n}")
|
||||
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("Expected an error because site definitions should overlap, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectServerBlocksCaseInsensitiveKey(t *testing.T) {
|
||||
filename := "Testfile"
|
||||
ctx := newContext().(*httpContext)
|
||||
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
|
||||
input := strings.NewReader("localhost {\n}\nLOCALHOST {\n}")
|
||||
sblocks, err := caddyfile.Parse(filename, input, nil)
|
||||
if err != nil {
|
||||
@@ -153,6 +201,80 @@ func TestInspectServerBlocksCaseInsensitiveKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyNormalization(t *testing.T) {
|
||||
originalCaseSensitivePath := CaseSensitivePath
|
||||
defer func() {
|
||||
CaseSensitivePath = originalCaseSensitivePath
|
||||
}()
|
||||
CaseSensitivePath = true
|
||||
|
||||
caseSensitiveData := []struct {
|
||||
orig string
|
||||
res string
|
||||
}{
|
||||
{
|
||||
orig: "http://host:1234/path",
|
||||
res: "http://host:1234/path",
|
||||
},
|
||||
{
|
||||
orig: "HTTP://A/ABCDEF",
|
||||
res: "http://a/ABCDEF",
|
||||
},
|
||||
{
|
||||
orig: "A/ABCDEF",
|
||||
res: "a/ABCDEF",
|
||||
},
|
||||
{
|
||||
orig: "A:2015/Path",
|
||||
res: "a:2015/Path",
|
||||
},
|
||||
{
|
||||
orig: ":80",
|
||||
res: "http://",
|
||||
},
|
||||
{
|
||||
orig: ":443",
|
||||
res: "https://",
|
||||
},
|
||||
{
|
||||
orig: ":1234",
|
||||
res: ":1234",
|
||||
},
|
||||
}
|
||||
for _, item := range caseSensitiveData {
|
||||
v := normalizedKey(item.orig)
|
||||
if v != item.res {
|
||||
t.Errorf("Normalization of `%s` with CaseSensitivePath option set to true must be equal to `%s`, got `%s` instead", item.orig, item.res, v)
|
||||
}
|
||||
}
|
||||
|
||||
CaseSensitivePath = false
|
||||
caseInsensitiveData := []struct {
|
||||
orig string
|
||||
res string
|
||||
}{
|
||||
{
|
||||
orig: "HTTP://A/ABCDEF",
|
||||
res: "http://a/abcdef",
|
||||
},
|
||||
{
|
||||
orig: "A/ABCDEF",
|
||||
res: "a/abcdef",
|
||||
},
|
||||
{
|
||||
orig: "A:2015/Port",
|
||||
res: "a:2015/port",
|
||||
},
|
||||
}
|
||||
for _, item := range caseInsensitiveData {
|
||||
v := normalizedKey(item.orig)
|
||||
if v != item.res {
|
||||
t.Errorf("Normalization of `%s` with CaseSensitivePath option set to false must be equal to `%s`, got `%s` instead", item.orig, item.res, v)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetConfig(t *testing.T) {
|
||||
// case insensitivity for key
|
||||
con := caddy.NewTestController("http", "")
|
||||
@@ -170,6 +292,14 @@ func TestGetConfig(t *testing.T) {
|
||||
if cfg == cfg3 {
|
||||
t.Errorf("Expected different configs using when key is different; got %p and %p", cfg, cfg3)
|
||||
}
|
||||
|
||||
con.Key = "foo/foobar"
|
||||
cfg4 := GetConfig(con)
|
||||
con.Key = "foo/Foobar"
|
||||
cfg5 := GetConfig(con)
|
||||
if cfg4 == cfg5 {
|
||||
t.Errorf("Expected different cases in path to differentiate keys in general")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectivesList(t *testing.T) {
|
||||
@@ -193,7 +323,7 @@ func TestDirectivesList(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContextSaveConfig(t *testing.T) {
|
||||
ctx := newContext().(*httpContext)
|
||||
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
|
||||
ctx.saveConfig("foo", new(SiteConfig))
|
||||
if _, ok := ctx.keysToSiteConfigs["foo"]; !ok {
|
||||
t.Error("Expected config to be saved, but it wasn't")
|
||||
@@ -209,3 +339,27 @@ func TestContextSaveConfig(t *testing.T) {
|
||||
t.Errorf("Expected len(siteConfigs) == %d, but was %d", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Test to make sure we are correctly hiding the Caddyfile
|
||||
func TestHideCaddyfile(t *testing.T) {
|
||||
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
|
||||
ctx.saveConfig("test", &SiteConfig{
|
||||
Root: Root,
|
||||
originCaddyfile: "Testfile",
|
||||
})
|
||||
err := hideCaddyfile(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to hide Caddyfile, got: %v", err)
|
||||
return
|
||||
}
|
||||
if len(ctx.siteConfigs[0].HiddenFiles) == 0 {
|
||||
t.Fatal("Failed to add Caddyfile to HiddenFiles.")
|
||||
return
|
||||
}
|
||||
for _, file := range ctx.siteConfigs[0].HiddenFiles {
|
||||
if file == "/Testfile" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("Caddyfile missing from HiddenFiles")
|
||||
}
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -20,24 +35,22 @@ import (
|
||||
//
|
||||
// Beware when accessing the Replacer value; it may be nil!
|
||||
type ResponseRecorder struct {
|
||||
http.ResponseWriter
|
||||
*ResponseWriterWrapper
|
||||
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.
|
||||
// NewResponseRecorder makes and returns a new ResponseRecorder.
|
||||
// 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(),
|
||||
ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w},
|
||||
status: http.StatusOK,
|
||||
start: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,53 +58,204 @@ func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder {
|
||||
// underlying ResponseWriter's WriteHeader method.
|
||||
func (r *ResponseRecorder) WriteHeader(status int) {
|
||||
r.status = status
|
||||
r.ResponseWriter.WriteHeader(status)
|
||||
r.ResponseWriterWrapper.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)
|
||||
n, err := r.ResponseWriterWrapper.Write(buf)
|
||||
if err == nil {
|
||||
r.size += n
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Size is a Getter to size property
|
||||
// Size returns the size of the recorded response body.
|
||||
func (r *ResponseRecorder) Size() int {
|
||||
return r.size
|
||||
}
|
||||
|
||||
// Status is a Getter to status property
|
||||
// Status returns the recorded response status code.
|
||||
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, NonHijackerError{Underlying: r.ResponseWriter}
|
||||
// ResponseBuffer is a type that conditionally buffers the
|
||||
// response in memory. It implements http.ResponseWriter so
|
||||
// that it can stream the response if it is not buffering.
|
||||
// Whether it buffers is decided by a func passed into the
|
||||
// constructor, NewResponseBuffer.
|
||||
//
|
||||
// This type implements http.ResponseWriter, so you can pass
|
||||
// this to the Next() middleware in the chain and record its
|
||||
// response. However, since the entire response body will be
|
||||
// buffered in memory, only use this when explicitly configured
|
||||
// and required for some specific reason. For example, the
|
||||
// text/template package only parses templates out of []byte
|
||||
// and not io.Reader, so the templates directive uses this
|
||||
// type to obtain the entire template text, but only on certain
|
||||
// requests that match the right Content-Type, etc.
|
||||
//
|
||||
// ResponseBuffer also implements io.ReaderFrom for performance
|
||||
// reasons. The standard lib's http.response type (unexported)
|
||||
// uses io.Copy to write the body. io.Copy makes an allocation
|
||||
// if the destination does not have a ReadFrom method (or if
|
||||
// the source does not have a WriteTo method, but that's
|
||||
// irrelevant here). Our ReadFrom is smart: if buffering, it
|
||||
// calls the buffer's ReadFrom, which makes no allocs because
|
||||
// it is already a buffer! If we're streaming the response
|
||||
// instead, ReadFrom uses io.CopyBuffer with a pooled buffer
|
||||
// that is managed within this package.
|
||||
type ResponseBuffer struct {
|
||||
*ResponseWriterWrapper
|
||||
Buffer *bytes.Buffer
|
||||
header http.Header
|
||||
status int
|
||||
shouldBuffer func(status int, header http.Header) bool
|
||||
stream bool
|
||||
rw http.ResponseWriter
|
||||
wroteHeader bool
|
||||
}
|
||||
|
||||
// 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(NonFlusherError{Underlying: r.ResponseWriter}) // should be recovered at the beginning of middleware stack
|
||||
// NewResponseBuffer returns a new ResponseBuffer that will
|
||||
// use buf to store the full body of the response if shouldBuffer
|
||||
// returns true. If shouldBuffer returns false, then the response
|
||||
// body will be streamed directly to rw.
|
||||
//
|
||||
// shouldBuffer will be passed the status code and header fields of
|
||||
// the response. With that information, the function should decide
|
||||
// whether to buffer the response in memory. For example: the templates
|
||||
// directive uses this to determine whether the response is the
|
||||
// right Content-Type (according to user config) for a template.
|
||||
//
|
||||
// For performance, the buf you pass in should probably be obtained
|
||||
// from a sync.Pool in order to reuse allocated space.
|
||||
func NewResponseBuffer(buf *bytes.Buffer, rw http.ResponseWriter,
|
||||
shouldBuffer func(status int, header http.Header) bool) *ResponseBuffer {
|
||||
rb := &ResponseBuffer{
|
||||
Buffer: buf,
|
||||
header: make(http.Header),
|
||||
status: http.StatusOK, // default status code
|
||||
shouldBuffer: shouldBuffer,
|
||||
rw: rw,
|
||||
}
|
||||
rb.ResponseWriterWrapper = &ResponseWriterWrapper{ResponseWriter: rw}
|
||||
return rb
|
||||
}
|
||||
|
||||
// Header returns the response header map.
|
||||
func (rb *ResponseBuffer) Header() http.Header {
|
||||
return rb.header
|
||||
}
|
||||
|
||||
// WriteHeader calls shouldBuffer to decide whether the
|
||||
// upcoming body should be buffered, and then writes
|
||||
// the header to the response.
|
||||
func (rb *ResponseBuffer) WriteHeader(status int) {
|
||||
if rb.wroteHeader {
|
||||
return
|
||||
}
|
||||
rb.wroteHeader = true
|
||||
|
||||
rb.status = status
|
||||
rb.stream = !rb.shouldBuffer(status, rb.header)
|
||||
if rb.stream {
|
||||
rb.CopyHeader()
|
||||
rb.ResponseWriterWrapper.WriteHeader(status)
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
// Write writes buf to rb.Buffer if buffering, otherwise
|
||||
// to the ResponseWriter directly if streaming.
|
||||
func (rb *ResponseBuffer) Write(buf []byte) (int, error) {
|
||||
if !rb.wroteHeader {
|
||||
rb.WriteHeader(http.StatusOK)
|
||||
}
|
||||
panic(NonCloseNotifierError{Underlying: r.ResponseWriter})
|
||||
|
||||
if rb.stream {
|
||||
return rb.ResponseWriterWrapper.Write(buf)
|
||||
}
|
||||
return rb.Buffer.Write(buf)
|
||||
}
|
||||
|
||||
// Buffered returns whether rb has decided to buffer the response.
|
||||
func (rb *ResponseBuffer) Buffered() bool {
|
||||
return !rb.stream
|
||||
}
|
||||
|
||||
// CopyHeader copies the buffered header in rb to the ResponseWriter,
|
||||
// but it does not write the header out.
|
||||
func (rb *ResponseBuffer) CopyHeader() {
|
||||
for field, val := range rb.header {
|
||||
rb.ResponseWriterWrapper.Header()[field] = val
|
||||
}
|
||||
}
|
||||
|
||||
// ReadFrom avoids allocations when writing to the buffer (if buffering),
|
||||
// and reduces allocations when writing to the ResponseWriter directly
|
||||
// (if streaming).
|
||||
//
|
||||
// In local testing with the templates directive, req/sec were improved
|
||||
// from ~8,200 to ~9,600 on templated files by ensuring that this type
|
||||
// implements io.ReaderFrom.
|
||||
func (rb *ResponseBuffer) ReadFrom(src io.Reader) (int64, error) {
|
||||
if !rb.wroteHeader {
|
||||
rb.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
if rb.stream {
|
||||
// first see if we can avoid any allocations at all
|
||||
if wt, ok := src.(io.WriterTo); ok {
|
||||
return wt.WriteTo(rb.ResponseWriterWrapper)
|
||||
}
|
||||
// if not, use a pooled copy buffer to reduce allocs
|
||||
// (this improved req/sec from ~25,300 to ~27,000 on
|
||||
// static files served directly with the fileserver,
|
||||
// but results fluctuated a little on each run).
|
||||
// a note of caution:
|
||||
// https://go-review.googlesource.com/c/22134#message-ff351762308fe05f6b72a487d6842e3988916486
|
||||
buf := respBufPool.Get().([]byte)
|
||||
n, err := io.CopyBuffer(rb.ResponseWriterWrapper, src, buf)
|
||||
respBufPool.Put(buf) // deferring this slowed down benchmarks a smidgin, I think
|
||||
return n, err
|
||||
}
|
||||
return rb.Buffer.ReadFrom(src)
|
||||
}
|
||||
|
||||
// StatusCodeWriter returns an http.ResponseWriter that always
|
||||
// writes the status code stored in rb from when a response
|
||||
// was buffered to it.
|
||||
func (rb *ResponseBuffer) StatusCodeWriter(w http.ResponseWriter) http.ResponseWriter {
|
||||
return forcedStatusCodeWriter{w, rb}
|
||||
}
|
||||
|
||||
// forcedStatusCodeWriter is used to force a status code when
|
||||
// writing the header. It uses the status code saved on rb.
|
||||
// This is useful if passing a http.ResponseWriter into
|
||||
// http.ServeContent because ServeContent hard-codes 2xx status
|
||||
// codes. If we buffered the response, we force that status code
|
||||
// instead.
|
||||
type forcedStatusCodeWriter struct {
|
||||
http.ResponseWriter
|
||||
rb *ResponseBuffer
|
||||
}
|
||||
|
||||
func (fscw forcedStatusCodeWriter) WriteHeader(int) {
|
||||
fscw.ResponseWriter.WriteHeader(fscw.rb.status)
|
||||
}
|
||||
|
||||
// respBufPool is used for io.CopyBuffer when ResponseBuffer
|
||||
// is configured to stream a response.
|
||||
var respBufPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, 32*1024)
|
||||
},
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ HTTPInterfaces = (*ResponseRecorder)(nil)
|
||||
_ HTTPInterfaces = (*ResponseBuffer)(nil)
|
||||
_ io.ReaderFrom = (*ResponseBuffer)(nil)
|
||||
)
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -30,7 +44,7 @@ func TestWrite(t *testing.T) {
|
||||
responseTestString := "test"
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
buf := []byte(responseTestString)
|
||||
recordRequest.Write(buf)
|
||||
_, _ = recordRequest.Write(buf)
|
||||
if recordRequest.size != len(buf) {
|
||||
t.Fatalf("Expected the bytes written counter to be %d, but instead found %d\n", len(buf), recordRequest.size)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -13,6 +31,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
)
|
||||
|
||||
// requestReplacer is a strings.Replacer which is used to
|
||||
@@ -86,20 +107,30 @@ func (lw *limitWriter) String() string {
|
||||
// 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 {
|
||||
rb := newLimitWriter(MaxLogBodySize)
|
||||
if r.Body != nil {
|
||||
r.Body = struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{io.TeeReader(r.Body, rb), io.Closer(r.Body)}
|
||||
repl := &replacer{
|
||||
request: r,
|
||||
responseRecorder: rr,
|
||||
emptyValue: emptyValue,
|
||||
}
|
||||
return &replacer{
|
||||
request: r,
|
||||
requestBody: rb,
|
||||
responseRecorder: rr,
|
||||
customReplacements: make(map[string]string),
|
||||
emptyValue: emptyValue,
|
||||
|
||||
// extract customReplacements from a request replacer when present.
|
||||
if existing, ok := r.Context().Value(ReplacerCtxKey).(*replacer); ok {
|
||||
repl.requestBody = existing.requestBody
|
||||
repl.customReplacements = existing.customReplacements
|
||||
} else {
|
||||
// if there is no existing replacer, build one from scratch.
|
||||
rb := newLimitWriter(MaxLogBodySize)
|
||||
if r.Body != nil {
|
||||
r.Body = struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{io.TeeReader(r.Body, rb), io.Closer(r.Body)}
|
||||
}
|
||||
repl.requestBody = rb
|
||||
repl.customReplacements = make(map[string]string)
|
||||
}
|
||||
|
||||
return repl
|
||||
}
|
||||
|
||||
func canLogRequest(r *http.Request) bool {
|
||||
@@ -114,6 +145,14 @@ func canLogRequest(r *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// unescapeBraces finds escaped braces in s and returns
|
||||
// a string with those braces unescaped.
|
||||
func unescapeBraces(s string) string {
|
||||
s = strings.Replace(s, "\\{", "{", -1)
|
||||
s = strings.Replace(s, "\\}", "}", -1)
|
||||
return s
|
||||
}
|
||||
|
||||
// Replace performs a replacement of values on s and returns
|
||||
// the string with the replaced values.
|
||||
func (r *replacer) Replace(s string) string {
|
||||
@@ -123,32 +162,59 @@ func (r *replacer) Replace(s string) string {
|
||||
}
|
||||
|
||||
result := ""
|
||||
Placeholders: // process each placeholder in sequence
|
||||
for {
|
||||
idxStart := strings.Index(s, "{")
|
||||
if idxStart == -1 {
|
||||
// no placeholder anymore
|
||||
break
|
||||
}
|
||||
idxEnd := strings.Index(s[idxStart:], "}")
|
||||
if idxEnd == -1 {
|
||||
// unpaired placeholder
|
||||
break
|
||||
}
|
||||
idxEnd += idxStart
|
||||
var idxStart, idxEnd int
|
||||
|
||||
// get a replacement
|
||||
placeholder := s[idxStart : idxEnd+1]
|
||||
idxOffset := 0
|
||||
for { // find first unescaped opening brace
|
||||
searchSpace := s[idxOffset:]
|
||||
idxStart = strings.Index(searchSpace, "{")
|
||||
if idxStart == -1 {
|
||||
// no more placeholders
|
||||
break Placeholders
|
||||
}
|
||||
if idxStart == 0 || searchSpace[idxStart-1] != '\\' {
|
||||
// preceding character is not an escape
|
||||
idxStart += idxOffset
|
||||
break
|
||||
}
|
||||
// the brace we found was escaped
|
||||
// search the rest of the string next
|
||||
idxOffset += idxStart + 1
|
||||
}
|
||||
|
||||
idxOffset = 0
|
||||
for { // find first unescaped closing brace
|
||||
searchSpace := s[idxStart+idxOffset:]
|
||||
idxEnd = strings.Index(searchSpace, "}")
|
||||
if idxEnd == -1 {
|
||||
// unpaired placeholder
|
||||
break Placeholders
|
||||
}
|
||||
if idxEnd == 0 || searchSpace[idxEnd-1] != '\\' {
|
||||
// preceding character is not an escape
|
||||
idxEnd += idxOffset + idxStart
|
||||
break
|
||||
}
|
||||
// the brace we found was escaped
|
||||
// search the rest of the string next
|
||||
idxOffset += idxEnd + 1
|
||||
}
|
||||
|
||||
// get a replacement for the unescaped placeholder
|
||||
placeholder := unescapeBraces(s[idxStart : idxEnd+1])
|
||||
replacement := r.getSubstitution(placeholder)
|
||||
|
||||
// append prefix + replacement
|
||||
result += s[:idxStart] + replacement
|
||||
// append unescaped prefix + replacement
|
||||
result += strings.TrimPrefix(unescapeBraces(s[:idxStart]), "\\") + replacement
|
||||
|
||||
// strip out scanned parts
|
||||
s = s[idxEnd+1:]
|
||||
}
|
||||
|
||||
// append unscanned parts
|
||||
return result + s
|
||||
return result + unescapeBraces(s)
|
||||
}
|
||||
|
||||
func roundDuration(d time.Duration) time.Duration {
|
||||
@@ -181,6 +247,15 @@ func round(d, r time.Duration) time.Duration {
|
||||
return d
|
||||
}
|
||||
|
||||
// getPeerCert returns peer certificate
|
||||
func (r *replacer) getPeerCert() *x509.Certificate {
|
||||
if r.request.TLS != nil && len(r.request.TLS.PeerCertificates) > 0 {
|
||||
return r.request.TLS.PeerCertificates[0]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSubstitution retrieves value from corresponding key
|
||||
func (r *replacer) getSubstitution(key string) string {
|
||||
// search custom replacements first
|
||||
@@ -198,6 +273,29 @@ func (r *replacer) getSubstitution(key string) string {
|
||||
}
|
||||
}
|
||||
}
|
||||
// search response headers then
|
||||
if r.responseRecorder != nil && key[1] == '<' {
|
||||
want := key[2 : len(key)-1]
|
||||
for key, values := range r.responseRecorder.Header() {
|
||||
// Header placeholders (case-insensitive)
|
||||
if strings.EqualFold(key, want) {
|
||||
return strings.Join(values, ",")
|
||||
}
|
||||
}
|
||||
}
|
||||
// next check for cookies
|
||||
if key[1] == '~' {
|
||||
name := key[2 : len(key)-1]
|
||||
if cookie, err := r.request.Cookie(name); err == nil {
|
||||
return cookie.Value
|
||||
}
|
||||
}
|
||||
// next check for query argument
|
||||
if key[1] == '?' {
|
||||
query := r.request.URL.Query()
|
||||
name := key[2 : len(key)-1]
|
||||
return query.Get(name)
|
||||
}
|
||||
|
||||
// search default replacements in the end
|
||||
switch key {
|
||||
@@ -223,29 +321,27 @@ func (r *replacer) getSubstitution(key string) string {
|
||||
}
|
||||
return host
|
||||
case "{path}":
|
||||
// if a rewrite has happened, the original URI should be used as the path
|
||||
// rather than the rewritten URI
|
||||
path := r.request.Header.Get("Caddy-Rewrite-Original-URI")
|
||||
if path == "" {
|
||||
path = r.request.URL.Path
|
||||
}
|
||||
return path
|
||||
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
|
||||
return u.Path
|
||||
case "{path_escaped}":
|
||||
path := r.request.Header.Get("Caddy-Rewrite-Original-URI")
|
||||
if path == "" {
|
||||
path = r.request.URL.Path
|
||||
}
|
||||
return url.QueryEscape(path)
|
||||
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
|
||||
return url.QueryEscape(u.Path)
|
||||
case "{request_id}":
|
||||
reqid, _ := r.request.Context().Value(RequestIDCtxKey).(string)
|
||||
return reqid
|
||||
case "{rewrite_path}":
|
||||
return r.request.URL.Path
|
||||
case "{rewrite_path_escaped}":
|
||||
return url.QueryEscape(r.request.URL.Path)
|
||||
case "{query}":
|
||||
return r.request.URL.RawQuery
|
||||
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
|
||||
return u.RawQuery
|
||||
case "{query_escaped}":
|
||||
return url.QueryEscape(r.request.URL.RawQuery)
|
||||
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
|
||||
return url.QueryEscape(u.RawQuery)
|
||||
case "{fragment}":
|
||||
return r.request.URL.Fragment
|
||||
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
|
||||
return u.Fragment
|
||||
case "{proto}":
|
||||
return r.request.Proto
|
||||
case "{remote}":
|
||||
@@ -261,13 +357,25 @@ func (r *replacer) getSubstitution(key string) string {
|
||||
}
|
||||
return port
|
||||
case "{uri}":
|
||||
return r.request.URL.RequestURI()
|
||||
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
|
||||
return u.RequestURI()
|
||||
case "{uri_escaped}":
|
||||
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
|
||||
return url.QueryEscape(u.RequestURI())
|
||||
case "{rewrite_uri}":
|
||||
return r.request.URL.RequestURI()
|
||||
case "{rewrite_uri_escaped}":
|
||||
return url.QueryEscape(r.request.URL.RequestURI())
|
||||
case "{when}":
|
||||
return now().Format(timeFormat)
|
||||
case "{when_iso_local}":
|
||||
return now().Format(timeFormatISO)
|
||||
case "{when_iso}":
|
||||
return now().UTC().Format(timeFormatISOUTC)
|
||||
case "{when_unix}":
|
||||
return strconv.FormatInt(now().Unix(), 10)
|
||||
case "{when_unix_ms}":
|
||||
return strconv.FormatInt(nanoToMilliseconds(now().UnixNano()), 10)
|
||||
case "{file}":
|
||||
_, file := path.Split(r.request.URL.Path)
|
||||
return file
|
||||
@@ -286,11 +394,19 @@ func (r *replacer) getSubstitution(key string) string {
|
||||
}
|
||||
_, err := ioutil.ReadAll(r.request.Body)
|
||||
if err != nil {
|
||||
if _, ok := err.(MaxBytesExceeded); ok {
|
||||
if err == ErrMaxBytesExceeded {
|
||||
return r.emptyValue
|
||||
}
|
||||
}
|
||||
return requestReplacer.Replace(r.requestBody.String())
|
||||
case "{mitm}":
|
||||
if val, ok := r.request.Context().Value(caddy.CtxKey("mitm")).(bool); ok {
|
||||
if val {
|
||||
return "likely"
|
||||
}
|
||||
return "unlikely"
|
||||
}
|
||||
return "unknown"
|
||||
case "{status}":
|
||||
if r.responseRecorder == nil {
|
||||
return r.emptyValue
|
||||
@@ -312,14 +428,120 @@ func (r *replacer) getSubstitution(key string) string {
|
||||
}
|
||||
elapsedDuration := time.Since(r.responseRecorder.start)
|
||||
return strconv.FormatInt(convertToMilliseconds(elapsedDuration), 10)
|
||||
case "{tls_protocol}":
|
||||
if r.request.TLS != nil {
|
||||
if name, err := caddytls.GetSupportedProtocolName(r.request.TLS.Version); err == nil {
|
||||
return name
|
||||
} else {
|
||||
return "tls" // this should never happen, but guard in case
|
||||
}
|
||||
}
|
||||
return r.emptyValue // because not using a secure channel
|
||||
case "{tls_cipher}":
|
||||
if r.request.TLS != nil {
|
||||
if name, err := caddytls.GetSupportedCipherName(r.request.TLS.CipherSuite); err == nil {
|
||||
return name
|
||||
} else {
|
||||
return "UNKNOWN" // this should never happen, but guard in case
|
||||
}
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_escaped_cert}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
pemBlock := pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
return url.QueryEscape(string(pem.EncodeToMemory(&pemBlock)))
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_fingerprint}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return fmt.Sprintf("%x", sha256.Sum256(cert.Raw))
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_i_dn}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return cert.Issuer.String()
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_raw_cert}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return string(cert.Raw)
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_s_dn}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return cert.Subject.String()
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_serial}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return fmt.Sprintf("%x", cert.SerialNumber)
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_v_end}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return cert.NotAfter.In(time.UTC).Format("Jan 02 15:04:05 2006 MST")
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_v_remain}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
now := time.Now().In(time.UTC)
|
||||
days := int64(cert.NotAfter.Sub(now).Seconds() / 86400)
|
||||
return strconv.FormatInt(days, 10)
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_v_start}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return cert.NotBefore.Format("Jan 02 15:04:05 2006 MST")
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{server_port}":
|
||||
_, port, err := net.SplitHostPort(r.request.Host)
|
||||
if err != nil {
|
||||
if r.request.TLS != nil {
|
||||
return "443"
|
||||
} else {
|
||||
return "80"
|
||||
}
|
||||
}
|
||||
return port
|
||||
default:
|
||||
// {labelN}
|
||||
if strings.HasPrefix(key, "{label") {
|
||||
nStr := key[6 : len(key)-1] // get the integer N in "{labelN}"
|
||||
n, err := strconv.Atoi(nStr)
|
||||
if err != nil || n < 1 {
|
||||
return r.emptyValue
|
||||
}
|
||||
labels := strings.Split(r.request.Host, ".")
|
||||
if n > len(labels) {
|
||||
return r.emptyValue
|
||||
}
|
||||
return labels[n-1]
|
||||
}
|
||||
}
|
||||
|
||||
return r.emptyValue
|
||||
}
|
||||
|
||||
//convertToMilliseconds returns the number of milliseconds in the given duration
|
||||
func nanoToMilliseconds(d int64) int64 {
|
||||
return d / 1e6
|
||||
}
|
||||
|
||||
// convertToMilliseconds returns the number of milliseconds in the given duration
|
||||
func convertToMilliseconds(d time.Duration) int64 {
|
||||
return d.Nanoseconds() / 1e6
|
||||
return nanoToMilliseconds(d.Nanoseconds())
|
||||
}
|
||||
|
||||
// Set sets key to value in the r.customReplacements map.
|
||||
@@ -329,6 +551,7 @@ func (r *replacer) Set(key, value string) {
|
||||
|
||||
const (
|
||||
timeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||
timeFormatISO = "2006-01-02T15:04:05" // ISO 8601 with timezone to be assumed as local
|
||||
timeFormatISOUTC = "2006-01-02T15:04:05Z" // ISO 8601 with timezone to be assumed as UTC
|
||||
headerContentType = "Content-Type"
|
||||
contentTypeJSON = "application/json"
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
)
|
||||
|
||||
func TestNewReplacer(t *testing.T) {
|
||||
@@ -38,24 +62,32 @@ func TestReplace(t *testing.T) {
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost", reader)
|
||||
request, err := http.NewRequest("POST", "http://localhost.local/?foo=bar", reader)
|
||||
if err != nil {
|
||||
t.Fatal("Request Formation Failed\n")
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
|
||||
request = request.WithContext(ctx)
|
||||
|
||||
request.Header.Set("Custom", "foobarbaz")
|
||||
request.Header.Set("ShorterVal", "1")
|
||||
repl := NewReplacer(request, recordRequest, "-")
|
||||
// add some headers after creating replacer
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
// add some response headers
|
||||
recordRequest.Header().Set("Custom", "CustomResponseHeader")
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to determine hostname\n")
|
||||
t.Fatalf("Failed to determine hostname: %v", err)
|
||||
}
|
||||
|
||||
old := now
|
||||
now = func() time.Time {
|
||||
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
|
||||
// Note that the `-7` is seconds, not hours.
|
||||
return time.Date(2006, 1, 2, 15, 4, 5, 99999999, time.FixedZone("hardcoded", -7))
|
||||
}
|
||||
defer func() {
|
||||
now = old
|
||||
@@ -65,19 +97,37 @@ func TestReplace(t *testing.T) {
|
||||
expect string
|
||||
}{
|
||||
{"This hostname is {hostname}", "This hostname is " + hostname},
|
||||
{"This host is {host}.", "This host is localhost."},
|
||||
{"This host is {host}.", "This host is localhost.local."},
|
||||
{"This request method is {method}.", "This request method is POST."},
|
||||
{"The response status is {status}.", "The response status is 200."},
|
||||
{"{when}", "02/Jan/2006:15:04:05 +0000"},
|
||||
{"{when_iso}", "2006-01-02T15:04:12Z"},
|
||||
{"{when_iso_local}", "2006-01-02T15:04:05"},
|
||||
{"{when_unix}", "1136214252"},
|
||||
{"{when_unix_ms}", "1136214252099"},
|
||||
{"The Custom header is {>Custom}.", "The Custom header is foobarbaz."},
|
||||
{"The CustomAdd header is {>CustomAdd}.", "The CustomAdd header is caddy."},
|
||||
{"The request is {request}.", "The request is POST / HTTP/1.1\\r\\nHost: localhost\\r\\nCustom: foobarbaz\\r\\nCustomadd: caddy\\r\\nShorterval: 1\\r\\n\\r\\n."},
|
||||
{"The Custom response header is {<Custom}.", "The Custom response header is CustomResponseHeader."},
|
||||
{"Bad {>Custom placeholder", "Bad {>Custom placeholder"},
|
||||
{"The request is {request}.", "The request is POST /?foo=bar HTTP/1.1\\r\\nHost: localhost.local\\r\\n" +
|
||||
"Cookie: foo=bar; taste=delicious\\r\\nCustom: foobarbaz\\r\\nCustomadd: caddy\\r\\n" +
|
||||
"Shorterval: 1\\r\\n\\r\\n."},
|
||||
{"The cUsToM header is {>cUsToM}...", "The cUsToM header is foobarbaz..."},
|
||||
{"The cUsToM response header is {<CuSTom}.", "The cUsToM response header is CustomResponseHeader."},
|
||||
{"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 -"},
|
||||
{"Bad {}", "Bad -"},
|
||||
{"Cookies are {~taste}", "Cookies are delicious"},
|
||||
{"Missing cookie is {~missing}", "Missing cookie is -"},
|
||||
{"Query string is {query}", "Query string is foo=bar"},
|
||||
{"Query string value for foo is {?foo}", "Query string value for foo is bar"},
|
||||
{"Missing query string argument is {?missing}", "Missing query string argument is "},
|
||||
{"{label1} {label2} {label3} {label4}", "localhost local - -"},
|
||||
{"Label with missing number is {label} or {labelQQ}", "Label with missing number is - or -"},
|
||||
{"\\{ 'hostname': '{hostname}' \\}", "{ 'hostname': '" + hostname + "' }"},
|
||||
{"{server_port}", "80"},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
@@ -110,6 +160,234 @@ func TestReplace(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomServerPort(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost.local:8000/?foo=bar", reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
|
||||
request = request.WithContext(ctx)
|
||||
|
||||
repl := NewReplacer(request, recordRequest, "-")
|
||||
|
||||
testCase := struct {
|
||||
template string
|
||||
expect string
|
||||
}{
|
||||
template: "{server_port}",
|
||||
expect: "8000",
|
||||
}
|
||||
|
||||
if expected, actual := testCase.expect, repl.Replace(testCase.template); expected != actual {
|
||||
t.Errorf("for template '%s', expected '%s', got '%s'", testCase.template, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTlsReplace(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
|
||||
clientCertText := []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
|
||||
eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
|
||||
A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
|
||||
iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
|
||||
z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
|
||||
fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
|
||||
BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
|
||||
AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
|
||||
eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||
3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
|
||||
9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
block, _ := pem.Decode(clientCertText)
|
||||
if block == nil {
|
||||
t.Fatalf("failed to decode PEM certificate")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode PEM certificate: %v", err)
|
||||
}
|
||||
|
||||
request := &http.Request{
|
||||
Method: "GET",
|
||||
Host: "foo.com",
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Path: "/path/",
|
||||
Host: "foo.com",
|
||||
},
|
||||
Header: http.Header{},
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
RemoteAddr: "192.0.2.1:1234",
|
||||
RequestURI: "https://foo.com/path/",
|
||||
TLS: &tls.ConnectionState{
|
||||
Version: tls.VersionTLS12,
|
||||
HandshakeComplete: true,
|
||||
ServerName: "foo.com",
|
||||
CipherSuite: tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
PeerCertificates: []*x509.Certificate{cert},
|
||||
},
|
||||
}
|
||||
|
||||
repl := NewReplacer(request, recordRequest, "-")
|
||||
|
||||
now := time.Now().In(time.UTC)
|
||||
days := int64(cert.NotAfter.Sub(now).Seconds() / 86400)
|
||||
pemBlock := pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
|
||||
protocol, _ := caddytls.GetSupportedProtocolName(request.TLS.Version)
|
||||
cipher, _ := caddytls.GetSupportedCipherName(request.TLS.CipherSuite)
|
||||
cEscapedCert := url.QueryEscape(string(pem.EncodeToMemory(&pemBlock)))
|
||||
cFingerprint := fmt.Sprintf("%x", sha256.Sum256(cert.Raw))
|
||||
cIDn := cert.Issuer.String()
|
||||
cRawCert := string(cert.Raw)
|
||||
cSDn := cert.Subject.String()
|
||||
cSerial := fmt.Sprintf("%x", cert.SerialNumber)
|
||||
cVEnd := cert.NotAfter.In(time.UTC).Format("Jan 02 15:04:05 2006 MST")
|
||||
cVRemain := strconv.FormatInt(days, 10)
|
||||
cVStart := cert.NotBefore.Format("Jan 02 15:04:05 2006 MST")
|
||||
|
||||
testCases := []struct {
|
||||
template string
|
||||
expect string
|
||||
}{
|
||||
{"{tls_protocol}", protocol},
|
||||
{"{tls_cipher}", cipher},
|
||||
{"{tls_client_escaped_cert}", cEscapedCert},
|
||||
{"{tls_client_fingerprint}", cFingerprint},
|
||||
{"{tls_client_i_dn}", cIDn},
|
||||
{"{tls_client_raw_cert}", cRawCert},
|
||||
{"{tls_client_s_dn}", cSDn},
|
||||
{"{tls_client_serial}", cSerial},
|
||||
{"{tls_client_v_end}", cVEnd},
|
||||
{"{tls_client_v_remain}", cVRemain},
|
||||
{"{tls_client_v_start}", cVStart},
|
||||
{"{server_port}", "443"},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReplace(b *testing.B) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
|
||||
request = request.WithContext(ctx)
|
||||
|
||||
request.Header.Set("Custom", "foobarbaz")
|
||||
request.Header.Set("ShorterVal", "1")
|
||||
repl := NewReplacer(request, recordRequest, "-")
|
||||
// add some headers after creating replacer
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
// add some response headers
|
||||
recordRequest.Header().Set("Custom", "CustomResponseHeader")
|
||||
|
||||
now = func() time.Time {
|
||||
// Note that the `-7` is seconds, not hours.
|
||||
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
repl.Replace("This hostname is {hostname}")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReplaceEscaped(b *testing.B) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
|
||||
request = request.WithContext(ctx)
|
||||
|
||||
request.Header.Set("Custom", "foobarbaz")
|
||||
request.Header.Set("ShorterVal", "1")
|
||||
repl := NewReplacer(request, recordRequest, "-")
|
||||
// add some headers after creating replacer
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
// add some response headers
|
||||
recordRequest.Header().Set("Custom", "CustomResponseHeader")
|
||||
|
||||
now = func() time.Time {
|
||||
// Note that the `-7` is seconds, not hours.
|
||||
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
repl.Replace("\\{ 'hostname': '{hostname}' \\}")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseRecorderNil(t *testing.T) {
|
||||
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Custom", "foobarbaz")
|
||||
repl := NewReplacer(request, nil, "-")
|
||||
// add some headers after creating replacer
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
old := now
|
||||
now = func() time.Time {
|
||||
// Note that the `-7` is seconds, not hours.
|
||||
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
|
||||
}
|
||||
defer func() {
|
||||
now = old
|
||||
}()
|
||||
testCases := []struct {
|
||||
template string
|
||||
expect string
|
||||
}{
|
||||
{"The Custom response header is {<Custom}.", "The Custom response header is -."},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
@@ -140,6 +418,41 @@ func TestSet(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test function to test that various placeholders hold correct values after a rewrite
|
||||
// has been performed. The NewRequest actually contains the rewritten value.
|
||||
func TestPathRewrite(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://getcaddy.com/index.php?key=value", reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Request Formation Failed: %s\n", err.Error())
|
||||
}
|
||||
urlCopy := *request.URL
|
||||
urlCopy.Path = "a/custom/path.php"
|
||||
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, urlCopy)
|
||||
request = request.WithContext(ctx)
|
||||
|
||||
repl := NewReplacer(request, recordRequest, "")
|
||||
|
||||
if got, want := repl.Replace("This path is '{path}'"), "This path is 'a/custom/path.php'"; got != want {
|
||||
t.Errorf("{path} replacement failed; got '%s', want '%s'", got, want)
|
||||
}
|
||||
|
||||
if got, want := repl.Replace("This path is {rewrite_path}"), "This path is /index.php"; got != want {
|
||||
t.Errorf("{rewrite_path} replacement failed; got '%s', want '%s'", got, want)
|
||||
}
|
||||
if got, want := repl.Replace("This path is '{uri}'"), "This path is 'a/custom/path.php?key=value'"; got != want {
|
||||
t.Errorf("{uri} replacement failed; got '%s', want '%s'", got, want)
|
||||
}
|
||||
|
||||
if got, want := repl.Replace("This path is {rewrite_uri}"), "This path is /index.php?key=value"; got != want {
|
||||
t.Errorf("{rewrite_uri} replacement failed; got '%s', want '%s'", got, want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRound(t *testing.T) {
|
||||
var tests = map[time.Duration]time.Duration{
|
||||
// 599.935µs -> 560µs
|
||||
@@ -160,7 +473,7 @@ func TestRound(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMillisecondConverstion(t *testing.T) {
|
||||
func TestMillisecondConversion(t *testing.T) {
|
||||
var testCases = map[time.Duration]int64{
|
||||
2 * time.Second: 2000,
|
||||
9039492 * time.Nanosecond: 9,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ResponseWriterWrapper wrappers underlying ResponseWriter
|
||||
// and inherits its Hijacker/Pusher/CloseNotifier/Flusher as well.
|
||||
type ResponseWriterWrapper struct {
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
// Hijack implements http.Hijacker. It simply wraps the underlying
|
||||
// ResponseWriter's Hijack method if there is one, or returns an error.
|
||||
func (rww *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := rww.ResponseWriter.(http.Hijacker); ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
return nil, nil, NonHijackerError{Underlying: rww.ResponseWriter}
|
||||
}
|
||||
|
||||
// Flush implements http.Flusher. It simply wraps the underlying
|
||||
// ResponseWriter's Flush method if there is one, or panics.
|
||||
func (rww *ResponseWriterWrapper) Flush() {
|
||||
if f, ok := rww.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
} else {
|
||||
panic(NonFlusherError{Underlying: rww.ResponseWriter})
|
||||
}
|
||||
}
|
||||
|
||||
// CloseNotify implements http.CloseNotifier.
|
||||
// It just inherits the underlying ResponseWriter's CloseNotify method.
|
||||
// It panics if the underlying ResponseWriter is not a CloseNotifier.
|
||||
func (rww *ResponseWriterWrapper) CloseNotify() <-chan bool {
|
||||
if cn, ok := rww.ResponseWriter.(http.CloseNotifier); ok {
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
panic(NonCloseNotifierError{Underlying: rww.ResponseWriter})
|
||||
}
|
||||
|
||||
// Push implements http.Pusher.
|
||||
// It just inherits the underlying ResponseWriter's Push method.
|
||||
// It panics if the underlying ResponseWriter is not a Pusher.
|
||||
func (rww *ResponseWriterWrapper) Push(target string, opts *http.PushOptions) error {
|
||||
if pusher, hasPusher := rww.ResponseWriter.(http.Pusher); hasPusher {
|
||||
return pusher.Push(target, opts)
|
||||
}
|
||||
|
||||
return NonPusherError{Underlying: rww.ResponseWriter}
|
||||
}
|
||||
|
||||
// HTTPInterfaces mix all the interfaces that middleware ResponseWriters need to support.
|
||||
type HTTPInterfaces interface {
|
||||
http.ResponseWriter
|
||||
http.Pusher
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
http.Hijacker
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var _ HTTPInterfaces = (*ResponseWriterWrapper)(nil)
|
||||
+121
-42
@@ -1,64 +1,143 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
// LogRoller implements a type that provides a rolling logger.
|
||||
type LogRoller struct {
|
||||
Disabled bool
|
||||
Filename string
|
||||
MaxSize int
|
||||
MaxAge int
|
||||
MaxBackups int
|
||||
Compress bool
|
||||
LocalTime bool
|
||||
}
|
||||
|
||||
// GetLogWriter returns an io.Writer that writes to a rolling logger.
|
||||
// This should be called only from the main goroutine (like during
|
||||
// server setup) because this method is not thread-safe; it is careful
|
||||
// to create only one log writer per log file, even if the log file
|
||||
// is shared by different sites or middlewares. This ensures that
|
||||
// rolling is synchronized, since a process (or multiple processes)
|
||||
// should not create more than one roller on the same file at the
|
||||
// same time. See issue #1363.
|
||||
func (l LogRoller) GetLogWriter() io.Writer {
|
||||
return &lumberjack.Logger{
|
||||
Filename: l.Filename,
|
||||
MaxSize: l.MaxSize,
|
||||
MaxAge: l.MaxAge,
|
||||
MaxBackups: l.MaxBackups,
|
||||
LocalTime: l.LocalTime,
|
||||
absPath, err := filepath.Abs(l.Filename)
|
||||
if err != nil {
|
||||
absPath = l.Filename // oh well, hopefully they're consistent in how they specify the filename
|
||||
}
|
||||
lj, has := lumberjacks[absPath]
|
||||
if !has {
|
||||
lj = &lumberjack.Logger{
|
||||
Filename: l.Filename,
|
||||
MaxSize: l.MaxSize,
|
||||
MaxAge: l.MaxAge,
|
||||
MaxBackups: l.MaxBackups,
|
||||
Compress: l.Compress,
|
||||
LocalTime: l.LocalTime,
|
||||
}
|
||||
lumberjacks[absPath] = lj
|
||||
}
|
||||
return lj
|
||||
}
|
||||
|
||||
// IsLogRollerSubdirective is true if the subdirective is for the log roller.
|
||||
func IsLogRollerSubdirective(subdir string) bool {
|
||||
return subdir == directiveRotateSize ||
|
||||
subdir == directiveRotateAge ||
|
||||
subdir == directiveRotateKeep ||
|
||||
subdir == directiveRotateCompress ||
|
||||
subdir == directiveRotateDisable
|
||||
}
|
||||
|
||||
var errInvalidRollParameter = errors.New("invalid roller parameter")
|
||||
|
||||
// ParseRoller parses roller contents out of c.
|
||||
func ParseRoller(l *LogRoller, what string, where ...string) error {
|
||||
if l == nil {
|
||||
l = DefaultLogRoller()
|
||||
}
|
||||
|
||||
// rotate_compress doesn't accept any parameters.
|
||||
// others only accept one parameter
|
||||
if ((what == directiveRotateCompress || what == directiveRotateDisable) && len(where) != 0) ||
|
||||
((what != directiveRotateCompress && what != directiveRotateDisable) && len(where) != 1) {
|
||||
return errInvalidRollParameter
|
||||
}
|
||||
|
||||
var (
|
||||
value int
|
||||
err error
|
||||
)
|
||||
if what != directiveRotateCompress && what != directiveRotateDisable {
|
||||
value, err = strconv.Atoi(where[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch what {
|
||||
case directiveRotateDisable:
|
||||
l.Disabled = true
|
||||
case directiveRotateSize:
|
||||
l.MaxSize = value
|
||||
case directiveRotateAge:
|
||||
l.MaxAge = value
|
||||
case directiveRotateKeep:
|
||||
l.MaxBackups = value
|
||||
case directiveRotateCompress:
|
||||
l.Compress = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultLogRoller will roll logs by default.
|
||||
func DefaultLogRoller() *LogRoller {
|
||||
return &LogRoller{
|
||||
MaxSize: defaultRotateSize,
|
||||
MaxAge: defaultRotateAge,
|
||||
MaxBackups: defaultRotateKeep,
|
||||
Compress: false,
|
||||
LocalTime: true,
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
// c.nesting > 0 but, as soon as c meets a }, it thinks
|
||||
// the block is over and return false for c.NextBlock.
|
||||
for c.NextBlock() {
|
||||
what := c.Val()
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
value := c.Val()
|
||||
var err error
|
||||
switch what {
|
||||
case "size":
|
||||
size, err = strconv.Atoi(value)
|
||||
case "age":
|
||||
age, err = strconv.Atoi(value)
|
||||
case "keep":
|
||||
keep, err = strconv.Atoi(value)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &LogRoller{
|
||||
MaxSize: size,
|
||||
MaxAge: age,
|
||||
MaxBackups: keep,
|
||||
LocalTime: true,
|
||||
}, nil
|
||||
}
|
||||
const (
|
||||
// defaultRotateSize is 100 MB.
|
||||
defaultRotateSize = 100
|
||||
// defaultRotateAge is 14 days.
|
||||
defaultRotateAge = 14
|
||||
// defaultRotateKeep is 10 files.
|
||||
defaultRotateKeep = 10
|
||||
|
||||
directiveRotateDisable = "rotate_disable"
|
||||
directiveRotateSize = "rotate_size"
|
||||
directiveRotateAge = "rotate_age"
|
||||
directiveRotateKeep = "rotate_keep"
|
||||
directiveRotateCompress = "rotate_compress"
|
||||
)
|
||||
|
||||
// lumberjacks maps log filenames to the logger
|
||||
// that is being used to keep them rolled/maintained.
|
||||
var lumberjacks = make(map[string]io.Writer)
|
||||
|
||||
+493
-414
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,22 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -15,7 +30,7 @@ func TestAddress(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeHTTPServer(t *testing.T) {
|
||||
func TestMakeHTTPServerWithTimeouts(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
group []*SiteConfig
|
||||
expected Timeouts
|
||||
@@ -92,7 +107,7 @@ func TestMakeHTTPServer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
} {
|
||||
actual := makeHTTPServer("127.0.0.1:9005", tc.group)
|
||||
actual := makeHTTPServerWithTimeouts("127.0.0.1:9005", tc.group)
|
||||
|
||||
if got, want := actual.Addr, "127.0.0.1:9005"; got != want {
|
||||
t.Errorf("Test %d: Expected Addr=%s, but was %s", i, want, got)
|
||||
@@ -100,15 +115,155 @@ func TestMakeHTTPServer(t *testing.T) {
|
||||
if got, want := actual.ReadTimeout, tc.expected.ReadTimeout; got != want {
|
||||
t.Errorf("Test %d: Expected ReadTimeout=%v, but was %v", i, want, got)
|
||||
}
|
||||
// TODO: ReadHeaderTimeout and IdleTimeout require Go 1.8
|
||||
// if got, want := actual.ReadHeaderTimeout, tc.expected.ReadHeaderTimeout; got != want {
|
||||
// t.Errorf("Test %d: Expected ReadHeaderTimeout=%v, but was %v", i, want, got)
|
||||
// }
|
||||
if got, want := actual.ReadHeaderTimeout, tc.expected.ReadHeaderTimeout; got != want {
|
||||
t.Errorf("Test %d: Expected ReadHeaderTimeout=%v, but was %v", i, want, got)
|
||||
}
|
||||
if got, want := actual.WriteTimeout, tc.expected.WriteTimeout; got != want {
|
||||
t.Errorf("Test %d: Expected WriteTimeout=%v, but was %v", i, want, got)
|
||||
}
|
||||
// if got, want := actual.IdleTimeout, tc.expected.IdleTimeout; got != want {
|
||||
// t.Errorf("Test %d: Expected IdleTimeout=%v, but was %v", i, want, got)
|
||||
// }
|
||||
if got, want := actual.IdleTimeout, tc.expected.IdleTimeout; got != want {
|
||||
t.Errorf("Test %d: Expected IdleTimeout=%v, but was %v", i, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimPathPrefix(t *testing.T) {
|
||||
for i, pt := range []struct {
|
||||
url string
|
||||
prefix string
|
||||
expected string
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
url: "/my/path",
|
||||
prefix: "/my",
|
||||
expected: "/path",
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
url: "/my/%2f/path",
|
||||
prefix: "/my",
|
||||
expected: "/%2f/path",
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
url: "/my/path",
|
||||
prefix: "/my/",
|
||||
expected: "/path",
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
url: "/my///path",
|
||||
prefix: "/my",
|
||||
expected: "/path",
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
url: "/my///path",
|
||||
prefix: "/my",
|
||||
expected: "///path",
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
url: "/my/path///slash",
|
||||
prefix: "/my",
|
||||
expected: "/path///slash",
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
url: "/my/%2f/path/%2f",
|
||||
prefix: "/my",
|
||||
expected: "/%2f/path/%2f",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/my/%20/path",
|
||||
prefix: "/my",
|
||||
expected: "/%20/path",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/path",
|
||||
prefix: "",
|
||||
expected: "/path",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/path/my/",
|
||||
prefix: "/my",
|
||||
expected: "/path/my/",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "",
|
||||
prefix: "/my",
|
||||
expected: "/",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/apath",
|
||||
prefix: "",
|
||||
expected: "/apath",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/my/path/page.php?akey=value",
|
||||
prefix: "/my",
|
||||
expected: "/path/page.php?akey=value",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/my/path/page?key=value#fragment",
|
||||
prefix: "/my",
|
||||
expected: "/path/page?key=value#fragment",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/my/path/page#fragment",
|
||||
prefix: "/my",
|
||||
expected: "/path/page#fragment",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/my/apath?",
|
||||
prefix: "/my",
|
||||
expected: "/apath?",
|
||||
shouldFail: false,
|
||||
},
|
||||
} {
|
||||
|
||||
u, _ := url.Parse(pt.url)
|
||||
if got, want := trimPathPrefix(u, pt.prefix), pt.expected; got.String() != want {
|
||||
if !pt.shouldFail {
|
||||
|
||||
t.Errorf("Test %d: Expected='%s', but was '%s' ", i, want, got.String())
|
||||
}
|
||||
} else if pt.shouldFail {
|
||||
t.Errorf("SHOULDFAIL Test %d: Expected='%s', and was '%s' but should fail", i, want, got.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeHTTPServerWithHeaderLimit(t *testing.T) {
|
||||
for name, c := range map[string]struct {
|
||||
group []*SiteConfig
|
||||
expect int
|
||||
}{
|
||||
"disable": {
|
||||
group: []*SiteConfig{{}},
|
||||
expect: 0,
|
||||
},
|
||||
"oneSite": {
|
||||
group: []*SiteConfig{{Limits: Limits{
|
||||
MaxRequestHeaderSize: 100,
|
||||
}}},
|
||||
expect: 100,
|
||||
},
|
||||
"multiSites": {
|
||||
group: []*SiteConfig{
|
||||
{Limits: Limits{MaxRequestHeaderSize: 100}},
|
||||
{Limits: Limits{MaxRequestHeaderSize: 50}},
|
||||
},
|
||||
expect: 50,
|
||||
},
|
||||
} {
|
||||
c := c
|
||||
t.Run(name, func(t *testing.T) {
|
||||
actual := makeHTTPServerWithHeaderLimit(&http.Server{}, c.group)
|
||||
if got := actual.MaxHeaderBytes; got != c.expect {
|
||||
t.Errorf("Expect %d, but got %d", c.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
)
|
||||
|
||||
// SiteConfig contains information about a site
|
||||
@@ -12,6 +26,9 @@ type SiteConfig struct {
|
||||
// The address of the site
|
||||
Addr Address
|
||||
|
||||
// The list of viable index page names of the site
|
||||
IndexPages []string
|
||||
|
||||
// The hostname to bind listener to;
|
||||
// defaults to Addr.Host
|
||||
ListenHost string
|
||||
@@ -25,6 +42,9 @@ type SiteConfig struct {
|
||||
// Compiled middleware stack
|
||||
middlewareChain Handler
|
||||
|
||||
// listener middleware stack
|
||||
listenerMiddleware []ListenerMiddleware
|
||||
|
||||
// Directory from which to serve files
|
||||
Root string
|
||||
|
||||
@@ -35,8 +55,8 @@ type SiteConfig struct {
|
||||
// for a request.
|
||||
HiddenFiles []string
|
||||
|
||||
// Max amount of bytes a request can send on a given path
|
||||
MaxRequestBodySizes []PathLimit
|
||||
// Max request's header/body size
|
||||
Limits Limits
|
||||
|
||||
// The path to the Caddyfile used to generate this site config
|
||||
originCaddyfile string
|
||||
@@ -49,10 +69,14 @@ type SiteConfig struct {
|
||||
// preserving functionality needed for proxying,
|
||||
// websockets, etc.
|
||||
Timeouts Timeouts
|
||||
|
||||
// If true, any requests not matching other site definitions
|
||||
// may be served by this site.
|
||||
FallbackSite bool
|
||||
}
|
||||
|
||||
// Timeouts specify various timeouts for a server to use.
|
||||
// If the assocated bool field is true, then the duration
|
||||
// If the associated bool field is true, then the duration
|
||||
// value should be treated literally (i.e. a zero-value
|
||||
// duration would mean "no timeout"). If false, the duration
|
||||
// was left unset, so a zero-value duration would mean to
|
||||
@@ -68,6 +92,12 @@ type Timeouts struct {
|
||||
IdleTimeoutSet bool
|
||||
}
|
||||
|
||||
// Limits specify size limit of request's header and body.
|
||||
type Limits struct {
|
||||
MaxRequestHeaderSize int64
|
||||
MaxRequestBodySizes []PathLimit
|
||||
}
|
||||
|
||||
// PathLimit is a mapping from a site's path to its corresponding
|
||||
// maximum request body size (in bytes)
|
||||
type PathLimit struct {
|
||||
@@ -80,6 +110,11 @@ func (s *SiteConfig) AddMiddleware(m Middleware) {
|
||||
s.middleware = append(s.middleware, m)
|
||||
}
|
||||
|
||||
// AddListenerMiddleware adds a listener middleware to a site's listenerMiddleware stack.
|
||||
func (s *SiteConfig) AddListenerMiddleware(l ListenerMiddleware) {
|
||||
s.listenerMiddleware = append(s.listenerMiddleware, l)
|
||||
}
|
||||
|
||||
// TLSConfig returns s.TLS.
|
||||
func (s SiteConfig) TLSConfig() *caddytls.Config {
|
||||
return s.TLS
|
||||
@@ -99,3 +134,8 @@ func (s SiteConfig) Port() string {
|
||||
func (s SiteConfig) Middleware() []Middleware {
|
||||
return s.middleware
|
||||
}
|
||||
|
||||
// ListenerMiddleware returns s.listenerMiddleware
|
||||
func (s SiteConfig) ListenerMiddleware() []ListenerMiddleware {
|
||||
return s.listenerMiddleware
|
||||
}
|
||||
|
||||
@@ -1,19 +1,40 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
mathrand "math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/mholt/certmagic"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
@@ -26,6 +47,20 @@ type Context struct {
|
||||
Req *http.Request
|
||||
URL *url.URL
|
||||
Args []interface{} // defined by arguments to .Include
|
||||
|
||||
// just used for adding preload links for server push
|
||||
responseHeader http.Header
|
||||
}
|
||||
|
||||
// NewContextWithHeader creates a context with given response header.
|
||||
//
|
||||
// To plugin developer:
|
||||
// The returned context's exported fileds remain empty,
|
||||
// you should then initialize them if you want.
|
||||
func NewContextWithHeader(rh http.Header) Context {
|
||||
return Context{
|
||||
responseHeader: rh,
|
||||
}
|
||||
}
|
||||
|
||||
// Include returns the contents of filename relative to the site root.
|
||||
@@ -61,6 +96,18 @@ func (c Context) Header(name string) string {
|
||||
return c.Req.Header.Get(name)
|
||||
}
|
||||
|
||||
// Hostname gets the (remote) hostname of the client making the request.
|
||||
func (c Context) Hostname() string {
|
||||
ip := c.IP()
|
||||
|
||||
hostnameList, err := net.LookupAddr(ip)
|
||||
if err != nil || len(hostnameList) == 0 {
|
||||
return c.Req.RemoteAddr
|
||||
}
|
||||
|
||||
return hostnameList[0]
|
||||
}
|
||||
|
||||
// Env gets a map of the environment variables.
|
||||
func (c Context) Env() map[string]string {
|
||||
osEnv := os.Environ()
|
||||
@@ -83,6 +130,29 @@ func (c Context) IP() string {
|
||||
return ip
|
||||
}
|
||||
|
||||
// To mock the net.InterfaceAddrs from the test.
|
||||
var networkInterfacesFn = net.InterfaceAddrs
|
||||
|
||||
// ServerIP gets the (local) IP address of the server.
|
||||
// TODO: The bind directive should be honored in this method (see PR #1474).
|
||||
func (c Context) ServerIP() string {
|
||||
addrs, err := networkInterfacesFn()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, address := range addrs {
|
||||
// Validate the address and check if it's not a loopback
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil || ipnet.IP.To16() != nil {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// URI returns the raw, unprocessed request URI (including query
|
||||
// string and hash) obtained directly from the Request-Line of
|
||||
// the HTTP request.
|
||||
@@ -110,7 +180,7 @@ func (c Context) Port() (string, error) {
|
||||
if err != nil {
|
||||
if !strings.Contains(c.Req.Host, ":") {
|
||||
// common with sites served on the default port 80
|
||||
return "80", nil
|
||||
return strconv.Itoa(certmagic.HTTPPort), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
@@ -236,13 +306,15 @@ func ContextInclude(filename string, ctx interface{}, fs http.FileSystem) (strin
|
||||
return "", err
|
||||
}
|
||||
|
||||
tpl, err := template.New(filename).Parse(string(body))
|
||||
tpl, err := template.New(filename).Funcs(TemplateFuncs).Parse(string(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tpl.Execute(&buf, ctx)
|
||||
buf := includeBufs.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer includeBufs.Put(buf)
|
||||
err = tpl.Execute(buf, ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -321,3 +393,83 @@ func (c Context) Files(name string) ([]string, error) {
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// IsMITM returns true if it seems likely that the TLS connection
|
||||
// is being intercepted.
|
||||
func (c Context) IsMITM() bool {
|
||||
if val, ok := c.Req.Context().Value(MitmCtxKey).(bool); ok {
|
||||
return val
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RandomString generates a random string of random length given
|
||||
// length bounds. Thanks to http://stackoverflow.com/a/35615565/1048862
|
||||
// for the clever technique that is fairly fast, secure, and maintains
|
||||
// proper distributions over the dictionary.
|
||||
func (c Context) RandomString(minLen, maxLen int) string {
|
||||
const (
|
||||
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
letterIdxBits = 6 // 6 bits to represent 64 possibilities (indexes)
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // all 1-bits, as many as letterIdxBits
|
||||
)
|
||||
|
||||
if minLen < 0 || maxLen < 0 || maxLen < minLen {
|
||||
return ""
|
||||
}
|
||||
|
||||
n := mathrand.Intn(maxLen-minLen+1) + minLen // choose actual length
|
||||
|
||||
// secureRandomBytes returns a number of bytes using crypto/rand.
|
||||
secureRandomBytes := func(numBytes int) []byte {
|
||||
randomBytes := make([]byte, numBytes)
|
||||
if _, err := rand.Read(randomBytes); err != nil {
|
||||
log.Println("[ERROR] failed to read bytes: ", err)
|
||||
}
|
||||
return randomBytes
|
||||
}
|
||||
|
||||
result := make([]byte, n)
|
||||
bufferSize := int(float64(n) * 1.3)
|
||||
for i, j, randomBytes := 0, 0, []byte{}; i < n; j++ {
|
||||
if j%bufferSize == 0 {
|
||||
randomBytes = secureRandomBytes(bufferSize)
|
||||
}
|
||||
if idx := int(randomBytes[j%n] & letterIdxMask); idx < len(letterBytes) {
|
||||
result[i] = letterBytes[idx]
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// AddLink adds a link header in response
|
||||
// see https://www.w3.org/wiki/LinkHeader
|
||||
func (c Context) AddLink(link string) string {
|
||||
if c.responseHeader == nil {
|
||||
return ""
|
||||
}
|
||||
c.responseHeader.Add("Link", link)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Returns either TLS protocol version if TLS used or empty string otherwise
|
||||
func (c Context) TLSVersion() (ret string) {
|
||||
if c.Req.TLS != nil {
|
||||
// Safe to ignore an error
|
||||
ret, _ = caddytls.GetSupportedProtocolName(c.Req.TLS.Version)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// buffer pool for .Include context actions
|
||||
var includeBufs = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
// TemplateFuncs contains user-defined functions
|
||||
// for execution in templates.
|
||||
var TemplateFuncs = template.FuncMap{}
|
||||
@@ -1,10 +1,27 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -71,12 +88,20 @@ func TestInclude(t *testing.T) {
|
||||
shouldErr: true,
|
||||
expectedErrorContent: `type httpserver.Context`,
|
||||
},
|
||||
// Test 4 - all good, with custom function
|
||||
{
|
||||
fileContent: `hello {{ caddy }}`,
|
||||
expectedContent: "hello caddy",
|
||||
shouldErr: false,
|
||||
expectedErrorContent: "",
|
||||
},
|
||||
}
|
||||
|
||||
TemplateFuncs["caddy"] = func() string { return "caddy" }
|
||||
for i, test := range tests {
|
||||
testPrefix := getTestPrefix(i)
|
||||
|
||||
// WriteFile truncates the contentt
|
||||
// WriteFile truncates the content
|
||||
err := ioutil.WriteFile(absInFilePath, []byte(test.fileContent), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(testPrefix+"Failed to create test file. Error was: %v", err)
|
||||
@@ -137,7 +162,7 @@ func TestMarkdown(t *testing.T) {
|
||||
for i, test := range tests {
|
||||
testPrefix := getTestPrefix(i)
|
||||
|
||||
// WriteFile truncates the contentt
|
||||
// WriteFile truncates the content
|
||||
err := ioutil.WriteFile(absInFilePath, []byte(test.fileContent), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(testPrefix+"Failed to create test file. Error was: %v", err)
|
||||
@@ -235,6 +260,39 @@ func TestHeader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostname(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
tests := []struct {
|
||||
inputRemoteAddr string
|
||||
expectedHostname string
|
||||
}{
|
||||
// TODO(mholt): Fix these tests, they're not portable. i.e. my resolver
|
||||
// returns "fwdr-8.fwdr-8.fwdr-8.fwdr-8." instead of these google ones.
|
||||
// Test 0 - ipv4 with port
|
||||
// {"8.8.8.8:1111", "google-public-dns-a.google.com."},
|
||||
// // Test 1 - ipv4 without port
|
||||
// {"8.8.8.8", "google-public-dns-a.google.com."},
|
||||
// // Test 2 - ipv6 with port
|
||||
// {"[2001:4860:4860::8888]:11", "google-public-dns-a.google.com."},
|
||||
// // Test 3 - ipv6 without port and brackets
|
||||
// {"2001:4860:4860::8888", "google-public-dns-a.google.com."},
|
||||
// Test 4 - no hostname available
|
||||
{"0.0.0.0", "0.0.0.0"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
testPrefix := getTestPrefix(i)
|
||||
|
||||
context.Req.RemoteAddr = test.inputRemoteAddr
|
||||
actualHostname := context.Hostname()
|
||||
|
||||
if actualHostname != test.expectedHostname {
|
||||
t.Errorf(testPrefix+"Expected hostname %s, found %s", test.expectedHostname, actualHostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
@@ -302,6 +360,44 @@ func TestIP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type myIP string
|
||||
|
||||
func (ip myIP) mockInterfaces() ([]net.Addr, error) {
|
||||
a := net.ParseIP(string(ip))
|
||||
|
||||
return []net.Addr{
|
||||
&net.IPNet{IP: a, Mask: nil},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestServerIP(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
tests := []string{
|
||||
// Test 0 - ipv4
|
||||
"1.1.1.1",
|
||||
// Test 1 - ipv6
|
||||
"2001:db8:a0b:12f0::1",
|
||||
}
|
||||
|
||||
for i, expectedIP := range tests {
|
||||
testPrefix := getTestPrefix(i)
|
||||
|
||||
// Mock the network interface
|
||||
ip := myIP(expectedIP)
|
||||
networkInterfacesFn = ip.mockInterfaces
|
||||
defer func() {
|
||||
networkInterfacesFn = net.InterfaceAddrs
|
||||
}()
|
||||
|
||||
actualIP := context.ServerIP()
|
||||
|
||||
if actualIP != expectedIP {
|
||||
t.Errorf("%sExpected IP \"%s\", found \"%s\".", testPrefix, expectedIP, actualIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestURL(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
@@ -416,7 +512,7 @@ func TestMethod(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestPathMatches(t *testing.T) {
|
||||
func TestContextPathMatches(t *testing.T) {
|
||||
context := getContextOrFail(t)
|
||||
|
||||
tests := []struct {
|
||||
@@ -651,8 +747,9 @@ func initTestContext() (Context, error) {
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
return Context{Root: http.Dir(os.TempDir()), Req: request}, nil
|
||||
return Context{Root: http.Dir(os.TempDir()), responseHeader: res.Header(), Req: request}, nil
|
||||
}
|
||||
|
||||
func getContextOrFail(t *testing.T) Context {
|
||||
@@ -794,3 +891,72 @@ func TestFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddLink(t *testing.T) {
|
||||
for name, c := range map[string]struct {
|
||||
input string
|
||||
expectLinks []string
|
||||
}{
|
||||
"oneLink": {
|
||||
input: `{{.AddLink "</test.css>; rel=preload"}}`,
|
||||
expectLinks: []string{"</test.css>; rel=preload"},
|
||||
},
|
||||
"multipleLinks": {
|
||||
input: `{{.AddLink "</test1.css>; rel=preload"}} {{.AddLink "</test2.css>; rel=meta"}}`,
|
||||
expectLinks: []string{"</test1.css>; rel=preload", "</test2.css>; rel=meta"},
|
||||
},
|
||||
} {
|
||||
c := c
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := getContextOrFail(t)
|
||||
tmpl, err := template.New("").Parse(c.input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = tmpl.Execute(ioutil.Discard, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := ctx.responseHeader["Link"]; !reflect.DeepEqual(got, c.expectLinks) {
|
||||
t.Errorf("Result not match: expect %v, but got %v", c.expectLinks, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTlsVersion(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
tlsState *tls.ConnectionState
|
||||
expectedResult string
|
||||
}{
|
||||
{
|
||||
&tls.ConnectionState{Version: tls.VersionTLS10},
|
||||
"tls1.0",
|
||||
},
|
||||
{
|
||||
&tls.ConnectionState{Version: tls.VersionTLS11},
|
||||
"tls1.1",
|
||||
},
|
||||
{
|
||||
&tls.ConnectionState{Version: tls.VersionTLS12},
|
||||
"tls1.2",
|
||||
},
|
||||
// TLS not used
|
||||
{
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
// Unsupported version
|
||||
{
|
||||
&tls.ConnectionState{Version: 0x0399},
|
||||
"",
|
||||
},
|
||||
} {
|
||||
context := getContextOrFail(t)
|
||||
context.Req.TLS = test.tlsState
|
||||
result := context.TLSVersion()
|
||||
if result != test.expectedResult {
|
||||
t.Errorf("Expected %s got %s", test.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user