mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 15:24:46 +00:00
Compare commits
899 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b75943d59 | |||
| fc20a22685 | |||
| faa86ffeca | |||
| f7a14d9200 | |||
| 902e5beb4f | |||
| 7ca5f7569a | |||
| 4ac68711cd | |||
| 075ac1acf1 | |||
| cfef0e737f | |||
| 8ec0f7746b | |||
| 2b3707e2b9 | |||
| 7e760f9fcc | |||
| efb99d90d7 | |||
| e131125558 | |||
| af6bf5c4e5 | |||
| e3390fcb15 | |||
| fc5749328d | |||
| 59f5b0df83 | |||
| f6bd4aa039 | |||
| d2bc15e224 | |||
| bad2e8e812 | |||
| 20ed7557f9 | |||
| 8e9e1d973e | |||
| a97673e8b9 | |||
| e123bbe1c5 | |||
| 906eb2d840 | |||
| e09c62a796 | |||
| bd88093de0 | |||
| 0538953a39 | |||
| 88235356d5 | |||
| 99f39225f1 | |||
| 278df5478a | |||
| d3fd581b7b | |||
| 68d370af19 | |||
| 679a89c7df | |||
| a768132d37 | |||
| 9de4e88ca6 | |||
| 91eba53035 | |||
| 065d8355c5 | |||
| 4016334efc | |||
| d4d9a43dd7 | |||
| 046a30e3c7 | |||
| 7a9207a6e1 | |||
| b9cba7f2ae | |||
| 7f1c890a82 | |||
| f48a7cd3dd | |||
| d96c39196d | |||
| 032ba7b5e4 | |||
| e63ec34503 | |||
| 2a177e0fc4 | |||
| 1733fe8c65 | |||
| 06f7ba4523 | |||
| 7607ead143 | |||
| ac7fdd5572 | |||
| f3ba506880 | |||
| d2cb803336 | |||
| efd4556546 | |||
| 2e2718b7e3 | |||
| b849def673 | |||
| dd540af695 | |||
| e921ab35d5 | |||
| ae7fbc14f3 | |||
| 2fa50acc2d | |||
| c7a6304d32 | |||
| f4667c6345 | |||
| 6a6ba704f1 | |||
| 135c8e6d13 | |||
| 420e80ea50 | |||
| 337591b2bb | |||
| fa6bcdad04 | |||
| ee87c43eb9 | |||
| bccc58b54d | |||
| 3da1c25471 | |||
| 7d0a271000 | |||
| 102631d98d | |||
| 6c3d2fc339 | |||
| 1d79831651 | |||
| 0458ad41dd | |||
| b9453b84bb | |||
| ab81ff8bfb | |||
| f2339a7244 | |||
| 978e01f77e | |||
| 1275930f99 | |||
| 8e9091cc48 | |||
| 76add5ca77 | |||
| e8407848ec | |||
| 29d809535e | |||
| cc1c6d9abc | |||
| 69b28e358c | |||
| 5ed3d4e29a | |||
| 4f468b8a5d | |||
| e3aad7799e | |||
| cc8aa0efb5 | |||
| 475939a77f | |||
| 4ccef23dbc | |||
| 2b0d704777 | |||
| ee80e55833 | |||
| 2345720b2b | |||
| d714a62bd3 | |||
| 7d4afd4ae0 | |||
| 056693c814 | |||
| 73d948bc32 | |||
| 5e6f606f4e | |||
| 919227fc91 | |||
| b8ba5b444c | |||
| baed5f4eea | |||
| 08a8101308 | |||
| a2a4b06eb4 | |||
| ec509e114a | |||
| 7bc2462e36 | |||
| 92f647d45c | |||
| b2ac885370 | |||
| 2c480a72db | |||
| 32739e9f98 | |||
| 7ce2bb8b2f | |||
| 6f78ccde04 | |||
| 26a7b59f6f | |||
| 4b6437cc60 | |||
| f7fd4ea71c | |||
| 7bcab138c5 | |||
| f758361730 | |||
| fa2234d639 | |||
| 99d4e48656 | |||
| 0180b6d733 | |||
| 9ef6191515 | |||
| 2cf43abe8c | |||
| 46c147a1b2 | |||
| 1e71202c89 | |||
| 8250708b37 | |||
| 7499e5fa00 | |||
| e8fe34773c | |||
| 3254d08173 | |||
| d68ad89159 | |||
| f3244db861 | |||
| d22cb4a6ca | |||
| 8f0498f66a | |||
| a97233bb3e | |||
| 775c2bc93e | |||
| e4278bab97 | |||
| f81b0ee9e8 | |||
| 8a07a900fd | |||
| d727d3ade6 | |||
| 7a6ab54fcb | |||
| def8f57dbc | |||
| 9cde11f8e0 | |||
| d1a4710aa2 | |||
| 0dc3428424 | |||
| b07b8b4d4b | |||
| d433a0fa54 | |||
| 2f05f969e2 | |||
| 14002e44e1 | |||
| b12272529f | |||
| 377a9a8d27 | |||
| a0f6eb9d5e | |||
| 12302ba1bf | |||
| 317a7ea7e5 | |||
| 636ec75010 | |||
| 98deb95eae | |||
| c0f85ab85b | |||
| c5d1662244 | |||
| 8fd6074d67 | |||
| 7e6fc49979 | |||
| eb6697ae98 | |||
| 092e76eb55 | |||
| 62e1330e45 | |||
| 98736a03e1 | |||
| 4836216a9b | |||
| 470e6c35c5 | |||
| e2a8302bbc | |||
| 6eeaf4be4b | |||
| 5e2e757403 | |||
| 9422ea8ed8 | |||
| 1b511b2d25 | |||
| 05b903a32e | |||
| 398cc8b134 | |||
| e1a9e98cca | |||
| 057a0cc758 | |||
| ca43b0357f | |||
| 8cbd222e10 | |||
| a50c0ca9ad | |||
| 5352b3cf04 | |||
| 9552bb7bc7 | |||
| c54e8cd8e6 | |||
| aeda35699e | |||
| eee0d57ed0 | |||
| 0899d6a136 | |||
| f44e496dd9 | |||
| 3aebaaad01 | |||
| 9d9627f645 | |||
| 5c6207debc | |||
| 7220c2c214 | |||
| d17a61c15b | |||
| 5431e0ca12 | |||
| 706523389c | |||
| c2a32de35f | |||
| a1d485eca5 | |||
| 8a1ba1f8ca | |||
| b6d7076400 | |||
| 22cd8ceb8c | |||
| 9cd2780079 | |||
| 9909e9d63c | |||
| 051b2cf352 | |||
| 4cf462e322 | |||
| e56c4532ce | |||
| d78a5ba5da | |||
| 1c50618f97 | |||
| 630650b90e | |||
| d68ff390ca | |||
| 0571210374 | |||
| a16dee1d2a | |||
| 2f6f865f92 | |||
| e22422aafb | |||
| ff4cfeda0c | |||
| db01127191 | |||
| 1ff5fd3fdc | |||
| 5b12e74099 | |||
| b1edf5b96d | |||
| d07d24e5a2 | |||
| 0ab6867ae5 | |||
| ed2bac026d | |||
| e09dcf6d60 | |||
| 76fb329a65 | |||
| 7bac4b15b0 | |||
| 8a5343d0a5 | |||
| a84cbcde7e | |||
| 1a92f1acfe | |||
| c196679bc7 | |||
| 10fc450ae5 | |||
| 74556bcd7d | |||
| 97309d81ab | |||
| 0292727a95 | |||
| f33897615d | |||
| 6fa58aadba | |||
| ef3152f334 | |||
| d6036d96f0 | |||
| a6faa0c376 | |||
| 1086d5ede5 | |||
| c314d58b69 | |||
| 628545d229 | |||
| ead93e9f26 | |||
| 5f380f3a54 | |||
| 7814e870a7 | |||
| 7c7cf688e6 | |||
| a39d95d5f7 | |||
| 01ad2785ee | |||
| 6822e4f8ab | |||
| 834c0eaeed | |||
| 74a3026963 | |||
| 8445b88d3c | |||
| 7a55208c61 | |||
| 581cfb8480 | |||
| 201c462902 | |||
| ebae7a7024 | |||
| 70e675f42c | |||
| 8f46065ab5 | |||
| 41b9c22216 | |||
| 88ce63e785 | |||
| 2dc5f6ec8c | |||
| 8d41f99f2f | |||
| 173190aa79 | |||
| 9251b3aa1f | |||
| b0e27d1eac | |||
| 73a265f2fc | |||
| 9bc59bc78c | |||
| b73c588254 | |||
| 7e47667b08 | |||
| eea3d11e40 | |||
| dd32dc1364 | |||
| fc2333c934 | |||
| 571380b3f5 | |||
| eec6b87eea | |||
| 6cc7d99e32 | |||
| 59bbd51065 | |||
| e35f744b36 | |||
| a96d4243ba | |||
| d4733efb25 | |||
| d1be558ca7 | |||
| b88e0bc8f8 | |||
| 197a70c9c4 | |||
| e71b88fcaa | |||
| 157f5d1412 | |||
| 7024d193c9 | |||
| 794635fb54 | |||
| 1ee540a166 | |||
| 6bcc9a76e9 | |||
| 43f1c6ba82 | |||
| 0146a8d8ed | |||
| 36479ef11f | |||
| 58538619ea | |||
| 573d410606 | |||
| f6f10305a6 | |||
| 588f1a03c4 | |||
| f8fbbcd806 | |||
| 2ca4633f89 | |||
| 2ce11a20c4 | |||
| ff7c48568c | |||
| 958650be1f | |||
| eb51ff0a6d | |||
| 3f4407ce27 | |||
| d9636c73b4 | |||
| 6cbf90883d | |||
| 76ade67f95 | |||
| 997f2cf612 | |||
| ceb509ee98 | |||
| 5a3c0fdffa | |||
| 2822fbe3db | |||
| 5148d00516 | |||
| bb10e1dee5 | |||
| e445fd92f7 | |||
| bccf4a63dc | |||
| 7059ef8e13 | |||
| 1239006e96 | |||
| cbdf88ea28 | |||
| 79ebfa8304 | |||
| 5a5e49179a | |||
| 470a85e65d | |||
| d7da74cb9e | |||
| 27e1277a3b | |||
| 6238fd9022 | |||
| f985ed567f | |||
| d54c8cc745 | |||
| 548e85829a | |||
| fd14bf440b | |||
| e2b18364f4 | |||
| 52c4f875fa | |||
| dac077fef1 | |||
| 0e68b1d24e | |||
| 6968b714d0 | |||
| 9310241e1a | |||
| 1cb22817db | |||
| fe4b9b1535 | |||
| d04f48d872 | |||
| b5f6559179 | |||
| 89d408e3bd | |||
| 533d005159 | |||
| 98c3957d30 | |||
| 671754fd19 | |||
| d1801776b0 | |||
| 11c06b5a1f | |||
| 27cd83c2d3 | |||
| 75bdc96763 | |||
| c51879b17f | |||
| 738b4f8d25 | |||
| c7a44009d3 | |||
| 754ad59512 | |||
| 414cb12f02 | |||
| d0a1daac3b | |||
| 2afa307765 | |||
| a4a9f45b0a | |||
| 1da4fbbe0b | |||
| 836149a5b0 | |||
| 4ff215ee8c | |||
| 17e3073a17 | |||
| 071d595371 | |||
| b6c85401a5 | |||
| 3ef3e7a99a | |||
| 12ad264eb3 | |||
| 1082ac1c36 | |||
| a233f975c1 | |||
| 5f6e867685 | |||
| 78a9454023 | |||
| ca85df10ff | |||
| b8333b44a2 | |||
| e49a7a4389 | |||
| 5749425671 | |||
| fac9dfb6e5 | |||
| d9ec18314d | |||
| ed024d0741 | |||
| fd4000184c | |||
| bcc847963e | |||
| 1e8dea9112 | |||
| cd4af5696d | |||
| 8527d03a29 | |||
| fa8aa02270 | |||
| aa24338225 | |||
| 0924549efd | |||
| 67a3be5b7a | |||
| c33a516663 | |||
| ed1389ef08 | |||
| 8fb6508ae6 | |||
| ee8c8bd4c6 | |||
| 0269062b24 | |||
| bccf650165 | |||
| cecc74d3fb | |||
| 18992efa0c | |||
| bd8af7d80d | |||
| dff694b218 | |||
| 38af26e232 | |||
| 8f3526289a | |||
| 81dff44bb9 | |||
| 6e45e0d53b | |||
| a7d2de1e12 | |||
| 59cae0f622 | |||
| d678584d89 | |||
| f1b57526b3 | |||
| 3eb9efd9f0 | |||
| 8d99e92852 | |||
| 31ff7caeeb | |||
| 5d84874d53 | |||
| b06fe0fc5f | |||
| bf38e5aa12 | |||
| bf068e728f | |||
| f88732277a | |||
| a11c64b091 | |||
| e404c29edb | |||
| 98a0844f56 | |||
| d6d65d183b | |||
| 951d13d76c | |||
| f99ae90ca1 | |||
| e99d0b5bc4 | |||
| 48c5721bc6 | |||
| 235897ba21 | |||
| bf737a0efc | |||
| 4c5ebccacc | |||
| a3153c6add | |||
| a2bbe1bdc2 | |||
| 6526211a69 | |||
| 103b2eca00 | |||
| 14ea6298bc | |||
| b297e8bb90 | |||
| 6cc8c39fa8 | |||
| 4d4eedd1c0 | |||
| a00eda9538 | |||
| 9ff65c1e90 | |||
| 2dcf3bd011 | |||
| ae460b340b | |||
| bc54a7f87b | |||
| 75c3ca2f4a | |||
| 4bd17766a9 | |||
| b87cb9aee8 | |||
| 07af2a33b7 | |||
| 209091da39 | |||
| 4d43a70a38 | |||
| 5b35e968f3 | |||
| f3b0b33dc5 | |||
| aca0c93461 | |||
| eed7d7bbc9 | |||
| 1073f8db40 | |||
| e45695d4a3 | |||
| 7ce0aefea4 | |||
| 3ad99b241c | |||
| b25d38dd72 | |||
| 1720ac0fc6 | |||
| 0929e5f0ff | |||
| 36787d9cf7 | |||
| 5afa3251dd | |||
| 8ca0d86c85 | |||
| ebf5292bf9 | |||
| f9062ab473 | |||
| d14f3b39a7 | |||
| fd0529748a | |||
| 9d3a7bd08e | |||
| 1cf6ae37eb | |||
| 66da530ba3 | |||
| e9167f7f58 | |||
| 4494a27fab | |||
| 63833b07dd | |||
| da4d0b2bae | |||
| 027168c23a | |||
| 50435546c5 | |||
| 9018ee5d5e | |||
| 12447677cf | |||
| 6106737281 | |||
| 8250b67a9f | |||
| db5c6f2556 | |||
| 9dd7898792 | |||
| 92b3e8311f | |||
| 4a7763e497 | |||
| 9767ba1853 | |||
| 2c75326021 | |||
| 9023daba24 | |||
| 89d0e45d62 | |||
| a34099724e | |||
| 8c5498fad1 | |||
| 2c746b3361 | |||
| 954cd6adca | |||
| 8432735867 | |||
| d67fbbf94f | |||
| 39901e1d60 | |||
| 9df60276a9 | |||
| 6a9ba61242 | |||
| 848c44bd0b | |||
| 9f84706eae | |||
| 841344f1e7 | |||
| 25cfbec072 | |||
| f23e33c082 | |||
| d22b374208 | |||
| d6e867d4d1 | |||
| a7344435a5 | |||
| 206523344f | |||
| b0e69c4b8a | |||
| aa5ebb817a | |||
| 8e340d9598 | |||
| 4296b23087 | |||
| 452f8cef79 | |||
| 39065377b5 | |||
| d83d6d54ed | |||
| a2b41ea3e6 | |||
| 4481b9e46c | |||
| e5d6f969be | |||
| ded9dec4f0 | |||
| 89b738f8fa | |||
| 4f88982584 | |||
| bcd71b56e9 | |||
| 5c7b451e17 | |||
| b73c039070 | |||
| 53a69a228a | |||
| eda3a7a305 | |||
| f1ca2de515 | |||
| 27507ab192 | |||
| 6db934853d | |||
| 792520d313 | |||
| 8b9cfcde78 | |||
| 5ba3b3b309 | |||
| 63a29f421a | |||
| e20c4f8752 | |||
| ab4dda5427 | |||
| 5943808746 | |||
| ed57ee64e8 | |||
| 9c298e4851 | |||
| 8b794390e5 | |||
| a6c2348127 | |||
| 6681d179dc | |||
| 2146f71b45 | |||
| 3b93914612 | |||
| b4700a52e3 | |||
| 368066a966 | |||
| e2262085e5 | |||
| 5e2b43adb5 | |||
| c7dca16300 | |||
| 9d5bd256be | |||
| e9c2afec56 | |||
| 4e33281337 | |||
| ac559f86c9 | |||
| 117766562b | |||
| 5d76e940c7 | |||
| 1670ee87fb | |||
| a8ae6de213 | |||
| d7c4a89106 | |||
| e7354f4768 | |||
| 18ec338d4c | |||
| 7f97e2f030 | |||
| a278753bbf | |||
| 3004703074 | |||
| 67680f5536 | |||
| d8bee0b4d9 | |||
| a4f185fd28 | |||
| cf562ef8c8 | |||
| 820e0dfe51 | |||
| 0b16a473da | |||
| dbd90f270e | |||
| 7420439ed2 | |||
| 352207e933 | |||
| 5eb2874a13 | |||
| dca77ee13e | |||
| 0d22106416 | |||
| 88b53eb886 | |||
| b72ee69eef | |||
| 8a020d12e1 | |||
| a945518404 | |||
| 6cdd20e820 | |||
| d87bfcbe55 | |||
| 0df4f7dd24 | |||
| 7df3a1ab67 | |||
| c4fbb05c1b | |||
| d503aeaf77 | |||
| db0562c7b8 | |||
| ce22dd681a | |||
| a129572749 | |||
| 2d0b86f2e4 | |||
| ffac598fab | |||
| 4a76ed12e7 | |||
| 04b1e4f859 | |||
| 8c9d725eeb | |||
| de27361ffa | |||
| 8da61274b8 | |||
| 146c2d944a | |||
| d90a111c1d | |||
| d26df1c248 | |||
| 42cdb557a0 | |||
| 7c3ceeeaef | |||
| 391facbedf | |||
| 1c9f3ac7d4 | |||
| 6852047ef1 | |||
| a2109e4a78 | |||
| 01f0d67875 | |||
| 33d9b4027b | |||
| 9ed536c990 | |||
| 119d96cdc1 | |||
| 94ca4f98dd | |||
| b601b24f52 | |||
| b40d8557cf | |||
| a34604cfc7 | |||
| 1c1a8f21b2 | |||
| 78ffb1b846 | |||
| 92fd039440 | |||
| 55bf904689 | |||
| abab78730d | |||
| b855e33327 | |||
| 32336859f8 | |||
| 94c639d225 | |||
| 2ea491b1d0 | |||
| 9bc1c0c70b | |||
| 870f5fa907 | |||
| 06404bf3e8 | |||
| 3b4ed70cf3 | |||
| d45ca67498 | |||
| 1c0dac77d7 | |||
| 65e70650dc | |||
| 38fb0b28b6 | |||
| bda8fe2fbe | |||
| 6a8c7c0727 | |||
| 69fd502db3 | |||
| fcc393e2f0 | |||
| cad58b9b57 | |||
| 78cb60b85f | |||
| a08a7030d1 | |||
| 87203bbe25 | |||
| 11acb50cf7 | |||
| 23f2a04ed7 | |||
| 1805261263 | |||
| 53de779a0a | |||
| b7e91466f5 | |||
| 4c1b89576c | |||
| a1a554a29d | |||
| fdb1f961b3 | |||
| 293b9af4a7 | |||
| 029f7e0378 | |||
| 58c5e25b9a | |||
| 25e72f7760 | |||
| 7613410855 | |||
| c40cb7dc56 | |||
| 9ae5f306bf | |||
| 6159cb536f | |||
| ae4f687754 | |||
| ce7d0572fe | |||
| 030b768eeb | |||
| f36dc6cfd8 | |||
| 6dad2bf9c4 | |||
| 1475cf61ee | |||
| ebc003d478 | |||
| 543169c893 | |||
| d5769519b2 | |||
| 5974fb4cfd | |||
| eef5b78eac | |||
| 6b86f81c4a | |||
| a490443630 | |||
| b5cdf3b2c7 | |||
| 6886e5f90a | |||
| 9ac40dcf04 | |||
| a5a5b93b64 | |||
| cb39f26f27 | |||
| 8d7b2575ba | |||
| d7498b0c03 | |||
| 59051ba425 | |||
| d7268af555 | |||
| 8c1deb4064 | |||
| 986102401b | |||
| 196762d9d3 | |||
| 350a6f2bf5 | |||
| 3de3b2f734 | |||
| 4698ec8dee | |||
| 8c6181db9f | |||
| 64d3836645 | |||
| 87e2679744 | |||
| 61c814bd79 | |||
| be9a558867 | |||
| b00bfe8e0b | |||
| bc015995d8 | |||
| d01770107e | |||
| 18c359cb86 | |||
| e933ef9e1a | |||
| ea71b78e6d | |||
| 04fa10ea7b | |||
| 8e0be7cff1 | |||
| 3c13080fe1 | |||
| b6cd54d854 | |||
| f0cfad8efa | |||
| ed54d150fe | |||
| ca4887fb19 | |||
| eaf03305bd | |||
| 051908aaef | |||
| acd03e36e6 | |||
| c96b9e8d8f | |||
| 4a4a1bb6b1 | |||
| 0eebc7cef9 | |||
| 53a1fa46a8 | |||
| 86a7af3dc4 | |||
| 407c9550d7 | |||
| 2319003e10 | |||
| 1e6399c2f0 | |||
| b05d826d22 | |||
| d21989dba4 | |||
| a986355a61 | |||
| 812244d79f | |||
| be7b7c7149 | |||
| 03d7320a44 | |||
| ba4c8d8849 | |||
| 2c9b7361b7 | |||
| 8e8513e325 | |||
| 607dcff697 | |||
| 1d1cf6a827 | |||
| 61f3fab757 | |||
| e2ff7f65fc | |||
| da0792a321 | |||
| fa061ab54e | |||
| 197d65659a | |||
| 85d0afd3b0 | |||
| 20623255dd | |||
| afa5e68fe5 | |||
| 747427f816 | |||
| f99b6c6421 | |||
| 250bc54110 | |||
| bb530b87dd | |||
| 02587c1edc | |||
| 26fc20d406 | |||
| 8a829b773a | |||
| 71b98e6111 | |||
| fee13dc62f | |||
| 094e0c7592 | |||
| b57a953caa | |||
| d8ebde37ca | |||
| 0708a49848 | |||
| b26f3082e6 | |||
| cb6d424765 | |||
| 5753aa9f18 | |||
| bfae12008d | |||
| c52e0dc8ef | |||
| 55346444c9 | |||
| ba785ec58d | |||
| 3be2545ad4 | |||
| 4a8597c245 | |||
| 741cd66c9e | |||
| 7acea1ac99 | |||
| 22d771b51d | |||
| 00d6ab2eb7 | |||
| cc0a5ac3df | |||
| 5fb938d6d6 | |||
| cd5bdb837e | |||
| 107abf9d29 | |||
| 218ee30206 | |||
| 8764fbfdfa | |||
| 2463d6b92c | |||
| 810d268c99 | |||
| f5b479dbbc | |||
| b52728e829 | |||
| 8eeb452cce | |||
| 9323844ea7 | |||
| b698fe5ef3 | |||
| edb980d439 | |||
| 310bd0dbf1 | |||
| 1a96889141 | |||
| 60de05bfc1 | |||
| 3deef6197f | |||
| 70114c2145 | |||
| 5499c77e62 | |||
| 292a7f07a2 | |||
| 679f36303a | |||
| 1b61d699c4 | |||
| a7562dff68 | |||
| 8daf1e882f | |||
| 20466dacb7 | |||
| 5b3b592108 | |||
| 9952ce0069 | |||
| 1cbc8fb8ac | |||
| edc69694cb | |||
| bd15c6b8c3 | |||
| 42fe2e7266 | |||
| 44e3be2c88 | |||
| 61d5461138 | |||
| b696ca8b1c | |||
| 6a4d020c27 | |||
| 4791ba3b87 | |||
| cf1c9a3083 | |||
| d61e3fb130 | |||
| ca7d6797e1 | |||
| af0d04d0f3 | |||
| 6274567e16 | |||
| 8836ee1dda | |||
| 7afde79600 | |||
| 3d782f7162 | |||
| 058598ea58 | |||
| 8e617df914 | |||
| 7260f3e487 | |||
| a42b66e8bd | |||
| 28d556b8d4 | |||
| 33701f50ec | |||
| abfeebf67d | |||
| 3b293048f4 | |||
| ac3638f6b1 | |||
| f7ff41f1dc | |||
| f5c8ff77e9 | |||
| 87e06100df | |||
| 1ed9e0fceb | |||
| b9d6aaebbe | |||
| a99780ed9d | |||
| 92765b4261 | |||
| 8b88b4a403 | |||
| 2ce6720a6e | |||
| 772ccc9607 | |||
| 7724ff8176 | |||
| 2a3d486126 | |||
| 932e383051 | |||
| dbe3516204 | |||
| 6fc9f1b405 | |||
| 370c17e48c | |||
| 92da73aa9d | |||
| 0c65daaa7d | |||
| b46acd7f63 | |||
| e3a9aa4296 | |||
| 3886021ba5 | |||
| 4d3ebaf984 | |||
| 9131e842a5 | |||
| 4f9cfa6542 | |||
| a1a8645294 | |||
| b95b289a8c | |||
| 425554077f | |||
| 7b2f286210 | |||
| 0f893fab47 | |||
| 12c615e2e6 | |||
| 6e5ccd7c85 | |||
| 3ec500bdbb | |||
| 8f75feac94 | |||
| 448a7798f7 | |||
| dc3a228d51 | |||
| 554c97a8cb | |||
| fd1941dfbe | |||
| 5fd348dcfc | |||
| 1a6403b2fd | |||
| 55acf7283c | |||
| acb7d604fd | |||
| 6e761cb7ae | |||
| ae8d784e36 | |||
| c716dd273c | |||
| eb3c4a7a9f | |||
| 97a901a229 | |||
| e054c5bb7b | |||
| 0d87279b2f | |||
| f8638839c0 | |||
| 2f70b05c64 | |||
| e02d09a731 | |||
| 0ff491905f | |||
| 8c59254488 | |||
| 9e76e42e3c | |||
| e376a13025 | |||
| 3a086e9cc2 | |||
| fb82b2ced5 | |||
| be0514c5c9 | |||
| 046be63253 | |||
| a908453aa4 | |||
| 7a77ead423 | |||
| b89cc22896 | |||
| 6a7418e1af | |||
| df3ad2b223 | |||
| 8c870c19a6 | |||
| 83a1dc93d8 | |||
| 2b7fbbb7b7 | |||
| 2c878c47ed | |||
| 8cc69f2a95 | |||
| 7bb9e0af4c | |||
| fbe2989f61 | |||
| c3fa4552aa | |||
| 2cf327ba01 | |||
| dd0881f32b | |||
| 41c358147c | |||
| 976eb24883 | |||
| c782716e49 | |||
| f18209af7d | |||
| 6d63f84a75 | |||
| 1c6ea36e73 | |||
| 06f29306cd | |||
| 322f909edb | |||
| a37da2b165 | |||
| 710f66b0bb | |||
| 0b2b6c8e12 | |||
| acea15161c | |||
| c76283a2b4 | |||
| ae374c0463 | |||
| 42246f986c | |||
| 80f387214c | |||
| a368fbbe9b | |||
| 32df01a9da | |||
| 6dcf3a4cbc | |||
| 96f11de7ab | |||
| d03469b6d3 | |||
| 0cf6ce9aeb | |||
| e8f55cc911 | |||
| 35cee13175 | |||
| 5376df5439 | |||
| db9b6541d0 | |||
| 5bd4028ea7 | |||
| 1b2a96f96b | |||
| d50fee4fa0 | |||
| 6b3e2b020b | |||
| 6624a24040 | |||
| 7b81cf8aa6 | |||
| 26f5f80811 | |||
| 29f4650e25 | |||
| a14aa0322c |
@@ -0,0 +1,13 @@
|
||||
images:
|
||||
- name: cloudflared
|
||||
dockerfile: Dockerfile.$ARCH
|
||||
context: .
|
||||
versions:
|
||||
- latest
|
||||
registries:
|
||||
- name: docker.io/cloudflare
|
||||
user: env:DOCKER_USER
|
||||
password: env:DOCKER_PASSWORD
|
||||
architectures:
|
||||
- amd64
|
||||
- arm64
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: "\U0001F41B Bug report"
|
||||
about: Create a report to help us improve cloudflared
|
||||
title: "\U0001F41B"
|
||||
labels: 'Priority: Normal, Type: Bug'
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Configure '...'
|
||||
2. Run '....'
|
||||
3. See error
|
||||
|
||||
If it's an issue with Cloudflare Tunnel:
|
||||
4. Tunnel ID :
|
||||
5. cloudflared config:
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Environment and versions**
|
||||
- OS: [e.g. MacOS]
|
||||
- Architecture: [e.g. AMD, ARM]
|
||||
- Version: [e.g. 2022.02.0]
|
||||
|
||||
**Logs and errors**
|
||||
If applicable, add logs or errors to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: "\U0001F4DD Documentation"
|
||||
about: Request new or updated documentation for cloudflared
|
||||
title: "\U0001F4DD"
|
||||
labels: 'Priority: Normal, Type: Documentation'
|
||||
|
||||
---
|
||||
|
||||
**Available Documentation**
|
||||
A link to the documentation that is available today and the areas which could be improved.
|
||||
|
||||
**Suggested Documentation**
|
||||
A clear and concise description of the documentation, tutorial, or guide that should be added.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the documentation request here.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: "\U0001F4A1 Feature request"
|
||||
about: Suggest a feature or enhancement for cloudflared
|
||||
title: "\U0001F4A1"
|
||||
labels: 'Priority: Normal, Type: Feature Request'
|
||||
|
||||
---
|
||||
|
||||
**Describe the feature you'd like**
|
||||
A clear and concise description of the feature. What problem does it solve for you?
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
Are there any alternatives to solving this problem? If so, what was your experience with them?
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,18 @@
|
||||
on: [push, pull_request]
|
||||
name: Check
|
||||
jobs:
|
||||
check:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Test
|
||||
run: make test
|
||||
+11
-8
@@ -1,16 +1,19 @@
|
||||
.GOPATH/
|
||||
bin/
|
||||
tmp/
|
||||
guide/public
|
||||
/.GOPATH
|
||||
/tmp
|
||||
/bin
|
||||
.idea
|
||||
.build
|
||||
.vscode
|
||||
\#*\#
|
||||
cscope.*
|
||||
cloudflared
|
||||
cloudflared.exe
|
||||
!cmd/cloudflared/
|
||||
/cloudflared
|
||||
/cloudflared.pkg
|
||||
/cloudflared.exe
|
||||
/cloudflared.msi
|
||||
/cloudflared-x86-64*
|
||||
/cloudflared.1
|
||||
/packaging
|
||||
.DS_Store
|
||||
*-session.log
|
||||
ssh_server_tests/.env
|
||||
/.cover
|
||||
built_artifacts/
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# uninstall first in case this is an upgrade
|
||||
/usr/local/bin/cloudflared service uninstall
|
||||
|
||||
# install the new service using launchctl
|
||||
/usr/local/bin/cloudflared service install
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
/usr/local/bin/cloudflared service uninstall
|
||||
rm /usr/local/bin/cloudflared
|
||||
pkgutil --forget com.cloudflare.cloudflared
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$(uname)" != "Darwin" ]] ; then
|
||||
echo "This should be run on macOS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
go version
|
||||
export GO111MODULE=on
|
||||
|
||||
# build 'cloudflared-darwin-amd64.tgz'
|
||||
mkdir -p artifacts
|
||||
FILENAME="$(pwd)/artifacts/cloudflared-darwin-amd64.tgz"
|
||||
PKGNAME="$(pwd)/artifacts/cloudflared-amd64.pkg"
|
||||
TARGET_DIRECTORY=".build"
|
||||
BINARY_NAME="cloudflared"
|
||||
VERSION=$(git describe --tags --always --dirty="-dev")
|
||||
PRODUCT="cloudflared"
|
||||
CODE_SIGN_PRIV="code_sign.p12"
|
||||
CODE_SIGN_CERT="code_sign.cer"
|
||||
INSTALLER_PRIV="installer.p12"
|
||||
INSTALLER_CERT="installer.cer"
|
||||
BUNDLE_ID="com.cloudflare.cloudflared"
|
||||
SEC_DUP_MSG="security: SecKeychainItemImport: The specified item already exists in the keychain."
|
||||
export PATH="$PATH:/usr/local/bin"
|
||||
mkdir -p ../src/github.com/cloudflare/
|
||||
cp -r . ../src/github.com/cloudflare/cloudflared
|
||||
cd ../src/github.com/cloudflare/cloudflared
|
||||
GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared
|
||||
|
||||
# Add code signing private key to the key chain
|
||||
if [[ ! -z "$CFD_CODE_SIGN_KEY" ]]; then
|
||||
if [[ ! -z "$CFD_CODE_SIGN_PASS" ]]; then
|
||||
# write private key to disk and then import it keychain
|
||||
echo -n -e ${CFD_CODE_SIGN_KEY} | base64 -D > ${CODE_SIGN_PRIV}
|
||||
out=$(security import ${CODE_SIGN_PRIV} -A -P "${CFD_CODE_SIGN_PASS}" 2>&1)
|
||||
exitcode=$?
|
||||
if [ -n "$out" ]; then
|
||||
if [ $exitcode -eq 0 ]; then
|
||||
echo "$out"
|
||||
else
|
||||
if [ "$out" != "${SEC_DUP_MSG}" ]; then
|
||||
echo "$out" >&2
|
||||
exit $exitcode
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
rm ${CODE_SIGN_PRIV}
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add code signing certificate to the key chain
|
||||
if [[ ! -z "$CFD_CODE_SIGN_CERT" ]]; then
|
||||
# write certificate to disk and then import it keychain
|
||||
echo -n -e ${CFD_CODE_SIGN_CERT} | base64 -D > ${CODE_SIGN_CERT}
|
||||
out1=$(security import ${CODE_SIGN_CERT} -A 2>&1)
|
||||
exitcode1=$?
|
||||
if [ -n "$out1" ]; then
|
||||
if [ $exitcode1 -eq 0 ]; then
|
||||
echo "$out1"
|
||||
else
|
||||
if [ "$out1" != "${SEC_DUP_MSG}" ]; then
|
||||
echo "$out1" >&2
|
||||
exit $exitcode1
|
||||
else
|
||||
echo "already imported code signing certificate"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
rm ${CODE_SIGN_CERT}
|
||||
fi
|
||||
|
||||
# Add package signing private key to the key chain
|
||||
if [[ ! -z "$CFD_INSTALLER_KEY" ]]; then
|
||||
if [[ ! -z "$CFD_INSTALLER_PASS" ]]; then
|
||||
# write private key to disk and then import it into the keychain
|
||||
echo -n -e ${CFD_INSTALLER_KEY} | base64 -D > ${INSTALLER_PRIV}
|
||||
out2=$(security import ${INSTALLER_PRIV} -A -P "${CFD_INSTALLER_PASS}" 2>&1)
|
||||
exitcode2=$?
|
||||
if [ -n "$out2" ]; then
|
||||
if [ $exitcode2 -eq 0 ]; then
|
||||
echo "$out2"
|
||||
else
|
||||
if [ "$out2" != "${SEC_DUP_MSG}" ]; then
|
||||
echo "$out2" >&2
|
||||
exit $exitcode2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
rm ${INSTALLER_PRIV}
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add package signing certificate to the key chain
|
||||
if [[ ! -z "$CFD_INSTALLER_CERT" ]]; then
|
||||
# write certificate to disk and then import it keychain
|
||||
echo -n -e ${CFD_INSTALLER_CERT} | base64 -D > ${INSTALLER_CERT}
|
||||
out3=$(security import ${INSTALLER_CERT} -A 2>&1)
|
||||
exitcode3=$?
|
||||
if [ -n "$out3" ]; then
|
||||
if [ $exitcode3 -eq 0 ]; then
|
||||
echo "$out3"
|
||||
else
|
||||
if [ "$out3" != "${SEC_DUP_MSG}" ]; then
|
||||
echo "$out3" >&2
|
||||
exit $exitcode3
|
||||
else
|
||||
echo "already imported installer certificate"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
rm ${INSTALLER_CERT}
|
||||
fi
|
||||
|
||||
# get the code signing certificate name
|
||||
if [[ ! -z "$CFD_CODE_SIGN_NAME" ]]; then
|
||||
CODE_SIGN_NAME="${CFD_CODE_SIGN_NAME}"
|
||||
else
|
||||
if [[ -n "$(security find-certificate -c "Developer ID Application" | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)" ]]; then
|
||||
CODE_SIGN_NAME=$(security find-certificate -c "Developer ID Application" | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)
|
||||
else
|
||||
CODE_SIGN_NAME=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# get the package signing certificate name
|
||||
if [[ ! -z "$CFD_INSTALLER_NAME" ]]; then
|
||||
PKG_SIGN_NAME="${CFD_INSTALLER_NAME}"
|
||||
else
|
||||
if [[ -n "$(security find-certificate -c "Developer ID Installer" | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)" ]]; then
|
||||
PKG_SIGN_NAME=$(security find-certificate -c "Developer ID Installer" | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)
|
||||
else
|
||||
PKG_SIGN_NAME=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# sign the cloudflared binary
|
||||
if [[ ! -z "$CODE_SIGN_NAME" ]]; then
|
||||
codesign -s "${CODE_SIGN_NAME}" -f -v --timestamp --options runtime ${BINARY_NAME}
|
||||
|
||||
# notarize the binary
|
||||
if [[ ! -z "$CFD_NOTE_PASSWORD" ]]; then
|
||||
zip "${BINARY_NAME}.zip" ${BINARY_NAME}
|
||||
xcrun altool --notarize-app -f "${BINARY_NAME}.zip" -t osx -u ${CFD_NOTE_USERNAME} -p ${CFD_NOTE_PASSWORD} --primary-bundle-id ${BUNDLE_ID}
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# creating build directory
|
||||
rm -rf $TARGET_DIRECTORY
|
||||
mkdir "${TARGET_DIRECTORY}"
|
||||
mkdir "${TARGET_DIRECTORY}/contents"
|
||||
cp -r ".mac_resources/scripts" "${TARGET_DIRECTORY}/scripts"
|
||||
|
||||
# copy cloudflared into the build directory
|
||||
cp ${BINARY_NAME} "${TARGET_DIRECTORY}/contents/${PRODUCT}"
|
||||
|
||||
# compress cloudflared into a tar and gzipped file
|
||||
tar czf "$FILENAME" "${BINARY_NAME}"
|
||||
|
||||
# build the installer package
|
||||
if [[ ! -z "$PKG_SIGN_NAME" ]]; then
|
||||
pkgbuild --identifier com.cloudflare.${PRODUCT} \
|
||||
--version ${VERSION} \
|
||||
--scripts ${TARGET_DIRECTORY}/scripts \
|
||||
--root ${TARGET_DIRECTORY}/contents \
|
||||
--install-location /usr/local/bin \
|
||||
--sign "${PKG_SIGN_NAME}" \
|
||||
${PKGNAME}
|
||||
|
||||
# notarize the package
|
||||
if [[ ! -z "$CFD_NOTE_PASSWORD" ]]; then
|
||||
xcrun altool --notarize-app -f ${PKGNAME} -t osx -u ${CFD_NOTE_USERNAME} -p ${CFD_NOTE_PASSWORD} --primary-bundle-id ${BUNDLE_ID}
|
||||
xcrun stapler staple ${PKGNAME}
|
||||
fi
|
||||
else
|
||||
pkgbuild --identifier com.cloudflare.${PRODUCT} \
|
||||
--version ${VERSION} \
|
||||
--scripts ${TARGET_DIRECTORY}/scripts \
|
||||
--root ${TARGET_DIRECTORY}/contents \
|
||||
--install-location /usr/local/bin \
|
||||
${PKGNAME}
|
||||
fi
|
||||
|
||||
|
||||
# cleaning up the build directory
|
||||
rm -rf $TARGET_DIRECTORY
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
|
||||
echo "Skipping public release for an untagged commit."
|
||||
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${HOMEBREW_GITHUB_API_TOKEN:-}" == "" ]] ; then
|
||||
echo "Missing GITHUB_API_TOKEN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# "install" Homebrew
|
||||
git clone https://github.com/Homebrew/brew tmp/homebrew
|
||||
eval "$(tmp/homebrew/bin/brew shellenv)"
|
||||
brew update --force --quiet
|
||||
chmod -R go-w "$(brew --prefix)/share/zsh"
|
||||
|
||||
git config --global user.name "cloudflare-warp-bot"
|
||||
git config --global user.email "warp-bot@cloudflare.com"
|
||||
|
||||
# bump formula pr
|
||||
brew bump-formula-pr cloudflared --version="$VERSION" --no-browse
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FILENAME="${PWD}/artifacts/cloudflared-darwin-amd64.tgz"
|
||||
|
||||
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
|
||||
echo "Skipping public release for an untagged commit."
|
||||
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$FILENAME" ]] ; then
|
||||
echo "Missing $FILENAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${GITHUB_PRIVATE_KEY:-}" == "" ]] ; then
|
||||
echo "Missing GITHUB_PRIVATE_KEY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# upload to s3 bucket for use by Homebrew formula
|
||||
s3cmd \
|
||||
--acl-public --signature-v2 --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
|
||||
put "$FILENAME" "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
||||
s3cmd \
|
||||
--acl-public --signature-v2 --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
|
||||
cp "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz" "s3://cftunnel-docs/dl/cloudflared-stable-darwin-amd64.tgz"
|
||||
SHA256=$(sha256sum "$FILENAME" | cut -b1-64)
|
||||
|
||||
# set up git (note that UserKnownHostsFile is an absolute path so we can cd wherever)
|
||||
mkdir -p tmp
|
||||
ssh-keyscan -t rsa github.com > tmp/github.txt
|
||||
echo "$GITHUB_PRIVATE_KEY" > tmp/private.key
|
||||
chmod 0400 tmp/private.key
|
||||
export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=$PWD/tmp/github.txt -i $PWD/tmp/private.key -o IdentitiesOnly=yes"
|
||||
|
||||
# clone Homebrew repo into tmp/homebrew-cloudflare
|
||||
git clone git@github.com:cloudflare/homebrew-cloudflare.git tmp/homebrew-cloudflare
|
||||
cd tmp/homebrew-cloudflare
|
||||
git checkout -f master
|
||||
git reset --hard origin/master
|
||||
|
||||
# modify cloudflared.rb
|
||||
URL="https://packages.argotunnel.com/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
||||
tee cloudflared.rb <<EOF
|
||||
class Cloudflared < Formula
|
||||
desc 'Cloudflare Tunnel'
|
||||
homepage 'https://developers.cloudflare.com/cloudflare-one/connections/connect-apps'
|
||||
url '$URL'
|
||||
sha256 '$SHA256'
|
||||
version '$VERSION'
|
||||
def install
|
||||
bin.install 'cloudflared'
|
||||
end
|
||||
end
|
||||
EOF
|
||||
|
||||
# push cloudflared.rb
|
||||
git add cloudflared.rb
|
||||
git diff
|
||||
git config user.name "cloudflare-warp-bot"
|
||||
git config user.email "warp-bot@cloudflare.com"
|
||||
git commit -m "Release Cloudflare Tunnel $VERSION"
|
||||
|
||||
git push -v origin master
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
## 2022.9.0
|
||||
### New Features
|
||||
- cloudflared now rejects ingress rules with invalid http status codes for http_status.
|
||||
|
||||
## 2022.8.1
|
||||
### New Features
|
||||
- cloudflared now remembers if it connected to a certain protocol successfully. If it did, it does not fall back to a lower
|
||||
protocol on connection failures.
|
||||
|
||||
## 2022.7.1
|
||||
### New Features
|
||||
- It is now possible to connect cloudflared tunnel to Cloudflare Global Network with IPv6. See `cloudflared tunnel --help` and look for `edge-ip-version` for more information. For now, the default behavior is to still connect with IPv4 only.
|
||||
|
||||
### Bug Fixes
|
||||
- Several bug fixes related with QUIC transport (used between cloudflared tunnel and Cloudflare Global Network). Updating to this version is highly recommended.
|
||||
|
||||
## 2022.4.0
|
||||
### Bug Fixes
|
||||
- `cloudflared tunnel run` no longer logs the Tunnel token or JSON credentials in clear text as those are the secret
|
||||
that allows to run the Tunnel.
|
||||
|
||||
## 2022.3.4
|
||||
### New Features
|
||||
- It is now possible to retrieve the credentials that allow to run a Tunnel in case you forgot/lost them. This is
|
||||
achievable with: `cloudflared tunnel token --cred-file /path/to/file.json TUNNEL`. This new feature only works for
|
||||
Tunnels created with cloudflared version 2022.3.0 or more recent.
|
||||
|
||||
### Bug Fixes
|
||||
- `cloudflared service install` now starts the underlying agent service on Linux operating system (similarly to the
|
||||
behaviour in Windows and MacOS).
|
||||
|
||||
## 2022.3.3
|
||||
### Bug Fixes
|
||||
- `cloudflared service install` now starts the underlying agent service on Windows operating system (similarly to the
|
||||
behaviour in MacOS).
|
||||
|
||||
## 2022.3.1
|
||||
### Bug Fixes
|
||||
- Various fixes to the reliability of `quic` protocol, including an edge case that could lead to cloudflared crashing.
|
||||
|
||||
## 2022.3.0
|
||||
### New Features
|
||||
- It is now possible to configure Ingress Rules to point to an origin served by unix socket with either HTTP or HTTPS.
|
||||
If the origin starts with `unix:/` then we assume HTTP (existing behavior). Otherwise, the origin can start with
|
||||
`unix+tls:/` for HTTPS.
|
||||
|
||||
## 2022.2.1
|
||||
### New Features
|
||||
- This project now has a new LICENSE that is more compliant with open source purposes.
|
||||
|
||||
### Bug Fixes
|
||||
- Various fixes to the reliability of `quic` protocol.
|
||||
|
||||
## 2022.1.3
|
||||
### New Features
|
||||
- New `cloudflared tunnel vnet` commands to allow for private routing to be virtualized. This means that the same CIDR
|
||||
can now be used to point to two different Tunnels with `cloudflared tunnel route ip` command. More information will be
|
||||
made available on blog.cloudflare.com and developers.cloudflare.com/cloudflare-one once the feature is globally available.
|
||||
|
||||
### Bug Fixes
|
||||
- Correctly handle proxying UDP datagrams with no payload.
|
||||
- Bug fix for origins that use Server-Sent Events (SSE).
|
||||
|
||||
## 2022.1.0
|
||||
### Improvements
|
||||
- If a specific `protocol` property is defined (e.g. for `quic`), cloudflared no longer falls back to an older protocol
|
||||
(such as `http2`) in face of connectivity errors. This is important because some features are only supported in a specific
|
||||
protocol (e.g. UDP proxying only works for `quic`). Hence, if a user chooses a protocol, cloudflared now adheres to it
|
||||
no matter what.
|
||||
|
||||
### Bug Fixes
|
||||
- Stopping cloudflared running with `quic` protocol now respects graceful shutdown.
|
||||
|
||||
## 2021.12.2
|
||||
### Bug Fixes
|
||||
- Fix logging when `quic` transport is used and UDP traffic is proxied.
|
||||
- FIPS compliant cloudflared binaries will now be released as separate artifacts. Recall that these are only for linux
|
||||
and amd64.
|
||||
|
||||
## 2021.12.1
|
||||
### Bug Fixes
|
||||
- Fixes Github issue #530 where cloudflared 2021.12.0 could not reach origins that were HTTPS and using certain encryption
|
||||
methods forbidden by FIPS compliance (such as Let's Encrypt certificates). To address this fix we have temporarily reverted
|
||||
FIPS compliance from amd64 linux binaries that was recently introduced (or fixed actually as it was never working before).
|
||||
|
||||
## 2021.12.0
|
||||
### New Features
|
||||
- Cloudflared binary released for amd64 linux is now FIPS compliant.
|
||||
|
||||
### Improvements
|
||||
- Logging about connectivity to Cloudflare edge now only yields `ERR` level logging if there are no connections to
|
||||
Cloudflare edge that are active. Otherwise it logs `WARN` level.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixes Github issue #501.
|
||||
|
||||
## 2021.11.0
|
||||
### Improvements
|
||||
- Fallback from `protocol:quic` to `protocol:http2` immediately if UDP connectivity isn't available. This could be because of a firewall or
|
||||
egress rule.
|
||||
|
||||
## 2021.10.4
|
||||
### Improvements
|
||||
- Collect quic transport metrics on RTT, packets and bytes transferred.
|
||||
|
||||
### Bug Fixes
|
||||
- Fix race condition that was writing to the connection after the http2 handler returns.
|
||||
|
||||
## 2021.9.2
|
||||
|
||||
### New features
|
||||
- `cloudflared` can now run with `quic` as the underlying tunnel transport protocol. To try it, change or add "protocol: quic" to your config.yml file or
|
||||
run cloudflared with the `--protocol quic` flag. e.g:
|
||||
`cloudflared tunnel --protocol quic run <tunnel-name>`
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed some generic transport bugs in `quic` mode. It's advised to upgrade to at least this version (2021.9.2) when running `cloudflared`
|
||||
with `quic` protocol.
|
||||
- `cloudflared` docker images will now show version.
|
||||
|
||||
|
||||
## 2021.8.4
|
||||
### Improvements
|
||||
- Temporary tunnels (those hosted on trycloudflare.com that do not require a Cloudflare login) now run as Named Tunnels
|
||||
underneath. We recall that these tunnels should not be relied upon for production usage as they come with no guarantee
|
||||
of uptime. Previous cloudflared versions will soon be unable to run legacy temporary tunnels and will require an update
|
||||
(to this version or more recent).
|
||||
|
||||
## 2021.8.2
|
||||
### Improvements
|
||||
- Because Equinox os shutting down, all cloudflared releases are now present [here](https://github.com/cloudflare/cloudflared/releases).
|
||||
[Equinox](https://dl.equinox.io/cloudflare/cloudflared/stable) will no longer receive updates.
|
||||
|
||||
## 2021.8.0
|
||||
### Bug fixes
|
||||
- Prevents tunnel from accidentally running when only proxy-dns should run.
|
||||
|
||||
### Improvements
|
||||
- If auto protocol transport lookup fails, we now default to a transport instead of not connecting.
|
||||
|
||||
## 2021.6.0
|
||||
### Bug Fixes
|
||||
- Fixes a http2 transport (the new default for Named Tunnels) to work with unix socket origins.
|
||||
|
||||
|
||||
## 2021.5.10
|
||||
### Bug Fixes
|
||||
- Fixes a memory leak in h2mux transport that connects cloudflared to Cloudflare edge.
|
||||
|
||||
|
||||
## 2021.5.9
|
||||
### New Features
|
||||
- Uses new Worker based login helper service to facilitate token exchange in cloudflared flows.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixes Centos-7 builds.
|
||||
|
||||
## 2021.5.8
|
||||
### New Features
|
||||
- When creating a DNS record to point a hostname at a tunnel, you can now use --overwrite-dns to overwrite any existing
|
||||
DNS records with that hostname. This works both when using the CLI to provision DNS, as well as when starting an adhoc
|
||||
named tunnel, e.g.:
|
||||
- `cloudflared tunnel route dns --overwrite-dns foo-tunnel foo.example.com`
|
||||
- `cloudflared tunnel --overwrite-dns --name foo-tunnel --hostname foo.example.com`
|
||||
|
||||
## 2021.5.7
|
||||
### New Features
|
||||
- Named Tunnels will automatically select the protocol to connect to Cloudflare's edge network.
|
||||
|
||||
## 2021.5.0
|
||||
|
||||
### New Features
|
||||
- It is now possible to run the same tunnel using more than one `cloudflared` instance. This is a server-side change and
|
||||
is compatible with any client version that uses Named Tunnels.
|
||||
|
||||
To get started, visit our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/deploy-cloudflared-replicas).
|
||||
- `cloudflared tunnel ingress validate` will now warn about unused keys in your config file. This is helpful for
|
||||
detecting typos in your config.
|
||||
- If `cloudflared` detects it is running inside a Linux container, it will limit itself to use only the number of CPUs
|
||||
the pod has been granted, instead of trying to use every CPU available.
|
||||
|
||||
## 2021.4.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed proxying of websocket requests to avoid possibility of losing initial frames that were sent in the same TCP
|
||||
packet as response headers [#345](https://github.com/cloudflare/cloudflared/issues/345).
|
||||
- `proxy-dns` option now works in conjunction with running a named tunnel [#346](https://github.com/cloudflare/cloudflared/issues/346).
|
||||
|
||||
## 2021.3.6
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Reverted 2021.3.5 improvement to use HTTP/2 in a best-effort manner between cloudflared and origin services because
|
||||
it was found to break in some cases.
|
||||
|
||||
## 2021.3.5
|
||||
|
||||
### Improvements
|
||||
|
||||
- HTTP/2 transport is now always chosen if origin server supports it and the service url scheme is HTTPS.
|
||||
This was previously done in a best attempt manner.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- The MacOS binaries were not successfully released in 2021.3.3 and 2021.3.4. This release is aimed at addressing that.
|
||||
|
||||
## 2021.3.3
|
||||
|
||||
### Improvements
|
||||
|
||||
- Tunnel create command, as well as, running ad-hoc tunnels using `cloudflared tunnel -name NAME`, will not overwrite
|
||||
existing files when writing tunnel credentials.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Tunnel create and delete commands no longer use path to credentials from the configuration file.
|
||||
If you need ot place tunnel credentials file at a specific location, you must use `--credentials-file` flag.
|
||||
- Access ssh-gen creates properly named keys for SSH short lived certs.
|
||||
|
||||
|
||||
## 2021.3.2
|
||||
|
||||
### New Features
|
||||
|
||||
- It is now possible to obtain more detailed information about the cloudflared connectors to Cloudflare Edge via
|
||||
`cloudflared tunnel info <name/uuid>`. It is possible to sort the output as well as output in different formats,
|
||||
such as: `cloudflared tunnel info --sort-by version --invert-sort --output json <name/uuid>`.
|
||||
You can obtain more information via `cloudflared tunnel info --help`.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Don't look for configuration file in default paths when `--config FILE` flag is present after `tunnel` subcommand.
|
||||
- cloudflared access token command now functions correctly with the new token-per-app change from 2021.3.0.
|
||||
|
||||
|
||||
## 2021.3.0
|
||||
|
||||
### New Features
|
||||
|
||||
- [Cloudflare One Routing](https://developers.cloudflare.com/cloudflare-one/tutorials/warp-to-tunnel) specific commands
|
||||
now show up in the `cloudflared tunnel route --help` output.
|
||||
- There is a new ingress type that allows cloudflared to proxy SOCKS5 as a bastion. You can use it with an ingress
|
||||
rule by adding `service: socks-proxy`. Traffic is routed to any destination specified by the SOCKS5 packet but only
|
||||
if allowed by a rule. In the following example we allow proxying to a certain CIDR but explicitly forbid one address
|
||||
within it:
|
||||
```
|
||||
ingress:
|
||||
- hostname: socks.example.com
|
||||
service: socks-proxy
|
||||
originRequest:
|
||||
ipRules:
|
||||
- prefix: 192.168.1.8/32
|
||||
allow: false
|
||||
- prefix: 192.168.1.0/24
|
||||
ports: [80, 443]
|
||||
allow: true
|
||||
```
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
- Nested commands, such as `cloudflared tunnel run`, now consider CLI arguments even if they appear earlier on the
|
||||
command. For instance, `cloudflared --config config.yaml tunnel run` will now behave the same as
|
||||
`cloudflared tunnel --config config.yaml run`
|
||||
- Warnings are now shown in the output logs whenever cloudflared is running without the most recent version and
|
||||
`no-autoupdate` is `true`.
|
||||
- Access tokens are now stored per Access App instead of per request path. This decreases the number of times that the
|
||||
user is required to authenticate with an Access policy redundantly.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- GitHub [PR #317](https://github.com/cloudflare/cloudflared/issues/317) was broken in 2021.2.5 and is now fixed again.
|
||||
|
||||
## 2021.2.5
|
||||
|
||||
### New Features
|
||||
|
||||
- We introduce [Cloudflare One Routing](https://developers.cloudflare.com/cloudflare-one/tutorials/warp-to-tunnel) in
|
||||
beta mode. Cloudflare customer can now connect users and private networks with RFC 1918 IP addresses via the
|
||||
Cloudflare edge network. Users running Cloudflare WARP client in the same organization can connect to the services
|
||||
made available by Argo Tunnel IP routes. Please share your feedback in the GitHub issue tracker.
|
||||
|
||||
## 2021.2.4
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Reverts the Improvement released in 2021.2.3 for CLI arguments as it introduced a regression where cloudflared failed
|
||||
to read URLs in configuration files.
|
||||
- cloudflared now logs the reason for failed connections if the error is recoverable.
|
||||
|
||||
## 2021.2.3
|
||||
|
||||
### Backward Incompatible Changes
|
||||
|
||||
- Removes db-connect. The Cloudflare Workers product will continue to support db-connect implementations with versions
|
||||
of cloudflared that predate this release and include support for db-connect.
|
||||
|
||||
### New Features
|
||||
|
||||
- Introduces support for proxy configurations with websockets in arbitrary TCP connections (#318).
|
||||
|
||||
### Improvements
|
||||
|
||||
- (reverted) Nested command line argument handling.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- The maximum number of upstream connections is now limited by default which should fix reported issues of cloudflared
|
||||
exhausting CPU usage when faced with connectivity issues.
|
||||
+10
-7
@@ -1,8 +1,13 @@
|
||||
# use a builder image for building cloudflare
|
||||
FROM golang:1.13.3 as builder
|
||||
ENV GO111MODULE=on
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
ARG TARGET_GOOS
|
||||
ARG TARGET_GOARCH
|
||||
FROM golang:1.19 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
TARGET_GOOS=${TARGET_GOOS} \
|
||||
TARGET_GOARCH=${TARGET_GOARCH}
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
|
||||
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
@@ -12,10 +17,8 @@ COPY . .
|
||||
# compile cloudflared
|
||||
RUN make cloudflared
|
||||
|
||||
# ---
|
||||
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian10:nonroot
|
||||
FROM gcr.io/distroless/base-debian11:nonroot
|
||||
|
||||
# copy our compiled binary
|
||||
COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# use a builder image for building cloudflare
|
||||
FROM golang:1.19 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
|
||||
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
# copy our sources into the builder image
|
||||
COPY . .
|
||||
|
||||
# compile cloudflared
|
||||
RUN GOOS=linux GOARCH=amd64 make cloudflared
|
||||
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian11:nonroot
|
||||
|
||||
# copy our compiled binary
|
||||
COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
|
||||
# run as non-privileged user
|
||||
USER nonroot
|
||||
|
||||
# command / entrypoint of container
|
||||
ENTRYPOINT ["cloudflared", "--no-autoupdate"]
|
||||
CMD ["version"]
|
||||
@@ -0,0 +1,27 @@
|
||||
# use a builder image for building cloudflare
|
||||
FROM golang:1.19 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
|
||||
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
# copy our sources into the builder image
|
||||
COPY . .
|
||||
|
||||
# compile cloudflared
|
||||
RUN GOOS=linux GOARCH=arm64 make cloudflared
|
||||
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian11:nonroot-arm64
|
||||
|
||||
# copy our compiled binary
|
||||
COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
|
||||
# run as non-privileged user
|
||||
USER nonroot
|
||||
|
||||
# command / entrypoint of container
|
||||
ENTRYPOINT ["cloudflared", "--no-autoupdate"]
|
||||
CMD ["version"]
|
||||
@@ -1,155 +1,202 @@
|
||||
SERVICES AGREEMENT
|
||||
|
||||
Your installation of this software is symbol of your signature indicating that
|
||||
you accept the terms of this Services Agreement (this "Agreement"). This
|
||||
Agreement is a legal agreement between you (either an individual or a single
|
||||
entity) and CloudFlare, Inc. for the services being provided to you by
|
||||
CloudFlare or its authorized representative (the "Services"), including any
|
||||
computer software and any associated media, printed materials, and "online" or
|
||||
electronic documentation provided in connection with the Services (the
|
||||
"Software" and together with the Services are hereinafter collectively referred
|
||||
to as the "Solution"). If the user is not an individual, then "you" means your
|
||||
company, its officers, members, employees, agents, representatives, successors
|
||||
and assigns. BY USING THE SOLUTION, YOU ARE INDICATING THAT YOU HAVE READ, AND
|
||||
AGREE TO BE BOUND BY, THE POLICIES, TERMS, AND CONDITIONS SET FORTH BELOW IN
|
||||
THEIR ENTIRETY WITHOUT LIMITATION OR QUALIFICATION, AS WELL AS BY ALL APPLICABLE
|
||||
LAWS AND REGULATIONS, AS IF YOU HAD HANDWRITTEN YOUR NAME ON A CONTRACT. IF YOU
|
||||
DO NOT AGREE TO THESE TERMS AND CONDITIONS, YOU MAY NOT USE THE SOLUTION.
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
1. GRANT OF RIGHTS
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1.1 Grant of License. The Solution is licensed by CloudFlare and its
|
||||
licensors, not sold. Subject to the terms and conditions of this Agreement,
|
||||
CloudFlare hereby grants you a nonexclusive, nonsublicensable, nontransferable
|
||||
license to use the Solution. You may examine source code, if provided to you,
|
||||
solely for the limited purpose of evaluating the Software for security flaws.
|
||||
You may also use the Service to create derivative works which are exclusively
|
||||
compatible with any CloudFlare product serviceand no other product or service.
|
||||
This license applies to the parts of the Solution developed by CloudFlare. The
|
||||
Solution may also incorporate externally maintained libraries and other open software.
|
||||
These resources may be governed by other licenses.
|
||||
1. Definitions.
|
||||
|
||||
1.2 Restrictions. The license granted herein is granted solely to you and
|
||||
not, by implication or otherwise, to any of your parents, subsidiaries or
|
||||
affiliates. No right is granted hereunder to use the Solution to perform
|
||||
services for third parties. All rights not expressly granted hereunder are
|
||||
reserved to CloudFlare. You may not use the Solution except as explicitly
|
||||
permitted under this Agreement. You are expressly prohibited from modifying,
|
||||
adapting, translating, preparing derivative works from, decompiling, reverse
|
||||
engineering, disassembling or otherwise attempting to derive source code from
|
||||
the Software used to provide the Services or any internal data files generated
|
||||
by the Solution. You are also prohibited from removing, obscuring or altering
|
||||
any copyright notice, trademarks, or other proprietary rights notices affixed to
|
||||
or associated with the Solution.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
1.3 Ownership. As between the parties, CloudFlare and/or its licensors own
|
||||
and shall retain all right, title, and interest in and to the Solution,
|
||||
including any and all technology embodied therein, including all copyrights,
|
||||
patents, trade secrets, trade dress and other proprietary rights associated
|
||||
therewith, and any derivative works created there from.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
2. LIMITATION OF LIABILITY
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
YOU EXPRESSLY ACKNOWLEDGE AND AGREE THAT DOWNLOADING THE SOFTWARE IS AT YOUR
|
||||
SOLE RISK. THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTY OF ANY KIND
|
||||
AND CLOUDFLARE, ITS LICENSORS AND ITS AUTHORIZED REPRESENTATIVES (TOGETHER FOR
|
||||
PURPOSES HEREOF, "CLOUDFLARE") EXPRESSLY DISCLAIM ALL WARRANTIES, EXPRESS OR
|
||||
IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. CLOUDFLARE DOES NOT
|
||||
WARRANT THAT THE FUNCTIONS CONTAINED IN THE SOFTWARE WILL MEET YOUR
|
||||
REQUIREMENTS, OR THAT THE OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR
|
||||
ERROR-FREE, OR THAT DEFECTS IN THE SOFTWARE WILL BE CORRECTED. FURTHERMORE,
|
||||
CLOUDFLARE DOES NOT WARRANT OR MAKE ANY REPRESENTATIONS REGARDING THE SOFTWARE
|
||||
OR RELATED DOCUMENTATION IN TERMS OF THEIR CORRECTNESS, ACCURACY, RELIABILITY,
|
||||
OR OTHERWISE. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY CLOUDFLARE SHALL
|
||||
CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF THIS WARRANTY.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
3. CONFIDENTIALITY
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
It may be necessary during the set up and performance of the Solution for the
|
||||
parties to exchange Confidential Information. "Confidential Information" means
|
||||
any information whether oral, or written, of a private, secret, proprietary or
|
||||
confidential nature, concerning either party or its business operations,
|
||||
including without limitation: (a) your data and (b) CloudFlare's access control
|
||||
systems, specialized network equipment and techniques related to the Solution,
|
||||
use policies, which include trade secrets of CloudFlare and its licensors. Each
|
||||
party agrees to use the same degree of care to protect the confidentiality of
|
||||
the Confidential Information of the other party and to prevent its unauthorized
|
||||
use or dissemination as it uses to protect its own Confidential Information of a
|
||||
similar nature, but in no event shall exercise less than due diligence and
|
||||
reasonable care. Each party agrees to use the Confidential Information of the
|
||||
other party only for purposes related to the performance of this Agreement. All
|
||||
Confidential Information remains the property of the party disclosing the
|
||||
information and no license or other rights to Confidential Information is
|
||||
granted or implied hereby.
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
4. TERM AND TERMINATION
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
4.1 Term. This Agreement shall be effective upon download or install of the
|
||||
Software.
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
4.2 Termination. This Agreement may be terminated by CloudFlare or its
|
||||
authorized representative by written notice to you if any of the following
|
||||
events occur: (i) you fail to pay any amounts due for the Services and the
|
||||
Solution when due and after written notice of such nonpayment has been given to
|
||||
you; (ii) you are in material breach of any term, condition, or provision of
|
||||
this Agreement or any other agreement executed by you with CloudFlare or its
|
||||
authorized representative in connection with the provision of the Solution and
|
||||
Services (a "Related Agreement"); or (iii) you terminate or suspend your
|
||||
business, becomes subject to any bankruptcy or insolvency proceeding under
|
||||
federal or state statutes, or become insolvent or subject to direct control by a
|
||||
trustee, receiver or similar authority.
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
4.3 Effect of Termination. Upon the termination of this Agreement for any
|
||||
reason: (1) all license rights granted hereunder shall terminate and (2) all
|
||||
Confidential Information shall be returned to the disclosing party or destroyed.
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
5. MISCELLANEOUS
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
5.1 Assignment. You may not assign any of your rights or delegate any of
|
||||
your obligations under this Agreement, whether by operation of law or otherwise,
|
||||
without the prior express written consent of CloudFlare or its authorized
|
||||
representative. Any such assignment without the prior express written consent
|
||||
of CloudFlare or its authorized representative shall be void. Subject to the
|
||||
foregoing, this Agreement will bind and inure to the benefit of the parties,
|
||||
their respective successors and permitted assigns.
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
5.2 Waiver and Amendment. No modification, amendment or waiver of any
|
||||
provision of this Agreement shall be effective unless in writing and signed by
|
||||
the party to be charged. No failure or delay by either party in exercising any
|
||||
right, power, or remedy under this Agreement, except as specifically provided
|
||||
herein, shall operate as a waiver of any such right, power or remedy. Without
|
||||
limiting the foregoing, terms and conditions on any purchase orders or similar
|
||||
materials submitted by you to CloudFlare or its authorized representative shall
|
||||
be of no force or effect.
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
5.3 Governing Law. This Agreement shall be governed by the laws of the State
|
||||
of California, USA, excluding conflict of laws and provisions, and excluding the
|
||||
United Nations Convention on Contracts for the International Sale of Goods.
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
5.4 Notices. All notices, demands or consents required or permitted under
|
||||
this Agreement shall be in writing. Notice shall be sent to you at the e-mail
|
||||
address provided by you to CloudFlare or its authorized representative in
|
||||
connection with the Solution.
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
5.5 Independent Contractors. The parties are independent contractors.
|
||||
Neither party shall be deemed to be an employee, agent, partner or legal
|
||||
representative of the other for any purpose and neither shall have any right,
|
||||
power or authority to create any obligation or responsibility on behalf of the
|
||||
other.
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
5.6 Severability. If any provision of this Agreement is held by a court of
|
||||
competent jurisdiction to be contrary to law, such provision shall be changed
|
||||
and interpreted so as to best accomplish the objectives of the original
|
||||
provision to the fullest extent allowed by law and the remaining provisions of
|
||||
this Agreement shall remain in full force and effect.
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
5.7 Force Majeure. CloudFlare shall not be liable to the other party for any
|
||||
failure or delay in performance caused by reasons beyond its reasonable control.
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5.8 Complete Understanding. This Agreement and the Related Agreement
|
||||
constitute the final, complete and exclusive agreement between the parties with
|
||||
respect to the subject matter hereof, and supersedes all previous written and
|
||||
oral agreements and communications related to the subject matter of this
|
||||
Agreement. To the extent this Agreement and the Related Agreement conflict,
|
||||
this Agreement shall control.
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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.
|
||||
@@ -1,23 +1,111 @@
|
||||
VERSION := $(shell git describe --tags --always --dirty="-dev")
|
||||
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC')
|
||||
VERSION_FLAGS := -ldflags='-X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
|
||||
VERSION := $(shell git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut -c2-)
|
||||
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
|
||||
#e.g. w3.0.1 or w4.2.10. It trims off the w character when creating the MSI.
|
||||
|
||||
IMPORT_PATH := github.com/cloudflare/cloudflared
|
||||
PACKAGE_DIR := $(CURDIR)/packaging
|
||||
INSTALL_BINDIR := usr/local/bin
|
||||
|
||||
EQUINOX_FLAGS = --version="$(VERSION)" \
|
||||
--platforms="$(EQUINOX_BUILD_PLATFORMS)" \
|
||||
--app="$(EQUINOX_APP_ID)" \
|
||||
--token="$(EQUINOX_TOKEN)" \
|
||||
--channel="$(EQUINOX_CHANNEL)"
|
||||
|
||||
ifeq ($(EQUINOX_IS_DRAFT), true)
|
||||
EQUINOX_FLAGS := --draft $(EQUINOX_FLAGS)
|
||||
ifeq ($(ORIGINAL_NAME), true)
|
||||
# Used for builds that want FIPS compilation but want the artifacts generated to still have the original name.
|
||||
BINARY_NAME := cloudflared
|
||||
else ifeq ($(FIPS), true)
|
||||
# Used for FIPS compliant builds that do not match the case above.
|
||||
BINARY_NAME := cloudflared-fips
|
||||
else
|
||||
# Used for all other (non-FIPS) builds.
|
||||
BINARY_NAME := cloudflared
|
||||
endif
|
||||
|
||||
ifeq ($(GOARCH),)
|
||||
GOARCH := amd64
|
||||
ifeq ($(NIGHTLY), true)
|
||||
DEB_PACKAGE_NAME := $(BINARY_NAME)-nightly
|
||||
NIGHTLY_FLAGS := --conflicts cloudflared --replaces cloudflared
|
||||
else
|
||||
DEB_PACKAGE_NAME := $(BINARY_NAME)
|
||||
endif
|
||||
|
||||
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC')
|
||||
VERSION_FLAGS := -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"
|
||||
ifdef PACKAGE_MANAGER
|
||||
VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/cmd/cloudflared/updater.BuiltForPackageManager=$(PACKAGE_MANAGER)"
|
||||
endif
|
||||
|
||||
LINK_FLAGS :=
|
||||
ifeq ($(FIPS), true)
|
||||
LINK_FLAGS := -linkmode=external -extldflags=-static $(LINK_FLAGS)
|
||||
# Prevent linking with libc regardless of CGO enabled or not.
|
||||
GO_BUILD_TAGS := $(GO_BUILD_TAGS) osusergo netgo fips
|
||||
VERSION_FLAGS := $(VERSION_FLAGS) -X "main.BuildType=FIPS"
|
||||
endif
|
||||
|
||||
LDFLAGS := -ldflags='$(VERSION_FLAGS) $(LINK_FLAGS)'
|
||||
ifneq ($(GO_BUILD_TAGS),)
|
||||
GO_BUILD_TAGS := -tags "$(GO_BUILD_TAGS)"
|
||||
endif
|
||||
|
||||
ifeq ($(debug), 1)
|
||||
GO_BUILD_TAGS += -gcflags="all=-N -l"
|
||||
endif
|
||||
|
||||
IMPORT_PATH := github.com/cloudflare/cloudflared
|
||||
PACKAGE_DIR := $(CURDIR)/packaging
|
||||
PREFIX := /usr
|
||||
INSTALL_BINDIR := $(PREFIX)/bin/
|
||||
INSTALL_MANDIR := $(PREFIX)/share/man/man1/
|
||||
|
||||
LOCAL_ARCH ?= $(shell uname -m)
|
||||
ifneq ($(GOARCH),)
|
||||
TARGET_ARCH ?= $(GOARCH)
|
||||
else ifeq ($(LOCAL_ARCH),x86_64)
|
||||
TARGET_ARCH ?= amd64
|
||||
else ifeq ($(LOCAL_ARCH),amd64)
|
||||
TARGET_ARCH ?= amd64
|
||||
else ifeq ($(LOCAL_ARCH),i686)
|
||||
TARGET_ARCH ?= amd64
|
||||
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8)
|
||||
TARGET_ARCH ?= arm64
|
||||
else ifeq ($(LOCAL_ARCH),aarch64)
|
||||
TARGET_ARCH ?= arm64
|
||||
else ifeq ($(LOCAL_ARCH),arm64)
|
||||
TARGET_ARCH ?= arm64
|
||||
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 4),armv)
|
||||
TARGET_ARCH ?= arm
|
||||
else ifeq ($(LOCAL_ARCH),s390x)
|
||||
TARGET_ARCH ?= s390x
|
||||
else
|
||||
$(error This system's architecture $(LOCAL_ARCH) isn't supported)
|
||||
endif
|
||||
|
||||
LOCAL_OS ?= $(shell go env GOOS)
|
||||
ifeq ($(LOCAL_OS),linux)
|
||||
TARGET_OS ?= linux
|
||||
else ifeq ($(LOCAL_OS),darwin)
|
||||
TARGET_OS ?= darwin
|
||||
else ifeq ($(LOCAL_OS),windows)
|
||||
TARGET_OS ?= windows
|
||||
else ifeq ($(LOCAL_OS),freebsd)
|
||||
TARGET_OS ?= freebsd
|
||||
else
|
||||
$(error This system's OS $(LOCAL_OS) isn't supported)
|
||||
endif
|
||||
|
||||
ifeq ($(TARGET_OS), windows)
|
||||
EXECUTABLE_PATH=./$(BINARY_NAME).exe
|
||||
else
|
||||
EXECUTABLE_PATH=./$(BINARY_NAME)
|
||||
endif
|
||||
|
||||
ifeq ($(FLAVOR), centos-7)
|
||||
TARGET_PUBLIC_REPO ?= el7
|
||||
else
|
||||
TARGET_PUBLIC_REPO ?= $(FLAVOR)
|
||||
endif
|
||||
|
||||
ifneq ($(TARGET_ARM), )
|
||||
ARM_COMMAND := GOARM=$(TARGET_ARM)
|
||||
endif
|
||||
|
||||
ifeq ($(TARGET_ARM), 7)
|
||||
PACKAGE_ARCH := armhf
|
||||
else
|
||||
PACKAGE_ARCH := $(TARGET_ARCH)
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
@@ -28,33 +116,148 @@ clean:
|
||||
go clean
|
||||
|
||||
.PHONY: cloudflared
|
||||
cloudflared: tunnel-deps
|
||||
go build -v -mod=vendor $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
cloudflared:
|
||||
ifeq ($(FIPS), true)
|
||||
$(info Building cloudflared with go-fips)
|
||||
cp -f fips/fips.go.linux-amd64 cmd/cloudflared/fips.go
|
||||
endif
|
||||
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) $(ARM_COMMAND) go build -v -mod=vendor $(GO_BUILD_TAGS) $(LDFLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
ifeq ($(FIPS), true)
|
||||
rm -f cmd/cloudflared/fips.go
|
||||
./check-fips.sh cloudflared
|
||||
endif
|
||||
|
||||
.PHONY: container
|
||||
container:
|
||||
docker build -t cloudflare/cloudflared:"$(VERSION)" .
|
||||
docker build --build-arg=TARGET_ARCH=$(TARGET_ARCH) --build-arg=TARGET_OS=$(TARGET_OS) -t cloudflare/cloudflared-$(TARGET_OS)-$(TARGET_ARCH):"$(VERSION)" .
|
||||
|
||||
.PHONY: test
|
||||
test: vet
|
||||
go test -v -mod=vendor -race $(VERSION_FLAGS) ./...
|
||||
ifndef CI
|
||||
go test -v -mod=vendor -race $(LDFLAGS) ./...
|
||||
else
|
||||
@mkdir -p .cover
|
||||
go test -v -mod=vendor -race $(LDFLAGS) -coverprofile=".cover/c.out" ./...
|
||||
go tool cover -html ".cover/c.out" -o .cover/all.html
|
||||
endif
|
||||
|
||||
.PHONY: test-ssh-server
|
||||
test-ssh-server:
|
||||
docker-compose -f ssh_server_tests/docker-compose.yml up
|
||||
|
||||
.PHONY: cloudflared-deb
|
||||
cloudflared-deb: cloudflared
|
||||
define publish_package
|
||||
chmod 664 $(BINARY_NAME)*.$(1); \
|
||||
for HOST in $(CF_PKG_HOSTS); do \
|
||||
ssh-keyscan -t ecdsa $$HOST >> ~/.ssh/known_hosts; \
|
||||
scp -p -4 $(BINARY_NAME)*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/$(BINARY_NAME)/; \
|
||||
done
|
||||
endef
|
||||
|
||||
.PHONY: publish-deb
|
||||
publish-deb: cloudflared-deb
|
||||
$(call publish_package,deb,apt)
|
||||
|
||||
.PHONY: publish-rpm
|
||||
publish-rpm: cloudflared-rpm
|
||||
$(call publish_package,rpm,yum)
|
||||
|
||||
cloudflared.1: cloudflared_man_template
|
||||
cat cloudflared_man_template | sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' > cloudflared.1
|
||||
|
||||
install: cloudflared cloudflared.1
|
||||
mkdir -p $(DESTDIR)$(INSTALL_BINDIR) $(DESTDIR)$(INSTALL_MANDIR)
|
||||
install -m755 cloudflared $(DESTDIR)$(INSTALL_BINDIR)/cloudflared
|
||||
install -m644 cloudflared.1 $(DESTDIR)$(INSTALL_MANDIR)/cloudflared.1
|
||||
|
||||
# When we build packages, the package name will be FIPS-aware.
|
||||
# But we keep the binary installed by it to be named "cloudflared" regardless.
|
||||
define build_package
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t deb --deb-compression bzip2 \
|
||||
-a $(GOARCH) -v $(VERSION) -n cloudflared cloudflared=/usr/local/bin/
|
||||
cp cloudflared.1 $(PACKAGE_DIR)/cloudflared.1
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t $(1) \
|
||||
--description 'Cloudflare Tunnel daemon' \
|
||||
--vendor 'Cloudflare' \
|
||||
--license 'Apache License Version 2.0' \
|
||||
--url 'https://github.com/cloudflare/cloudflared' \
|
||||
-m 'Cloudflare <support@cloudflare.com>' \
|
||||
-a $(PACKAGE_ARCH) -v $(VERSION) -n $(DEB_PACKAGE_NAME) $(NIGHTLY_FLAGS) --after-install postinst.sh --after-remove postrm.sh \
|
||||
cloudflared=$(INSTALL_BINDIR) cloudflared.1=$(INSTALL_MANDIR)
|
||||
endef
|
||||
|
||||
.PHONY: cloudflared-deb
|
||||
cloudflared-deb: cloudflared cloudflared.1
|
||||
$(call build_package,deb)
|
||||
|
||||
.PHONY: cloudflared-rpm
|
||||
cloudflared-rpm: cloudflared cloudflared.1
|
||||
$(call build_package,rpm)
|
||||
|
||||
.PHONY: cloudflared-pkg
|
||||
cloudflared-pkg: cloudflared cloudflared.1
|
||||
$(call build_package,osxpkg)
|
||||
|
||||
.PHONY: cloudflared-msi
|
||||
cloudflared-msi: cloudflared
|
||||
wixl --define Version=$(VERSION) --define Path=$(EXECUTABLE_PATH) --output cloudflared-$(VERSION)-$(TARGET_ARCH).msi cloudflared.wxs
|
||||
|
||||
.PHONY: cloudflared-darwin-amd64.tgz
|
||||
cloudflared-darwin-amd64.tgz: cloudflared
|
||||
tar czf cloudflared-darwin-amd64.tgz cloudflared
|
||||
rm cloudflared
|
||||
|
||||
.PHONY: cloudflared-junos
|
||||
cloudflared-junos: cloudflared jetez-certificate.pem jetez-key.pem
|
||||
jetez --source . \
|
||||
-j jet.yaml \
|
||||
--key jetez-key.pem \
|
||||
--cert jetez-certificate.pem \
|
||||
--version $(VERSION)
|
||||
rm jetez-*.pem
|
||||
|
||||
jetez-certificate.pem:
|
||||
ifndef JETEZ_CERT
|
||||
$(error JETEZ_CERT not defined)
|
||||
endif
|
||||
@echo "Writing JetEZ certificate"
|
||||
@echo "$$JETEZ_CERT" > jetez-certificate.pem
|
||||
|
||||
jetez-key.pem:
|
||||
ifndef JETEZ_KEY
|
||||
$(error JETEZ_KEY not defined)
|
||||
endif
|
||||
@echo "Writing JetEZ key"
|
||||
@echo "$$JETEZ_KEY" > jetez-key.pem
|
||||
|
||||
.PHONY: publish-cloudflared-junos
|
||||
publish-cloudflared-junos: cloudflared-junos cloudflared-x86-64.latest.s3
|
||||
ifndef S3_ENDPOINT
|
||||
$(error S3_HOST not defined)
|
||||
endif
|
||||
ifndef S3_URI
|
||||
$(error S3_URI not defined)
|
||||
endif
|
||||
ifndef S3_ACCESS_KEY
|
||||
$(error S3_ACCESS_KEY not defined)
|
||||
endif
|
||||
ifndef S3_SECRET_KEY
|
||||
$(error S3_SECRET_KEY not defined)
|
||||
endif
|
||||
sha256sum cloudflared-x86-64-$(VERSION).tgz | awk '{printf $$1}' > cloudflared-x86-64-$(VERSION).tgz.shasum
|
||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
|
||||
put cloudflared-x86-64-$(VERSION).tgz $(S3_URI)/cloudflared-x86-64-$(VERSION).tgz
|
||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
|
||||
put cloudflared-x86-64-$(VERSION).tgz.shasum $(S3_URI)/cloudflared-x86-64-$(VERSION).tgz.shasum
|
||||
dpkg --compare-versions "$(VERSION)" gt "$(shell cat cloudflared-x86-64.latest.s3)" && \
|
||||
echo -n "$(VERSION)" > cloudflared-x86-64.latest && \
|
||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
|
||||
put cloudflared-x86-64.latest $(S3_URI)/cloudflared-x86-64.latest || \
|
||||
echo "Latest version not updated"
|
||||
|
||||
cloudflared-x86-64.latest.s3:
|
||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force \
|
||||
get $(S3_URI)/cloudflared-x86-64.latest cloudflared-x86-64.latest.s3
|
||||
|
||||
.PHONY: homebrew-upload
|
||||
homebrew-upload: cloudflared-darwin-amd64.tgz
|
||||
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $$^ $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz
|
||||
@@ -64,24 +267,43 @@ homebrew-upload: cloudflared-darwin-amd64.tgz
|
||||
homebrew-release: homebrew-upload
|
||||
./publish-homebrew-formula.sh cloudflared-darwin-amd64.tgz $(VERSION) homebrew-cloudflare
|
||||
|
||||
.PHONY: release
|
||||
release: bin/equinox
|
||||
bin/equinox release $(EQUINOX_FLAGS) -- $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
.PHONY: github-release
|
||||
github-release: cloudflared
|
||||
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
|
||||
|
||||
bin/equinox:
|
||||
mkdir -p bin
|
||||
curl -s https://bin.equinox.io/c/75JtLRTsJ3n/release-tool-beta-$(EQUINOX_PLATFORM).tgz | tar xz -C bin/
|
||||
.PHONY: github-release-built-pkgs
|
||||
github-release-built-pkgs:
|
||||
python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION)
|
||||
|
||||
.PHONY: tunnel-deps
|
||||
tunnel-deps: tunnelrpc/tunnelrpc.capnp.go
|
||||
.PHONY: release-pkgs-linux
|
||||
release-pkgs-linux:
|
||||
python3 ./release_pkgs.py
|
||||
|
||||
tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp
|
||||
.PHONY: github-message
|
||||
github-message:
|
||||
python3 github_message.py --release-version $(VERSION)
|
||||
|
||||
.PHONY: github-mac-upload
|
||||
github-mac-upload:
|
||||
python3 github_release.py --path artifacts/cloudflared-darwin-amd64.tgz --release-version $(VERSION) --name cloudflared-darwin-amd64.tgz
|
||||
python3 github_release.py --path artifacts/cloudflared-amd64.pkg --release-version $(VERSION) --name cloudflared-amd64.pkg
|
||||
|
||||
.PHONY: tunnelrpc-deps
|
||||
tunnelrpc-deps:
|
||||
which capnp # https://capnproto.org/install.html
|
||||
which capnpc-go # go get zombiezen.com/go/capnproto2/capnpc-go
|
||||
capnp compile -ogo tunnelrpc/tunnelrpc.capnp
|
||||
|
||||
.PHONY: quic-deps
|
||||
quic-deps:
|
||||
which capnp
|
||||
which capnpc-go
|
||||
capnp compile -ogo quic/schema/quic_metadata_protocol.capnp
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet -mod=vendor ./...
|
||||
which go-sumtype # go get github.com/BurntSushi/go-sumtype
|
||||
go-sumtype $$(go list -mod=vendor ./...)
|
||||
|
||||
.PHONY: goimports
|
||||
goimports:
|
||||
for d in $$(go list -mod=readonly -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc) ; do goimports -format-only -local github.com/cloudflare/cloudflared -w $$d ; done
|
||||
|
||||
@@ -1,9 +1,60 @@
|
||||
# Argo Tunnel client
|
||||
# Cloudflare Tunnel client
|
||||
|
||||
Contains the command-line client and its libraries for Argo Tunnel, a tunneling daemon that proxies any local webserver through the Cloudflare network.
|
||||
Contains the command-line client for Cloudflare Tunnel, a tunneling daemon that proxies traffic from the Cloudflare network to your origins.
|
||||
This daemon sits between Cloudflare network and your origin (e.g. a webserver). Cloudflare attracts client requests and sends them to you
|
||||
via this daemon, without requiring you to poke holes on your firewall --- your origin can remain as closed as possible.
|
||||
Extensive documentation can be found in the [Cloudflare Tunnel section](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps) of the Cloudflare Docs.
|
||||
All usages related with proxying to your origins are available under `cloudflared tunnel help`.
|
||||
|
||||
## Getting started
|
||||
You can also use `cloudflared` to access Tunnel origins (that are protected with `cloudflared tunnel`) for TCP traffic
|
||||
at Layer 4 (i.e., not HTTP/websocket), which is relevant for use cases such as SSH, RDP, etc.
|
||||
Such usages are available under `cloudflared access help`.
|
||||
|
||||
go install github.com/cloudflare/cloudflared/cmd/cloudflared
|
||||
You can instead use [WARP client](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/private-networks)
|
||||
to access private origins behind Tunnels for Layer 4 traffic without requiring `cloudflared access` commands on the client side.
|
||||
|
||||
User documentation for Argo Tunnel can be found at https://developers.cloudflare.com/argo-tunnel/
|
||||
|
||||
## Before you get started
|
||||
|
||||
Before you use Cloudflare Tunnel, you'll need to complete a few steps in the Cloudflare dashboard: you need to add a
|
||||
website to your Cloudflare account. Note that today it is possible to use Tunnel without a website (e.g. for private
|
||||
routing), but for legacy reasons this requirement is still necessary:
|
||||
1. [Add a website to Cloudflare](https://support.cloudflare.com/hc/en-us/articles/201720164-Creating-a-Cloudflare-account-and-adding-a-website)
|
||||
2. [Change your domain nameservers to Cloudflare](https://support.cloudflare.com/hc/en-us/articles/205195708)
|
||||
|
||||
|
||||
## Installing `cloudflared`
|
||||
|
||||
Downloads are available as standalone binaries, a Docker image, and Debian, RPM, and Homebrew packages. You can also find releases here on the `cloudflared` GitHub repository.
|
||||
|
||||
* You can [install on macOS](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#macos) via Homebrew or by downloading the [latest Darwin amd64 release](https://github.com/cloudflare/cloudflared/releases)
|
||||
* Binaries, Debian, and RPM packages for Linux [can be found here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#linux)
|
||||
* A Docker image of `cloudflared` is [available on DockerHub](https://hub.docker.com/r/cloudflare/cloudflared)
|
||||
* You can install on Windows machines with the [steps here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#windows)
|
||||
|
||||
User documentation for Cloudflare Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
|
||||
|
||||
|
||||
## Creating Tunnels and routing traffic
|
||||
|
||||
Once installed, you can authenticate `cloudflared` into your Cloudflare account and begin creating Tunnels to serve traffic to your origins.
|
||||
|
||||
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/create-tunnel)
|
||||
* Route traffic to that Tunnel:
|
||||
* Via public [DNS records in Cloudflare](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/dns)
|
||||
* Or via a public hostname guided by a [Cloudflare Load Balancer](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/lb)
|
||||
* Or from [WARP client private traffic](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/private-net/)
|
||||
|
||||
|
||||
## TryCloudflare
|
||||
|
||||
Want to test Cloudflare Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/trycloudflare).
|
||||
|
||||
## Deprecated versions
|
||||
|
||||
Cloudflare currently supports versions of `cloudflared` 2020.5.1 and later. Breaking changes unrelated to feature availability may be introduced that will impact versions released prior to 2020.5.1. You can read more about upgrading `cloudflared` in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#updating-cloudflared).
|
||||
|
||||
| Version(s) | Deprecation status |
|
||||
|---|---|
|
||||
| 2020.5.1 and later | Supported |
|
||||
| Versions prior to 2020.5.1 | No longer supported |
|
||||
|
||||
+1015
File diff suppressed because it is too large
Load Diff
@@ -1,106 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// DirectoryUploadManager is used to manage file uploads on an interval from a directory
|
||||
type DirectoryUploadManager struct {
|
||||
logger *logrus.Logger
|
||||
uploader Uploader
|
||||
rootDirectory string
|
||||
sweepInterval time.Duration
|
||||
ticker *time.Ticker
|
||||
shutdownC chan struct{}
|
||||
workQueue chan string
|
||||
}
|
||||
|
||||
// NewDirectoryUploadManager create a new DirectoryUploadManager
|
||||
// uploader is an Uploader to use as an actual uploading engine
|
||||
// directory is the directory to sweep for files to upload
|
||||
// sweepInterval is how often to iterate the directory and upload the files within
|
||||
func NewDirectoryUploadManager(logger *logrus.Logger, uploader Uploader, directory string, sweepInterval time.Duration, shutdownC chan struct{}) *DirectoryUploadManager {
|
||||
workerCount := 10
|
||||
manager := &DirectoryUploadManager{
|
||||
logger: logger,
|
||||
uploader: uploader,
|
||||
rootDirectory: directory,
|
||||
sweepInterval: sweepInterval,
|
||||
shutdownC: shutdownC,
|
||||
workQueue: make(chan string, workerCount),
|
||||
}
|
||||
|
||||
//start workers
|
||||
for i := 0; i < workerCount; i++ {
|
||||
go manager.worker()
|
||||
}
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
// Upload a file using the uploader
|
||||
// This is useful for "out of band" uploads that need to be triggered immediately instead of waiting for the sweep
|
||||
func (m *DirectoryUploadManager) Upload(filepath string) error {
|
||||
return m.uploader.Upload(filepath)
|
||||
}
|
||||
|
||||
// Start the upload ticker to walk the directories
|
||||
func (m *DirectoryUploadManager) Start() {
|
||||
m.ticker = time.NewTicker(m.sweepInterval)
|
||||
go m.run()
|
||||
}
|
||||
|
||||
func (m *DirectoryUploadManager) run() {
|
||||
for {
|
||||
select {
|
||||
case <-m.shutdownC:
|
||||
m.ticker.Stop()
|
||||
return
|
||||
case <-m.ticker.C:
|
||||
m.sweep()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sweep the directory and kick off uploads
|
||||
func (m *DirectoryUploadManager) sweep() {
|
||||
filepath.Walk(m.rootDirectory, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
//30 days ago
|
||||
retentionTime := 30 * (time.Hour * 24)
|
||||
checkTime := time.Now().Add(-time.Duration(retentionTime))
|
||||
|
||||
//delete the file it is stale
|
||||
if info.ModTime().Before(checkTime) {
|
||||
os.Remove(path)
|
||||
return nil
|
||||
}
|
||||
//add the upload to the work queue
|
||||
go func() {
|
||||
m.workQueue <- path
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// worker handles upload requests
|
||||
func (m *DirectoryUploadManager) worker() {
|
||||
for {
|
||||
select {
|
||||
case <-m.shutdownC:
|
||||
return
|
||||
case filepath := <-m.workQueue:
|
||||
if err := m.Upload(filepath); err != nil {
|
||||
m.logger.WithError(err).Error("Cannot upload file to s3 bucket")
|
||||
} else {
|
||||
os.Remove(filepath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type MockUploader struct {
|
||||
shouldFail bool
|
||||
}
|
||||
|
||||
func (m *MockUploader) Upload(filepath string) error {
|
||||
if m.shouldFail {
|
||||
return errors.New("upload set to fail")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMockUploader(shouldFail bool) Uploader {
|
||||
return &MockUploader{shouldFail: shouldFail}
|
||||
}
|
||||
|
||||
func getDirectoryPath(t *testing.T) string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the test directory!", err)
|
||||
}
|
||||
return filepath.Join(dir, "uploads")
|
||||
}
|
||||
|
||||
func setupTestDirectory(t *testing.T) string {
|
||||
path := getDirectoryPath(t)
|
||||
os.RemoveAll(path)
|
||||
time.Sleep(100 * time.Millisecond) //short way to wait for the OS to delete the folder
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the test directory!", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func createUploadManager(t *testing.T, shouldFailUpload bool) *DirectoryUploadManager {
|
||||
rootDirectory := setupTestDirectory(t)
|
||||
uploader := NewMockUploader(shouldFailUpload)
|
||||
logger := logrus.New()
|
||||
shutdownC := make(chan struct{})
|
||||
return NewDirectoryUploadManager(logger, uploader, rootDirectory, 1*time.Second, shutdownC)
|
||||
}
|
||||
|
||||
func createFile(t *testing.T, fileName string) (*os.File, string) {
|
||||
path := filepath.Join(getDirectoryPath(t), fileName)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatal("upload to create file for sweep test", err)
|
||||
}
|
||||
return f, path
|
||||
}
|
||||
|
||||
func TestUploadSuccess(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
path := filepath.Join(getDirectoryPath(t), "test_file")
|
||||
if err := manager.Upload(path); err != nil {
|
||||
t.Fatal("the upload request method failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadFailure(t *testing.T) {
|
||||
manager := createUploadManager(t, true)
|
||||
path := filepath.Join(getDirectoryPath(t), "test_file")
|
||||
if err := manager.Upload(path); err == nil {
|
||||
t.Fatal("the upload request method should have failed and didn't", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweepSuccess(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
f, path := createFile(t, "test_file")
|
||||
defer f.Close()
|
||||
|
||||
manager.Start()
|
||||
time.Sleep(2 * time.Second)
|
||||
if _, err := os.Stat(path); os.IsExist(err) {
|
||||
//the file should have been deleted
|
||||
t.Fatal("the manager failed to delete the file", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweepFailure(t *testing.T) {
|
||||
manager := createUploadManager(t, true)
|
||||
f, path := createFile(t, "test_file")
|
||||
defer f.Close()
|
||||
|
||||
manager.Start()
|
||||
time.Sleep(2 * time.Second)
|
||||
_, serr := f.Stat()
|
||||
if serr != nil {
|
||||
//the file should still exist
|
||||
os.Remove(path)
|
||||
t.Fatal("the manager failed to delete the file", serr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHighLoad(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
for i := 0; i < 30; i++ {
|
||||
f, _ := createFile(t, randomString(6))
|
||||
defer f.Close()
|
||||
}
|
||||
manager.Start()
|
||||
time.Sleep(4 * time.Second)
|
||||
|
||||
directory := getDirectoryPath(t)
|
||||
files, err := ioutil.ReadDir(directory)
|
||||
if err != nil || len(files) > 0 {
|
||||
t.Fatalf("the manager failed to upload all the files: %s files left: %d", err, len(files))
|
||||
}
|
||||
}
|
||||
|
||||
// LowerCase [a-z]
|
||||
const randSet = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
// String returns a string of length 'n' from a set of letters 'lset'
|
||||
func randomString(n int) string {
|
||||
b := make([]byte, n)
|
||||
lsetLen := len(randSet)
|
||||
for i := range b {
|
||||
b[i] = randSet[rand.Intn(lsetLen)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
//FileUploader aws compliant bucket upload
|
||||
type FileUploader struct {
|
||||
storage *s3.S3
|
||||
bucketName string
|
||||
clientID string
|
||||
secretID string
|
||||
}
|
||||
|
||||
// NewFileUploader creates a new S3 compliant bucket uploader
|
||||
func NewFileUploader(bucketName, region, accessKeyID, secretID, token, s3Host string) (*FileUploader, error) {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(region),
|
||||
Credentials: credentials.NewStaticCredentials(accessKeyID, secretID, token),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var storage *s3.S3
|
||||
if s3Host != "" {
|
||||
storage = s3.New(sess, &aws.Config{Endpoint: aws.String(s3Host)})
|
||||
} else {
|
||||
storage = s3.New(sess)
|
||||
}
|
||||
|
||||
return &FileUploader{
|
||||
storage: storage,
|
||||
bucketName: bucketName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Upload a file to the bucket
|
||||
func (u *FileUploader) Upload(filepath string) error {
|
||||
info, err := os.Stat(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, serr := u.storage.PutObjectWithContext(context.Background(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(u.bucketName),
|
||||
Key: aws.String(info.Name()),
|
||||
Body: file,
|
||||
})
|
||||
return serr
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
// UploadManager is used to manage file uploads on an interval
|
||||
type UploadManager interface {
|
||||
Upload(string) error
|
||||
Start()
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package awsuploader
|
||||
|
||||
// Uploader the functions required to upload to a bucket
|
||||
type Uploader interface {
|
||||
//Upload a file to the bucket
|
||||
Upload(string) error
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Pool struct {
|
||||
// A Pool must not be copied after first use.
|
||||
// https://golang.org/pkg/sync/#Pool
|
||||
buffers sync.Pool
|
||||
}
|
||||
|
||||
func NewPool(bufferSize int) *Pool {
|
||||
return &Pool{
|
||||
buffers: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, bufferSize)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) Get() []byte {
|
||||
return p.buffers.Get().([]byte)
|
||||
}
|
||||
|
||||
func (p *Pool) Put(buf []byte) {
|
||||
p.buffers.Put(buf)
|
||||
}
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
|
||||
# This controls the directory the built artifacts go into
|
||||
export ARTIFACT_DIR=built_artifacts/
|
||||
mkdir -p $ARTIFACT_DIR
|
||||
|
||||
arch=("amd64")
|
||||
export TARGET_ARCH=$arch
|
||||
export TARGET_OS=linux
|
||||
export FIPS=true
|
||||
# For BoringCrypto to link, we need CGO enabled. Otherwise compilation fails.
|
||||
export CGO_ENABLED=1
|
||||
|
||||
make cloudflared-deb
|
||||
mv cloudflared-fips\_$VERSION\_$arch.deb $ARTIFACT_DIR/cloudflared-fips-linux-$arch.deb
|
||||
|
||||
# rpm packages invert the - and _ and use x86_64 instead of amd64.
|
||||
RPMVERSION=$(echo $VERSION|sed -r 's/-/_/g')
|
||||
RPMARCH="x86_64"
|
||||
make cloudflared-rpm
|
||||
mv cloudflared-fips-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-fips-linux-$RPMARCH.rpm
|
||||
|
||||
# finally move the linux binary as well.
|
||||
mv ./cloudflared $ARTIFACT_DIR/cloudflared-fips-linux-$arch
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
|
||||
# Avoid depending on C code since we don't need it.
|
||||
export CGO_ENABLED=0
|
||||
|
||||
# This controls the directory the built artifacts go into
|
||||
export ARTIFACT_DIR=built_artifacts/
|
||||
mkdir -p $ARTIFACT_DIR
|
||||
windowsArchs=("amd64" "386")
|
||||
export TARGET_OS=windows
|
||||
for arch in ${windowsArchs[@]}; do
|
||||
export TARGET_ARCH=$arch
|
||||
make cloudflared-msi
|
||||
mv ./cloudflared.exe $ARTIFACT_DIR/cloudflared-windows-$arch.exe
|
||||
mv cloudflared-$VERSION-$arch.msi $ARTIFACT_DIR/cloudflared-windows-$arch.msi
|
||||
done
|
||||
|
||||
|
||||
linuxArchs=("386" "amd64" "arm" "armhf" "arm64")
|
||||
export TARGET_OS=linux
|
||||
for arch in ${linuxArchs[@]}; do
|
||||
unset TARGET_ARM
|
||||
export TARGET_ARCH=$arch
|
||||
|
||||
## Support for armhf builds
|
||||
if [[ $arch == armhf ]] ; then
|
||||
export TARGET_ARCH=arm
|
||||
export TARGET_ARM=7
|
||||
fi
|
||||
|
||||
make cloudflared-deb
|
||||
mv cloudflared\_$VERSION\_$arch.deb $ARTIFACT_DIR/cloudflared-linux-$arch.deb
|
||||
|
||||
# rpm packages invert the - and _ and use x86_64 instead of amd64.
|
||||
RPMVERSION=$(echo $VERSION|sed -r 's/-/_/g')
|
||||
RPMARCH=$arch
|
||||
if [ $arch == "amd64" ];then
|
||||
RPMARCH="x86_64"
|
||||
fi
|
||||
if [ $arch == "arm64" ]; then
|
||||
RPMARCH="aarch64"
|
||||
fi
|
||||
make cloudflared-rpm
|
||||
mv cloudflared-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-linux-$RPMARCH.rpm
|
||||
|
||||
# finally move the linux binary as well.
|
||||
mv ./cloudflared $ARTIFACT_DIR/cloudflared-linux-$arch
|
||||
done
|
||||
+88
-119
@@ -4,27 +4,44 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
cloudflaredWebsocket "github.com/cloudflare/cloudflared/websocket"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
)
|
||||
|
||||
const (
|
||||
LogFieldOriginURL = "originURL"
|
||||
CFAccessTokenHeader = "Cf-Access-Token"
|
||||
cfJumpDestinationHeader = "Cf-Access-Jump-Destination"
|
||||
)
|
||||
|
||||
type StartOptions struct {
|
||||
OriginURL string
|
||||
Headers http.Header
|
||||
AppInfo *token.AppInfo
|
||||
OriginURL string
|
||||
Headers http.Header
|
||||
Host string
|
||||
TLSClientConfig *tls.Config
|
||||
}
|
||||
|
||||
// Connection wraps up all the needed functions to forward over the tunnel
|
||||
type Connection interface {
|
||||
// ServeStream is used to forward data from the client to the edge
|
||||
ServeStream(*StartOptions, io.ReadWriter) error
|
||||
}
|
||||
|
||||
// StdinoutStream is empty struct for wrapping stdin/stdout
|
||||
// into a single ReadWriter
|
||||
type StdinoutStream struct {
|
||||
}
|
||||
type StdinoutStream struct{}
|
||||
|
||||
// Read will read from Stdin
|
||||
func (c *StdinoutStream) Read(p []byte) (int, error) {
|
||||
@@ -37,93 +54,64 @@ func (c *StdinoutStream) Write(p []byte) (int, error) {
|
||||
return os.Stdout.Write(p)
|
||||
}
|
||||
|
||||
// Helper to allow defering the response close with a check that the resp is not nil
|
||||
// Helper to allow deferring the response close with a check that the resp is not nil
|
||||
func closeRespBody(resp *http.Response) {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// StartForwarder will setup a listener on a specified address/port and then
|
||||
// forward connections to the origin by calling `Serve()`.
|
||||
func StartForwarder(conn Connection, address string, shutdownC <-chan struct{}, options *StartOptions) error {
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start forwarding server")
|
||||
}
|
||||
return Serve(conn, listener, shutdownC, options)
|
||||
}
|
||||
|
||||
// StartClient will copy the data from stdin/stdout over a WebSocket connection
|
||||
// to the edge (originURL)
|
||||
func StartClient(logger *logrus.Logger, stream io.ReadWriter, options *StartOptions) error {
|
||||
return serveStream(logger, stream, options)
|
||||
}
|
||||
|
||||
// StartServer will setup a listener on a specified address/port and then
|
||||
// forward connections to the origin by calling `Serve()`.
|
||||
func StartServer(logger *logrus.Logger, address string, shutdownC <-chan struct{}, options *StartOptions) error {
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("failed to start forwarding server")
|
||||
return err
|
||||
}
|
||||
logger.Info("Started listening on ", address)
|
||||
return Serve(logger, listener, shutdownC, options)
|
||||
func StartClient(conn Connection, stream io.ReadWriter, options *StartOptions) error {
|
||||
return conn.ServeStream(options, stream)
|
||||
}
|
||||
|
||||
// Serve accepts incoming connections on the specified net.Listener.
|
||||
// Each connection is handled in a new goroutine: its data is copied over a
|
||||
// WebSocket connection to the edge (originURL).
|
||||
// `Serve` always closes `listener`.
|
||||
func Serve(logger *logrus.Logger, listener net.Listener, shutdownC <-chan struct{}, options *StartOptions) error {
|
||||
func Serve(remoteConn Connection, listener net.Listener, shutdownC <-chan struct{}, options *StartOptions) error {
|
||||
defer listener.Close()
|
||||
for {
|
||||
select {
|
||||
case <-shutdownC:
|
||||
return nil
|
||||
default:
|
||||
errChan := make(chan error)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
// don't block if parent goroutine quit early
|
||||
select {
|
||||
case errChan <- err:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
go serveConnection(logger, conn, options)
|
||||
go serveConnection(remoteConn, conn, options)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-shutdownC:
|
||||
return nil
|
||||
case err := <-errChan:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// serveConnection handles connections for the Serve() call
|
||||
func serveConnection(logger *logrus.Logger, c net.Conn, options *StartOptions) {
|
||||
func serveConnection(remoteConn Connection, c net.Conn, options *StartOptions) {
|
||||
defer c.Close()
|
||||
serveStream(logger, c, options)
|
||||
}
|
||||
|
||||
// serveStream will serve the data over the WebSocket stream
|
||||
func serveStream(logger *logrus.Logger, conn io.ReadWriter, options *StartOptions) error {
|
||||
wsConn, err := createWebsocketStream(options)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("failed to connect to %s\n", options.OriginURL)
|
||||
return err
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
cloudflaredWebsocket.Stream(wsConn, conn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createWebsocketStream will create a WebSocket connection to stream data over
|
||||
// It also handles redirects from Access and will present that flow if
|
||||
// the token is not present on the request
|
||||
func createWebsocketStream(options *StartOptions) (*cloudflaredWebsocket.Conn, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = options.Headers
|
||||
|
||||
wsConn, resp, err := cloudflaredWebsocket.ClientConnect(req, nil)
|
||||
defer closeRespBody(resp)
|
||||
if err != nil && IsAccessResponse(resp) {
|
||||
wsConn, err = createAccessAuthenticatedStream(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cloudflaredWebsocket.Conn{Conn: wsConn}, nil
|
||||
_ = remoteConn.ServeStream(options, c)
|
||||
}
|
||||
|
||||
// IsAccessResponse checks the http Response to see if the url location
|
||||
@@ -137,63 +125,21 @@ func IsAccessResponse(resp *http.Response) bool {
|
||||
if err != nil || location == nil {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(location.Path, "/cdn-cgi/access/login") {
|
||||
if strings.HasPrefix(location.Path, token.AccessLoginWorkerPath) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// createAccessAuthenticatedStream will try load a token from storage and make
|
||||
// a connection with the token set on the request. If it still get redirect,
|
||||
// this probably means the token in storage is invalid (expired/revoked). If that
|
||||
// happens it deletes the token and runs the connection again, so the user can
|
||||
// login again and generate a new one.
|
||||
func createAccessAuthenticatedStream(options *StartOptions) (*websocket.Conn, error) {
|
||||
wsConn, resp, err := createAccessWebSocketStream(options)
|
||||
defer closeRespBody(resp)
|
||||
if err == nil {
|
||||
return wsConn, nil
|
||||
}
|
||||
|
||||
if !IsAccessResponse(resp) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Access Token is invalid for some reason. Go through regen flow
|
||||
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := token.RemoveTokenIfExists(originReq.URL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wsConn, resp, err = createAccessWebSocketStream(options)
|
||||
defer closeRespBody(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wsConn, nil
|
||||
}
|
||||
|
||||
// createAccessWebSocketStream builds an Access request and makes a connection
|
||||
func createAccessWebSocketStream(options *StartOptions) (*websocket.Conn, *http.Response, error) {
|
||||
req, err := BuildAccessRequest(options)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return cloudflaredWebsocket.ClientConnect(req, nil)
|
||||
}
|
||||
|
||||
// buildAccessRequest builds an HTTP request with the Access token set
|
||||
func BuildAccessRequest(options *StartOptions) (*http.Request, error) {
|
||||
// BuildAccessRequest builds an HTTP request with the Access token set
|
||||
func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := token.FetchToken(req.URL)
|
||||
token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -204,7 +150,7 @@ func BuildAccessRequest(options *StartOptions) (*http.Request, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
originRequest.Header.Set("cf-access-token", token)
|
||||
originRequest.Header.Set(CFAccessTokenHeader, token)
|
||||
|
||||
for k, v := range options.Headers {
|
||||
if len(v) >= 1 {
|
||||
@@ -214,3 +160,26 @@ func BuildAccessRequest(options *StartOptions) (*http.Request, error) {
|
||||
|
||||
return originRequest, nil
|
||||
}
|
||||
|
||||
func SetBastionDest(header http.Header, destination string) {
|
||||
if destination != "" {
|
||||
header.Set(cfJumpDestinationHeader, destination)
|
||||
}
|
||||
}
|
||||
|
||||
func ResolveBastionDest(r *http.Request) (string, error) {
|
||||
jumpDestination := r.Header.Get(cfJumpDestinationHeader)
|
||||
if jumpDestination == "" {
|
||||
return "", fmt.Errorf("Did not receive final destination from client. The --destination flag is likely not set on the client side")
|
||||
}
|
||||
// Strip scheme and path set by client. Without a scheme
|
||||
// Parsing a hostname and path without scheme might not return an error due to parsing ambiguities
|
||||
if jumpURL, err := url.Parse(jumpDestination); err == nil && jumpURL.Host != "" {
|
||||
return removePath(jumpURL.Host), nil
|
||||
}
|
||||
return removePath(jumpDestination), nil
|
||||
}
|
||||
|
||||
func removePath(dest string) string {
|
||||
return strings.SplitN(dest, "/", 2)[0]
|
||||
}
|
||||
|
||||
+109
-10
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
ws "github.com/gorilla/websocket"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -43,7 +43,8 @@ func (s *testStreamer) Write(p []byte) (int, error) {
|
||||
|
||||
func TestStartClient(t *testing.T) {
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
logger := logrus.New()
|
||||
log := zerolog.Nop()
|
||||
wsConn := NewWSConnection(&log)
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
|
||||
@@ -52,12 +53,12 @@ func TestStartClient(t *testing.T) {
|
||||
OriginURL: "http://" + ts.Listener.Addr().String(),
|
||||
Headers: nil,
|
||||
}
|
||||
err := StartClient(logger, buf, options)
|
||||
err := StartClient(wsConn, buf, options)
|
||||
assert.NoError(t, err)
|
||||
buf.Write([]byte(message))
|
||||
_, _ = buf.Write([]byte(message))
|
||||
|
||||
readBuffer := make([]byte, len(message))
|
||||
buf.Read(readBuffer)
|
||||
_, _ = buf.Read(readBuffer)
|
||||
assert.Equal(t, message, string(readBuffer))
|
||||
}
|
||||
|
||||
@@ -67,8 +68,9 @@ func TestStartServer(t *testing.T) {
|
||||
t.Fatalf("Error starting listener: %v", err)
|
||||
}
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
logger := logrus.New()
|
||||
log := zerolog.Nop()
|
||||
shutdownC := make(chan struct{})
|
||||
wsConn := NewWSConnection(&log)
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
options := &StartOptions{
|
||||
@@ -77,17 +79,18 @@ func TestStartServer(t *testing.T) {
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := Serve(logger, listener, shutdownC, options)
|
||||
err := Serve(wsConn, listener, shutdownC, options)
|
||||
if err != nil {
|
||||
t.Fatalf("Error running server: %v", err)
|
||||
t.Errorf("Error running server: %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
conn.Write([]byte(message))
|
||||
_, _ = conn.Write([]byte(message))
|
||||
|
||||
readBuffer := make([]byte, len(message))
|
||||
conn.Read(readBuffer)
|
||||
_, _ = conn.Read(readBuffer)
|
||||
assert.Equal(t, string(readBuffer), message)
|
||||
}
|
||||
|
||||
@@ -153,3 +156,99 @@ func testRequest(t *testing.T, url string, stream io.ReadWriter) *http.Request {
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func TestBastionDestination(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
header http.Header
|
||||
expectedDest string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "hostname destination",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"localhost"},
|
||||
},
|
||||
expectedDest: "localhost",
|
||||
},
|
||||
{
|
||||
name: "hostname destination with port",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"localhost:9000"},
|
||||
},
|
||||
expectedDest: "localhost:9000",
|
||||
},
|
||||
{
|
||||
name: "hostname destination with scheme and port",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"ssh://localhost:9000"},
|
||||
},
|
||||
expectedDest: "localhost:9000",
|
||||
},
|
||||
{
|
||||
name: "full hostname url",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"ssh://localhost:9000/metrics"},
|
||||
},
|
||||
expectedDest: "localhost:9000",
|
||||
},
|
||||
{
|
||||
name: "hostname destination with port and path",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"localhost:9000/metrics"},
|
||||
},
|
||||
expectedDest: "localhost:9000",
|
||||
},
|
||||
{
|
||||
name: "ip destination",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"127.0.0.1"},
|
||||
},
|
||||
expectedDest: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "ip destination with port",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"127.0.0.1:9000"},
|
||||
},
|
||||
expectedDest: "127.0.0.1:9000",
|
||||
},
|
||||
{
|
||||
name: "ip destination with port and path",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"127.0.0.1:9000/metrics"},
|
||||
},
|
||||
expectedDest: "127.0.0.1:9000",
|
||||
},
|
||||
{
|
||||
name: "ip destination with schem and port",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"tcp://127.0.0.1:9000"},
|
||||
},
|
||||
expectedDest: "127.0.0.1:9000",
|
||||
},
|
||||
{
|
||||
name: "full ip url",
|
||||
header: http.Header{
|
||||
cfJumpDestinationHeader: []string{"ssh://127.0.0.1:9000/metrics"},
|
||||
},
|
||||
expectedDest: "127.0.0.1:9000",
|
||||
},
|
||||
{
|
||||
name: "no destination",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
r := &http.Request{
|
||||
Header: test.header,
|
||||
}
|
||||
dest, err := ResolveBastionDest(r)
|
||||
if test.wantErr {
|
||||
assert.Error(t, err, "Test %s expects error", test.name)
|
||||
} else {
|
||||
assert.NoError(t, err, "Test %s expects no error, got error %v", test.name, err)
|
||||
assert.Equal(t, test.expectedDest, dest, "Test %s expect dest %s, got %s", test.name, test.expectedDest, dest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
// Websocket is used to carry data via WS binary frames over the tunnel from client to the origin
|
||||
// This implements the functions for glider proxy (sock5) and the carrier interface
|
||||
type Websocket struct {
|
||||
log *zerolog.Logger
|
||||
isSocks bool
|
||||
}
|
||||
|
||||
// NewWSConnection returns a new connection object
|
||||
func NewWSConnection(log *zerolog.Logger) Connection {
|
||||
return &Websocket{
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeStream will create a Websocket client stream connection to the edge
|
||||
// it blocks and writes the raw data from conn over the tunnel
|
||||
func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) error {
|
||||
wsConn, err := createWebsocketStream(options, ws.log)
|
||||
if err != nil {
|
||||
ws.log.Err(err).Str(LogFieldOriginURL, options.OriginURL).Msg("failed to connect to origin")
|
||||
return err
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
cfwebsocket.Stream(wsConn, conn, ws.log)
|
||||
return nil
|
||||
}
|
||||
|
||||
// createWebsocketStream will create a WebSocket connection to stream data over
|
||||
// It also handles redirects from Access and will present that flow if
|
||||
// the token is not present on the request
|
||||
func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebsocket.GorillaConn, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = options.Headers
|
||||
if options.Host != "" {
|
||||
req.Host = options.Host
|
||||
}
|
||||
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug().Msgf("Websocket request: %s", string(dump))
|
||||
|
||||
dialer := &websocket.Dialer{
|
||||
TLSClientConfig: options.TLSClientConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
wsConn, resp, err := clientConnect(req, dialer)
|
||||
defer closeRespBody(resp)
|
||||
|
||||
if err != nil && IsAccessResponse(resp) {
|
||||
// Only get Access app info if we know the origin is protected by Access
|
||||
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appInfo, err := token.GetAppInfo(originReq.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options.AppInfo = appInfo
|
||||
|
||||
wsConn, err = createAccessAuthenticatedStream(options, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfwebsocket.GorillaConn{Conn: wsConn}, nil
|
||||
}
|
||||
|
||||
var stripWebsocketHeaders = []string{
|
||||
"Upgrade",
|
||||
"Connection",
|
||||
"Sec-Websocket-Key",
|
||||
"Sec-Websocket-Version",
|
||||
"Sec-Websocket-Extensions",
|
||||
}
|
||||
|
||||
// the gorilla websocket library sets its own Upgrade, Connection, Sec-WebSocket-Key,
|
||||
// Sec-WebSocket-Version and Sec-Websocket-Extensions headers.
|
||||
// https://github.com/gorilla/websocket/blob/master/client.go#L189-L194.
|
||||
func websocketHeaders(req *http.Request) http.Header {
|
||||
wsHeaders := make(http.Header)
|
||||
for key, val := range req.Header {
|
||||
wsHeaders[key] = val
|
||||
}
|
||||
// Assume the header keys are in canonical format.
|
||||
for _, header := range stripWebsocketHeaders {
|
||||
wsHeaders.Del(header)
|
||||
}
|
||||
wsHeaders.Set("Host", req.Host) // See TUN-1097
|
||||
return wsHeaders
|
||||
}
|
||||
|
||||
// clientConnect creates a WebSocket client connection for provided request. Caller is responsible for closing
|
||||
// the connection. The response body may not contain the entire response and does
|
||||
// not need to be closed by the application.
|
||||
func clientConnect(req *http.Request, dialler *websocket.Dialer) (*websocket.Conn, *http.Response, error) {
|
||||
req.URL.Scheme = changeRequestScheme(req.URL)
|
||||
wsHeaders := websocketHeaders(req)
|
||||
if dialler == nil {
|
||||
dialler = &websocket.Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
}
|
||||
conn, response, err := dialler.Dial(req.URL.String(), wsHeaders)
|
||||
if err != nil {
|
||||
return nil, response, err
|
||||
}
|
||||
return conn, response, nil
|
||||
}
|
||||
|
||||
// changeRequestScheme is needed as the gorilla websocket library requires the ws scheme.
|
||||
// (even though it changes it back to http/https, but ¯\_(ツ)_/¯.)
|
||||
func changeRequestScheme(reqURL *url.URL) string {
|
||||
switch reqURL.Scheme {
|
||||
case "https":
|
||||
return "wss"
|
||||
case "http":
|
||||
return "ws"
|
||||
case "":
|
||||
return "ws"
|
||||
default:
|
||||
return reqURL.Scheme
|
||||
}
|
||||
}
|
||||
|
||||
// createAccessAuthenticatedStream will try load a token from storage and make
|
||||
// a connection with the token set on the request. If it still get redirect,
|
||||
// this probably means the token in storage is invalid (expired/revoked). If that
|
||||
// happens it deletes the token and runs the connection again, so the user can
|
||||
// login again and generate a new one.
|
||||
func createAccessAuthenticatedStream(options *StartOptions, log *zerolog.Logger) (*websocket.Conn, error) {
|
||||
wsConn, resp, err := createAccessWebSocketStream(options, log)
|
||||
defer closeRespBody(resp)
|
||||
if err == nil {
|
||||
return wsConn, nil
|
||||
}
|
||||
|
||||
if !IsAccessResponse(resp) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Access Token is invalid for some reason. Go through regen flow
|
||||
if err := token.RemoveTokenIfExists(options.AppInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wsConn, resp, err = createAccessWebSocketStream(options, log)
|
||||
defer closeRespBody(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wsConn, nil
|
||||
}
|
||||
|
||||
// createAccessWebSocketStream builds an Access request and makes a connection
|
||||
func createAccessWebSocketStream(options *StartOptions, log *zerolog.Logger) (*websocket.Conn, *http.Response, error) {
|
||||
req, err := BuildAccessRequest(options, log)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
log.Debug().Msgf("Access Websocket request: %s", string(dump))
|
||||
|
||||
conn, resp, err := clientConnect(req, nil)
|
||||
|
||||
if resp != nil {
|
||||
r, err := httputil.DumpResponse(resp, true)
|
||||
if r != nil {
|
||||
log.Debug().Msgf("Websocket response: %q", r)
|
||||
} else if err != nil {
|
||||
log.Debug().Msgf("Websocket response error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return conn, resp, err
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gws "github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/websocket"
|
||||
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
func websocketClientTLSConfig(t *testing.T) *tls.Config {
|
||||
certPool := x509.NewCertPool()
|
||||
helloCert, err := tlsconfig.GetHelloCertificateX509()
|
||||
assert.NoError(t, err)
|
||||
certPool.AddCert(helloCert)
|
||||
assert.NotNil(t, certPool)
|
||||
return &tls.Config{RootCAs: certPool}
|
||||
}
|
||||
|
||||
func TestWebsocketHeaders(t *testing.T) {
|
||||
req := testRequest(t, "http://example.com", nil)
|
||||
wsHeaders := websocketHeaders(req)
|
||||
for _, header := range stripWebsocketHeaders {
|
||||
assert.Empty(t, wsHeaders[header])
|
||||
}
|
||||
assert.Equal(t, "curl/7.59.0", wsHeaders.Get("User-Agent"))
|
||||
}
|
||||
|
||||
func TestServe(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
shutdownC := make(chan struct{})
|
||||
errC := make(chan error)
|
||||
listener, err := hello.CreateTLSListener("localhost:1111")
|
||||
assert.NoError(t, err)
|
||||
defer listener.Close()
|
||||
|
||||
go func() {
|
||||
errC <- hello.StartHelloWorldServer(&log, listener, shutdownC)
|
||||
}()
|
||||
|
||||
req := testRequest(t, "https://localhost:1111/ws", nil)
|
||||
|
||||
tlsConfig := websocketClientTLSConfig(t)
|
||||
assert.NotNil(t, tlsConfig)
|
||||
d := gws.Dialer{TLSClientConfig: tlsConfig}
|
||||
conn, resp, err := clientConnect(req, &d)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
messageSize := rand.Int()%2048 + 1
|
||||
clientMessage := make([]byte, messageSize)
|
||||
// rand.Read always returns len(clientMessage) and a nil error
|
||||
rand.Read(clientMessage)
|
||||
err = conn.WriteMessage(websocket.BinaryFrame, clientMessage)
|
||||
assert.NoError(t, err)
|
||||
|
||||
messageType, message, err := conn.ReadMessage()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, websocket.BinaryFrame, messageType)
|
||||
assert.Equal(t, clientMessage, message)
|
||||
}
|
||||
|
||||
_ = conn.Close()
|
||||
close(shutdownC)
|
||||
<-errC
|
||||
}
|
||||
|
||||
func TestWebsocketWrapper(t *testing.T) {
|
||||
listener, err := hello.CreateTLSListener("localhost:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
serverErrorChan := make(chan error)
|
||||
helloSvrCtx, cancelHelloSvr := context.WithCancel(context.Background())
|
||||
defer func() { <-serverErrorChan }()
|
||||
defer cancelHelloSvr()
|
||||
go func() {
|
||||
log := zerolog.Nop()
|
||||
serverErrorChan <- hello.StartHelloWorldServer(&log, listener, helloSvrCtx.Done())
|
||||
}()
|
||||
|
||||
tlsConfig := websocketClientTLSConfig(t)
|
||||
d := gws.Dialer{TLSClientConfig: tlsConfig, HandshakeTimeout: time.Minute}
|
||||
testAddr := fmt.Sprintf("https://%s/ws", listener.Addr().String())
|
||||
req := testRequest(t, testAddr, nil)
|
||||
conn, resp, err := clientConnect(req, &d)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
|
||||
|
||||
// Websocket now connected to test server so lets check our wrapper
|
||||
wrapper := cfwebsocket.GorillaConn{Conn: conn}
|
||||
buf := make([]byte, 100)
|
||||
wrapper.Write([]byte("abc"))
|
||||
n, err := wrapper.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, n, 3)
|
||||
require.Equal(t, "abc", string(buf[:n]))
|
||||
|
||||
// Test partial read, read 1 of 3 bytes in one read and the other 2 in another read
|
||||
wrapper.Write([]byte("abc"))
|
||||
buf = buf[:1]
|
||||
n, err = wrapper.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, n, 1)
|
||||
require.Equal(t, "a", string(buf[:n]))
|
||||
buf = buf[:cap(buf)]
|
||||
n, err = wrapper.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, n, 2)
|
||||
require.Equal(t, "bc", string(buf[:n]))
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package certutil
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type namedTunnelToken struct {
|
||||
ZoneID string `json:"zoneID"`
|
||||
AccountID string `json:"accountID"`
|
||||
ServiceKey string `json:"serviceKey"`
|
||||
}
|
||||
|
||||
type OriginCert struct {
|
||||
PrivateKey interface{}
|
||||
Cert *x509.Certificate
|
||||
ZoneID string
|
||||
ServiceKey string
|
||||
AccountID string
|
||||
}
|
||||
|
||||
func DecodeOriginCert(blocks []byte) (*OriginCert, error) {
|
||||
if len(blocks) == 0 {
|
||||
return nil, fmt.Errorf("Cannot decode empty certificate")
|
||||
}
|
||||
originCert := OriginCert{}
|
||||
block, rest := pem.Decode(blocks)
|
||||
for {
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
switch block.Type {
|
||||
case "PRIVATE KEY":
|
||||
if originCert.PrivateKey != nil {
|
||||
return nil, fmt.Errorf("Found multiple private key in the certificate")
|
||||
}
|
||||
// RSA private key
|
||||
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot parse private key")
|
||||
}
|
||||
originCert.PrivateKey = privateKey
|
||||
case "CERTIFICATE":
|
||||
if originCert.Cert != nil {
|
||||
return nil, fmt.Errorf("Found multiple certificates in the certificate")
|
||||
}
|
||||
cert, err := x509.ParseCertificates(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot parse certificate")
|
||||
} else if len(cert) > 1 {
|
||||
return nil, fmt.Errorf("Found multiple certificates in the certificate")
|
||||
}
|
||||
originCert.Cert = cert[0]
|
||||
case "WARP TOKEN", "ARGO TUNNEL TOKEN":
|
||||
if originCert.ZoneID != "" || originCert.ServiceKey != "" {
|
||||
return nil, fmt.Errorf("Found multiple tokens in the certificate")
|
||||
}
|
||||
// The token is a string,
|
||||
// Try the newer JSON format
|
||||
ntt := namedTunnelToken{}
|
||||
if err := json.Unmarshal(block.Bytes, &ntt); err == nil {
|
||||
originCert.ZoneID = ntt.ZoneID
|
||||
originCert.ServiceKey = ntt.ServiceKey
|
||||
originCert.AccountID = ntt.AccountID
|
||||
} else {
|
||||
// Try the older format, where the zoneID and service key are separated by
|
||||
// a new line character
|
||||
token := string(block.Bytes)
|
||||
s := strings.Split(token, "\n")
|
||||
if len(s) != 2 {
|
||||
return nil, fmt.Errorf("Cannot parse token")
|
||||
}
|
||||
originCert.ZoneID = s[0]
|
||||
originCert.ServiceKey = s[1]
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown block %s in the certificate", block.Type)
|
||||
}
|
||||
block, rest = pem.Decode(rest)
|
||||
}
|
||||
|
||||
if originCert.PrivateKey == nil {
|
||||
return nil, fmt.Errorf("Missing private key in the certificate")
|
||||
} else if originCert.Cert == nil {
|
||||
return nil, fmt.Errorf("Missing certificate in the certificate")
|
||||
} else if originCert.ZoneID == "" || originCert.ServiceKey == "" {
|
||||
return nil, fmt.Errorf("Missing token in the certificate")
|
||||
}
|
||||
|
||||
return &originCert, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package certutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadOriginCert(t *testing.T) {
|
||||
cert, err := DecodeOriginCert([]byte{})
|
||||
assert.Equal(t, fmt.Errorf("Cannot decode empty certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err := ioutil.ReadFile("test-cert-no-key.pem")
|
||||
assert.Nil(t, err)
|
||||
cert, err = DecodeOriginCert(blocks)
|
||||
assert.Equal(t, fmt.Errorf("Missing private key in the certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err = ioutil.ReadFile("test-cert-two-certificates.pem")
|
||||
assert.Nil(t, err)
|
||||
cert, err = DecodeOriginCert(blocks)
|
||||
assert.Equal(t, fmt.Errorf("Found multiple certificates in the certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err = ioutil.ReadFile("test-cert-unknown-block.pem")
|
||||
assert.Nil(t, err)
|
||||
cert, err = DecodeOriginCert(blocks)
|
||||
assert.Equal(t, fmt.Errorf("Unknown block RSA PRIVATE KEY in the certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err = ioutil.ReadFile("test-cert.pem")
|
||||
assert.Nil(t, err)
|
||||
cert, err = DecodeOriginCert(blocks)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, cert)
|
||||
assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID)
|
||||
key := "v1.0-58bd4f9e28f7b3c28e05a35ff3e80ab4fd9644ef3fece537eb0d12e2e9258217-183442fbb0bbdb3e571558fec9b5589ebd77aafc87498ee3f09f64a4ad79ffe8791edbae08b36c1d8f1d70a8670de56922dff92b15d214a524f4ebfa1958859e-7ce80f79921312a6022c5d25e2d380f82ceaefe3fbdc43dd13b080e3ef1e26f7"
|
||||
assert.Equal(t, key, cert.ServiceKey)
|
||||
}
|
||||
|
||||
func TestNewlineArgoTunnelToken(t *testing.T) {
|
||||
ArgoTunnelTokenTest(t, "test-argo-tunnel-cert.pem")
|
||||
}
|
||||
|
||||
func TestJSONArgoTunnelToken(t *testing.T) {
|
||||
// The given cert's Argo Tunnel Token was generated by base64 encoding this JSON:
|
||||
// {
|
||||
// "zoneID": "7b0a4d77dfb881c1a3b7d61ea9443e19",
|
||||
// "serviceKey": "test-service-key",
|
||||
// "accountID": "abcdabcdabcdabcd1234567890abcdef"
|
||||
// }
|
||||
ArgoTunnelTokenTest(t, "test-argo-tunnel-cert-json.pem")
|
||||
}
|
||||
|
||||
func ArgoTunnelTokenTest(t *testing.T, path string) {
|
||||
blocks, err := ioutil.ReadFile(path)
|
||||
assert.Nil(t, err)
|
||||
cert, err := DecodeOriginCert(blocks)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, cert)
|
||||
assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID)
|
||||
key := "test-service-key"
|
||||
assert.Equal(t, key, cert.ServiceKey)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN ARGO TUNNEL TOKEN-----
|
||||
eyJ6b25lSUQiOiAiN2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkiLCAi
|
||||
c2VydmljZUtleSI6ICJ0ZXN0LXNlcnZpY2Uta2V5IiwgImFjY291bnRJRCI6ICJh
|
||||
YmNkYWJjZGFiY2RhYmNkMTIzNDU2Nzg5MGFiY2RlZiJ9
|
||||
-----END ARGO TUNNEL TOKEN-----
|
||||
@@ -0,0 +1,56 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN ARGO TUNNEL TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdGVzdC1zZXJ2aWNlLWtl
|
||||
eQ==
|
||||
-----END ARGO TUNNEL TOKEN-----
|
||||
@@ -0,0 +1,33 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN WARP TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
|
||||
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
|
||||
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
|
||||
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
|
||||
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
|
||||
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
|
||||
ZWYxZTI2Zjc=
|
||||
-----END WARP TOKEN-----
|
||||
@@ -0,0 +1,85 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN WARP TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
|
||||
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
|
||||
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
|
||||
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
|
||||
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
|
||||
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
|
||||
ZWYxZTI2Zjc=
|
||||
-----END WARP TOKEN-----
|
||||
@@ -0,0 +1,89 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN WARP TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
|
||||
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
|
||||
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
|
||||
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
|
||||
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
|
||||
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
|
||||
ZWYxZTI2Zjc=
|
||||
-----END WARP TOKEN-----
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -0,0 +1,61 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN WARP TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
|
||||
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
|
||||
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
|
||||
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
|
||||
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
|
||||
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
|
||||
ZWYxZTI2Zjc=
|
||||
-----END WARP TOKEN-----
|
||||
@@ -0,0 +1,186 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 15 * time.Second
|
||||
jsonContentType = "application/json"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrBadRequest = errors.New("incorrect request parameters")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrAPINoSuccess = errors.New("API call failed")
|
||||
)
|
||||
|
||||
type RESTClient struct {
|
||||
baseEndpoints *baseEndpoints
|
||||
authToken string
|
||||
userAgent string
|
||||
client http.Client
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
type baseEndpoints struct {
|
||||
accountLevel url.URL
|
||||
zoneLevel url.URL
|
||||
accountRoutes url.URL
|
||||
accountVnets url.URL
|
||||
}
|
||||
|
||||
var _ Client = (*RESTClient)(nil)
|
||||
|
||||
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/cfd_tunnel", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||
}
|
||||
accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
|
||||
}
|
||||
accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
|
||||
}
|
||||
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||
}
|
||||
httpTransport := http.Transport{
|
||||
TLSHandshakeTimeout: defaultTimeout,
|
||||
ResponseHeaderTimeout: defaultTimeout,
|
||||
}
|
||||
http2.ConfigureTransport(&httpTransport)
|
||||
return &RESTClient{
|
||||
baseEndpoints: &baseEndpoints{
|
||||
accountLevel: *accountLevelEndpoint,
|
||||
zoneLevel: *zoneLevelEndpoint,
|
||||
accountRoutes: *accountRoutesEndpoint,
|
||||
accountVnets: *accountVnetsEndpoint,
|
||||
},
|
||||
authToken: authToken,
|
||||
userAgent: userAgent,
|
||||
client: http.Client{
|
||||
Transport: &httpTransport,
|
||||
Timeout: defaultTimeout,
|
||||
},
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
if bodyBytes, err := json.Marshal(body); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to serialize json body")
|
||||
} else {
|
||||
bodyReader = bytes.NewBuffer(bodyBytes)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url.String(), bodyReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "can't create %s request", method)
|
||||
}
|
||||
req.Header.Set("User-Agent", r.userAgent)
|
||||
if bodyReader != nil {
|
||||
req.Header.Set("Content-Type", jsonContentType)
|
||||
}
|
||||
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
|
||||
req.Header.Add("Accept", "application/json;version=1")
|
||||
return r.client.Do(req)
|
||||
}
|
||||
|
||||
func parseResponse(reader io.Reader, data interface{}) error {
|
||||
// Schema for Tunnelstore responses in the v1 API.
|
||||
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
|
||||
var result response
|
||||
// First, parse the wrapper and check the API call succeeded
|
||||
if err := json.NewDecoder(reader).Decode(&result); err != nil {
|
||||
return errors.Wrap(err, "failed to decode response")
|
||||
}
|
||||
if err := result.checkErrors(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !result.Success {
|
||||
return ErrAPINoSuccess
|
||||
}
|
||||
// At this point we know the API call succeeded, so, parse out the inner
|
||||
// result into the datatype provided as a parameter.
|
||||
if err := json.Unmarshal(result.Result, &data); err != nil {
|
||||
return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Success bool `json:"success,omitempty"`
|
||||
Errors []apiErr `json:"errors,omitempty"`
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
func (r *response) checkErrors() error {
|
||||
if len(r.Errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(r.Errors) == 1 {
|
||||
return r.Errors[0]
|
||||
}
|
||||
var messages string
|
||||
for _, e := range r.Errors {
|
||||
messages += fmt.Sprintf("%s; ", e)
|
||||
}
|
||||
return fmt.Errorf("API errors: %s", messages)
|
||||
}
|
||||
|
||||
type apiErr struct {
|
||||
Code json.Number `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (e apiErr) Error() string {
|
||||
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
|
||||
if resp.Header.Get("Content-Type") == "application/json" {
|
||||
var errorsResp response
|
||||
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
|
||||
if err := errorsResp.checkErrors(); err != nil {
|
||||
return errors.Errorf("Failed to %s: %s", op, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
case http.StatusBadRequest:
|
||||
return ErrBadRequest
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return ErrUnauthorized
|
||||
case http.StatusNotFound:
|
||||
return ErrNotFound
|
||||
}
|
||||
return errors.Errorf("API call to %s failed with status %d: %s", op,
|
||||
resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TunnelClient interface {
|
||||
CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
|
||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||
GetTunnelToken(tunnelID uuid.UUID) (string, error)
|
||||
DeleteTunnel(tunnelID uuid.UUID) error
|
||||
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
||||
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
||||
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
|
||||
}
|
||||
|
||||
type HostnameClient interface {
|
||||
RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error)
|
||||
}
|
||||
|
||||
type IPRouteClient interface {
|
||||
ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error)
|
||||
AddRoute(newRoute NewRoute) (Route, error)
|
||||
DeleteRoute(params DeleteRouteParams) error
|
||||
GetByIP(params GetRouteByIpParams) (DetailedRoute, error)
|
||||
}
|
||||
|
||||
type VnetClient interface {
|
||||
CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error)
|
||||
ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error)
|
||||
DeleteVirtualNetwork(id uuid.UUID) error
|
||||
UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
TunnelClient
|
||||
HostnameClient
|
||||
IPRouteClient
|
||||
VnetClient
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Change = string
|
||||
|
||||
const (
|
||||
ChangeNew = "new"
|
||||
ChangeUpdated = "updated"
|
||||
ChangeUnchanged = "unchanged"
|
||||
)
|
||||
|
||||
// HostnameRoute represents a record type that can route to a tunnel
|
||||
type HostnameRoute interface {
|
||||
json.Marshaler
|
||||
RecordType() string
|
||||
UnmarshalResult(body io.Reader) (HostnameRouteResult, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
type HostnameRouteResult interface {
|
||||
// SuccessSummary explains what will route to this tunnel when it's provisioned successfully
|
||||
SuccessSummary() string
|
||||
}
|
||||
|
||||
type DNSRoute struct {
|
||||
userHostname string
|
||||
overwriteExisting bool
|
||||
}
|
||||
|
||||
type DNSRouteResult struct {
|
||||
route *DNSRoute
|
||||
CName Change `json:"cname"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func NewDNSRoute(userHostname string, overwriteExisting bool) HostnameRoute {
|
||||
return &DNSRoute{
|
||||
userHostname: userHostname,
|
||||
overwriteExisting: overwriteExisting,
|
||||
}
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
UserHostname string `json:"user_hostname"`
|
||||
OverwriteExisting bool `json:"overwrite_existing"`
|
||||
}{
|
||||
Type: dr.RecordType(),
|
||||
UserHostname: dr.userHostname,
|
||||
OverwriteExisting: dr.overwriteExisting,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) {
|
||||
var result DNSRouteResult
|
||||
err := parseResponse(body, &result)
|
||||
result.route = dr
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) RecordType() string {
|
||||
return "dns"
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) String() string {
|
||||
return fmt.Sprintf("%s %s", dr.RecordType(), dr.userHostname)
|
||||
}
|
||||
|
||||
func (res *DNSRouteResult) SuccessSummary() string {
|
||||
var msgFmt string
|
||||
switch res.CName {
|
||||
case ChangeNew:
|
||||
msgFmt = "Added CNAME %s which will route to this tunnel"
|
||||
case ChangeUpdated: // this is not currently returned by tunnelsore
|
||||
msgFmt = "%s updated to route to your tunnel"
|
||||
case ChangeUnchanged:
|
||||
msgFmt = "%s is already configured to route to your tunnel"
|
||||
}
|
||||
return fmt.Sprintf(msgFmt, res.hostname())
|
||||
}
|
||||
|
||||
// hostname yields the resulting name for the DNS route; if that is not available from Cloudflare API, then the
|
||||
// requested name is returned instead (should not be the common path, it is just a fall-back).
|
||||
func (res *DNSRouteResult) hostname() string {
|
||||
if res.Name != "" {
|
||||
return res.Name
|
||||
}
|
||||
return res.route.userHostname
|
||||
}
|
||||
|
||||
type LBRoute struct {
|
||||
lbName string
|
||||
lbPool string
|
||||
}
|
||||
|
||||
type LBRouteResult struct {
|
||||
route *LBRoute
|
||||
LoadBalancer Change `json:"load_balancer"`
|
||||
Pool Change `json:"pool"`
|
||||
}
|
||||
|
||||
func NewLBRoute(lbName, lbPool string) HostnameRoute {
|
||||
return &LBRoute{
|
||||
lbName: lbName,
|
||||
lbPool: lbPool,
|
||||
}
|
||||
}
|
||||
|
||||
func (lr *LBRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
LBName string `json:"lb_name"`
|
||||
LBPool string `json:"lb_pool"`
|
||||
}{
|
||||
Type: lr.RecordType(),
|
||||
LBName: lr.lbName,
|
||||
LBPool: lr.lbPool,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
func (lr *LBRoute) RecordType() string {
|
||||
return "lb"
|
||||
}
|
||||
|
||||
func (lb *LBRoute) String() string {
|
||||
return fmt.Sprintf("%s %s %s", lb.RecordType(), lb.lbName, lb.lbPool)
|
||||
}
|
||||
|
||||
func (lr *LBRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) {
|
||||
var result LBRouteResult
|
||||
err := parseResponse(body, &result)
|
||||
result.route = lr
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (res *LBRouteResult) SuccessSummary() string {
|
||||
var msg string
|
||||
switch res.LoadBalancer + "," + res.Pool {
|
||||
case "new,new":
|
||||
msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin"
|
||||
case "new,updated":
|
||||
msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin"
|
||||
case "new,unchanged":
|
||||
msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin"
|
||||
case "updated,new":
|
||||
msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s"
|
||||
case "updated,updated":
|
||||
msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s"
|
||||
case "updated,unchanged":
|
||||
msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s"
|
||||
case "unchanged,updated":
|
||||
msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s"
|
||||
case "unchanged,unchanged":
|
||||
msg = "Load balancer %s already uses pool %s which has this tunnel as an origin"
|
||||
case "unchanged,new":
|
||||
// this state is not possible
|
||||
fallthrough
|
||||
default:
|
||||
msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool)
|
||||
}
|
||||
|
||||
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error) {
|
||||
endpoint := r.baseEndpoints.zoneLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID))
|
||||
resp, err := r.sendRequest("PUT", endpoint, route)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return route.UnmarshalResult(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("add route", resp)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDNSRouteUnmarshalResult(t *testing.T) {
|
||||
route := &DNSRoute{
|
||||
userHostname: "example.com",
|
||||
}
|
||||
|
||||
result, err := route.UnmarshalResult(strings.NewReader(`{"success": true, "result": {"cname": "new"}}`))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &DNSRouteResult{
|
||||
route: route,
|
||||
CName: ChangeNew,
|
||||
}, result)
|
||||
|
||||
badJSON := []string{
|
||||
`abc`,
|
||||
`{"success": false, "result": {"cname": "new"}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"cname": "new"}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}, {"code": 1004, "message":"Cannot use tunnel as origin for non-proxied load balancer"}], "result": {"cname": "new"}}`,
|
||||
`{"result": {"cname": "new"}}`,
|
||||
`{"result": {"cname": "new"}}`,
|
||||
}
|
||||
|
||||
for _, j := range badJSON {
|
||||
_, err = route.UnmarshalResult(strings.NewReader(j))
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLBRouteUnmarshalResult(t *testing.T) {
|
||||
route := &LBRoute{
|
||||
lbName: "lb.example.com",
|
||||
lbPool: "pool",
|
||||
}
|
||||
|
||||
result, err := route.UnmarshalResult(strings.NewReader(`{"success": true, "result": {"pool": "unchanged", "load_balancer": "updated"}}`))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &LBRouteResult{
|
||||
route: route,
|
||||
LoadBalancer: ChangeUpdated,
|
||||
Pool: ChangeUnchanged,
|
||||
}, result)
|
||||
|
||||
badJSON := []string{
|
||||
`abc`,
|
||||
`{"success": false, "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}, {"code": 1004, "message":"Cannot use tunnel as origin for non-proxied load balancer"}], "result": {"pool": "unchanged", "load_balancer": "updated"}}`,
|
||||
`{"result": {"pool": "unchanged", "load_balancer": "updated"}}`,
|
||||
}
|
||||
|
||||
for _, j := range badJSON {
|
||||
_, err = route.UnmarshalResult(strings.NewReader(j))
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLBRouteResultSuccessSummary(t *testing.T) {
|
||||
route := &LBRoute{
|
||||
lbName: "lb.example.com",
|
||||
lbPool: "POOL",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
lb Change
|
||||
pool Change
|
||||
expected string
|
||||
}{
|
||||
{ChangeNew, ChangeNew, "Created load balancer lb.example.com and added a new pool POOL with this tunnel as an origin"},
|
||||
{ChangeNew, ChangeUpdated, "Created load balancer lb.example.com with an existing pool POOL which was updated to use this tunnel as an origin"},
|
||||
{ChangeNew, ChangeUnchanged, "Created load balancer lb.example.com with an existing pool POOL which already has this tunnel as an origin"},
|
||||
{ChangeUpdated, ChangeNew, "Added new pool POOL with this tunnel as an origin to load balancer lb.example.com"},
|
||||
{ChangeUpdated, ChangeUpdated, "Updated pool POOL to use this tunnel as an origin and added it to load balancer lb.example.com"},
|
||||
{ChangeUpdated, ChangeUnchanged, "Added pool POOL, which already has this tunnel as an origin, to load balancer lb.example.com"},
|
||||
{ChangeUnchanged, ChangeNew, "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"},
|
||||
{ChangeUnchanged, ChangeUpdated, "Added this tunnel as an origin in pool POOL which is already used by load balancer lb.example.com"},
|
||||
{ChangeUnchanged, ChangeUnchanged, "Load balancer lb.example.com already uses pool POOL which has this tunnel as an origin"},
|
||||
{"", "", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"},
|
||||
{"a", "b", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard"},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
res := &LBRouteResult{
|
||||
route: route,
|
||||
LoadBalancer: tt.lb,
|
||||
Pool: tt.pool,
|
||||
}
|
||||
actual := res.SuccessSummary()
|
||||
assert.Equal(t, tt.expected, actual, "case %d", i+1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Route is a mapping from customer's IP space to a tunnel.
|
||||
// Each route allows the customer to route eyeballs in their corporate network
|
||||
// to certain private IP ranges. Each Route represents an IP range in their
|
||||
// network, and says that eyeballs can reach that route using the corresponding
|
||||
// tunnel.
|
||||
type Route struct {
|
||||
Network CIDR `json:"network"`
|
||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||
// Optional field. When unset, it means the Route belongs to the default virtual network.
|
||||
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
||||
Comment string `json:"comment"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
}
|
||||
|
||||
// CIDR is just a newtype wrapper around net.IPNet. It adds JSON unmarshalling.
|
||||
type CIDR net.IPNet
|
||||
|
||||
func (c CIDR) String() string {
|
||||
n := net.IPNet(c)
|
||||
return n.String()
|
||||
}
|
||||
|
||||
func (c CIDR) MarshalJSON() ([]byte, error) {
|
||||
str := c.String()
|
||||
json, err := json.Marshal(str)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error serializing CIDR into JSON")
|
||||
}
|
||||
return json, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses a JSON string into net.IPNet
|
||||
func (c *CIDR) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return errors.Wrap(err, "error parsing cidr string")
|
||||
}
|
||||
_, network, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing invalid network from backend")
|
||||
}
|
||||
if network == nil {
|
||||
return fmt.Errorf("backend returned invalid network %s", s)
|
||||
}
|
||||
*c = CIDR(*network)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRoute has all the parameters necessary to add a new route to the table.
|
||||
type NewRoute struct {
|
||||
Network net.IPNet
|
||||
TunnelID uuid.UUID
|
||||
Comment string
|
||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||
VNetID *uuid.UUID
|
||||
}
|
||||
|
||||
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
|
||||
func (r NewRoute) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&struct {
|
||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||
Comment string `json:"comment"`
|
||||
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
||||
}{
|
||||
TunnelID: r.TunnelID,
|
||||
Comment: r.Comment,
|
||||
VNetID: r.VNetID,
|
||||
})
|
||||
}
|
||||
|
||||
// DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
|
||||
type DetailedRoute struct {
|
||||
Network CIDR `json:"network"`
|
||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||
// Optional field. When unset, it means the DetailedRoute belongs to the default virtual network.
|
||||
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
||||
Comment string `json:"comment"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
TunnelName string `json:"tunnel_name"`
|
||||
}
|
||||
|
||||
// IsZero checks if DetailedRoute is the zero value.
|
||||
func (r *DetailedRoute) IsZero() bool {
|
||||
return r.TunnelID == uuid.Nil
|
||||
}
|
||||
|
||||
// TableString outputs a table row summarizing the route, to be used
|
||||
// when showing the user their routing table.
|
||||
func (r DetailedRoute) TableString() string {
|
||||
deletedColumn := "-"
|
||||
if !r.DeletedAt.IsZero() {
|
||||
deletedColumn = r.DeletedAt.Format(time.RFC3339)
|
||||
}
|
||||
vnetColumn := "default"
|
||||
if r.VNetID != nil {
|
||||
vnetColumn = r.VNetID.String()
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||
r.Network.String(),
|
||||
vnetColumn,
|
||||
r.Comment,
|
||||
r.TunnelID,
|
||||
r.TunnelName,
|
||||
r.CreatedAt.Format(time.RFC3339),
|
||||
deletedColumn,
|
||||
)
|
||||
}
|
||||
|
||||
type DeleteRouteParams struct {
|
||||
Network net.IPNet
|
||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||
VNetID *uuid.UUID
|
||||
}
|
||||
|
||||
type GetRouteByIpParams struct {
|
||||
Ip net.IP
|
||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||
VNetID *uuid.UUID
|
||||
}
|
||||
|
||||
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
|
||||
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.RawQuery = filter.Encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListDetailedRoutes(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list routes", resp)
|
||||
}
|
||||
|
||||
// AddRoute calls the Tunnelstore POST endpoint for a given route.
|
||||
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String()))
|
||||
resp, err := r.sendRequest("POST", endpoint, newRoute)
|
||||
if err != nil {
|
||||
return Route{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseRoute(resp.Body)
|
||||
}
|
||||
|
||||
return Route{}, r.statusCodeToError("add route", resp)
|
||||
}
|
||||
|
||||
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
||||
func (r *RESTClient) DeleteRoute(params DeleteRouteParams) error {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
|
||||
setVnetParam(&endpoint, params.VNetID)
|
||||
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
_, err := parseRoute(resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
return r.statusCodeToError("delete route", resp)
|
||||
}
|
||||
|
||||
// GetByIP checks which route will proxy a given IP.
|
||||
func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
|
||||
setVnetParam(&endpoint, params.VNetID)
|
||||
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return DetailedRoute{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseDetailedRoute(resp.Body)
|
||||
}
|
||||
|
||||
return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
||||
}
|
||||
|
||||
func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
|
||||
var routes []*DetailedRoute
|
||||
err := parseResponse(body, &routes)
|
||||
return routes, err
|
||||
}
|
||||
|
||||
func parseRoute(body io.ReadCloser) (Route, error) {
|
||||
var route Route
|
||||
err := parseResponse(body, &route)
|
||||
return route, err
|
||||
}
|
||||
|
||||
func parseDetailedRoute(body io.ReadCloser) (DetailedRoute, error) {
|
||||
var route DetailedRoute
|
||||
err := parseResponse(body, &route)
|
||||
return route, err
|
||||
}
|
||||
|
||||
// setVnetParam overwrites the URL's query parameters with a query param to scope the HostnameRoute action to a certain
|
||||
// virtual network (if one is provided).
|
||||
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
|
||||
queryParams := url.Values{}
|
||||
if vnetID != nil {
|
||||
queryParams.Set("virtual_network_id", vnetID.String())
|
||||
}
|
||||
endpoint.RawQuery = queryParams.Encode()
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
filterIpRouteDeleted = cli.BoolFlag{
|
||||
Name: "filter-is-deleted",
|
||||
Usage: "If false (default), only show non-deleted routes. If true, only show deleted routes.",
|
||||
}
|
||||
filterIpRouteTunnelID = cli.StringFlag{
|
||||
Name: "filter-tunnel-id",
|
||||
Usage: "Show only routes with the given tunnel ID.",
|
||||
}
|
||||
filterSubsetIpRoute = cli.StringFlag{
|
||||
Name: "filter-network-is-subset-of",
|
||||
Aliases: []string{"nsub"},
|
||||
Usage: "Show only routes whose network is a subset of the given network.",
|
||||
}
|
||||
filterSupersetIpRoute = cli.StringFlag{
|
||||
Name: "filter-network-is-superset-of",
|
||||
Aliases: []string{"nsup"},
|
||||
Usage: "Show only routes whose network is a superset of the given network.",
|
||||
}
|
||||
filterIpRouteComment = cli.StringFlag{
|
||||
Name: "filter-comment-is",
|
||||
Usage: "Show only routes with this comment.",
|
||||
}
|
||||
filterIpRouteByVnet = cli.StringFlag{
|
||||
Name: "filter-vnet-id",
|
||||
Usage: "Show only routes that are attached to the given virtual network ID.",
|
||||
}
|
||||
|
||||
// Flags contains all filter flags.
|
||||
IpRouteFilterFlags = []cli.Flag{
|
||||
&filterIpRouteDeleted,
|
||||
&filterIpRouteTunnelID,
|
||||
&filterSubsetIpRoute,
|
||||
&filterSupersetIpRoute,
|
||||
&filterIpRouteComment,
|
||||
&filterIpRouteByVnet,
|
||||
}
|
||||
)
|
||||
|
||||
// IpRouteFilter which routes get queried.
|
||||
type IpRouteFilter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
// NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied.
|
||||
func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
|
||||
f := &IpRouteFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
|
||||
// Set deletion filter
|
||||
if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
||||
f.deleted()
|
||||
} else {
|
||||
f.notDeleted()
|
||||
}
|
||||
|
||||
if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil {
|
||||
return nil, err
|
||||
} else if subset != nil {
|
||||
f.networkIsSupersetOf(*subset)
|
||||
}
|
||||
|
||||
if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil {
|
||||
return nil, err
|
||||
} else if superset != nil {
|
||||
f.networkIsSupersetOf(*superset)
|
||||
}
|
||||
|
||||
if comment := c.String(filterIpRouteComment.Name); comment != "" {
|
||||
f.commentIs(comment)
|
||||
}
|
||||
|
||||
if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" {
|
||||
u, err := uuid.Parse(tunnelID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteTunnelID.Name)
|
||||
}
|
||||
f.tunnelID(u)
|
||||
}
|
||||
|
||||
if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" {
|
||||
u, err := uuid.Parse(vnetId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteByVnet.Name)
|
||||
}
|
||||
f.vnetID(u)
|
||||
}
|
||||
|
||||
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
|
||||
f.MaxFetchSize(uint(maxFetch))
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Parses a CIDR from the flag. If the flag was unset, returns (nil, nil).
|
||||
func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) {
|
||||
if !c.IsSet(flag.Name) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_, subset, err := net.ParseCIDR(c.String(flag.Name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if subset == nil {
|
||||
return nil, fmt.Errorf("Invalid CIDR supplied for %s", flag.Name)
|
||||
}
|
||||
|
||||
return subset, nil
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) commentIs(comment string) {
|
||||
f.queryParams.Set("comment", comment)
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) notDeleted() {
|
||||
f.queryParams.Set("is_deleted", "false")
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) deleted() {
|
||||
f.queryParams.Set("is_deleted", "true")
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) networkIsSubsetOf(superset net.IPNet) {
|
||||
f.queryParams.Set("network_subset", superset.String())
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) networkIsSupersetOf(subset net.IPNet) {
|
||||
f.queryParams.Set("network_superset", subset.String())
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) existedAt(existedAt time.Time) {
|
||||
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) tunnelID(id uuid.UUID) {
|
||||
f.queryParams.Set("tunnel_id", id.String())
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) vnetID(id uuid.UUID) {
|
||||
f.queryParams.Set("virtual_network_id", id.String())
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f IpRouteFilter) Encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUnmarshalRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Json string
|
||||
HasVnet bool
|
||||
}{
|
||||
{
|
||||
`{
|
||||
"network":"10.1.2.40/29",
|
||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||
"comment":"test",
|
||||
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||
"deleted_at":null
|
||||
}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
`{
|
||||
"network":"10.1.2.40/29",
|
||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||
"comment":"test",
|
||||
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||
"deleted_at":null,
|
||||
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9"
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
data := testCase.Json
|
||||
|
||||
var r Route
|
||||
err := json.Unmarshal([]byte(data), &r)
|
||||
|
||||
// Check everything worked
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
|
||||
require.Equal(t, "test", r.Comment)
|
||||
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, CIDR(*cidr), r.Network)
|
||||
require.Equal(t, "test", r.Comment)
|
||||
|
||||
if testCase.HasVnet {
|
||||
require.Equal(t, uuid.MustParse("38c95083-8191-4110-8339-3f438d44fdb9"), *r.VNetID)
|
||||
} else {
|
||||
require.Nil(t, r.VNetID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetailedRouteJsonRoundtrip(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Json string
|
||||
HasVnet bool
|
||||
}{
|
||||
{
|
||||
`{
|
||||
"network":"10.1.2.40/29",
|
||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||
"comment":"test",
|
||||
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||
"deleted_at":"2021-01-14T05:01:42.183002Z",
|
||||
"tunnel_name":"Mr. Tun"
|
||||
}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
`{
|
||||
"network":"10.1.2.40/29",
|
||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9",
|
||||
"comment":"test",
|
||||
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||
"deleted_at":"2021-01-14T05:01:42.183002Z",
|
||||
"tunnel_name":"Mr. Tun"
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
data := testCase.Json
|
||||
|
||||
var r DetailedRoute
|
||||
err := json.Unmarshal([]byte(data), &r)
|
||||
|
||||
// Check everything worked
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
|
||||
require.Equal(t, "test", r.Comment)
|
||||
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, CIDR(*cidr), r.Network)
|
||||
require.Equal(t, "test", r.Comment)
|
||||
require.Equal(t, "Mr. Tun", r.TunnelName)
|
||||
|
||||
if testCase.HasVnet {
|
||||
require.Equal(t, uuid.MustParse("38c95083-8191-4110-8339-3f438d44fdb9"), *r.VNetID)
|
||||
} else {
|
||||
require.Nil(t, r.VNetID)
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(r)
|
||||
require.NoError(t, err)
|
||||
obtainedJson := string(bytes)
|
||||
data = strings.Replace(data, "\t", "", -1)
|
||||
data = strings.Replace(data, "\n", "", -1)
|
||||
require.Equal(t, data, obtainedJson)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalNewRoute(t *testing.T) {
|
||||
_, network, err := net.ParseCIDR("1.2.3.4/32")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, network)
|
||||
vnetId := uuid.New()
|
||||
|
||||
newRoutes := []NewRoute{
|
||||
{
|
||||
Network: *network,
|
||||
TunnelID: uuid.New(),
|
||||
Comment: "hi",
|
||||
},
|
||||
{
|
||||
Network: *network,
|
||||
TunnelID: uuid.New(),
|
||||
Comment: "hi",
|
||||
VNetID: &vnetId,
|
||||
},
|
||||
}
|
||||
|
||||
for _, newRoute := range newRoutes {
|
||||
// Test where receiver is struct
|
||||
serialized, err := json.Marshal(newRoute)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
|
||||
|
||||
// Test where receiver is pointer to struct
|
||||
serialized, err = json.Marshal(&newRoute)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
|
||||
|
||||
if newRoute.VNetID == nil {
|
||||
require.False(t, strings.Contains(string(serialized), "virtual_network_id"))
|
||||
} else {
|
||||
require.True(t, strings.Contains(string(serialized), "virtual_network_id"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteTableString(t *testing.T) {
|
||||
_, network, err := net.ParseCIDR("1.2.3.4/32")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, network)
|
||||
r := DetailedRoute{
|
||||
Network: CIDR(*network),
|
||||
}
|
||||
row := r.TableString()
|
||||
fmt.Println(row)
|
||||
require.True(t, strings.HasPrefix(row, "1.2.3.4/32"))
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrTunnelNameConflict = errors.New("tunnel with name already exists")
|
||||
|
||||
type Tunnel struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
Connections []Connection `json:"connections"`
|
||||
}
|
||||
|
||||
type TunnelWithToken struct {
|
||||
Tunnel
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
ColoName string `json:"colo_name"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
IsPendingReconnect bool `json:"is_pending_reconnect"`
|
||||
OriginIP net.IP `json:"origin_ip"`
|
||||
OpenedAt time.Time `json:"opened_at"`
|
||||
}
|
||||
|
||||
type ActiveClient struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Features []string `json:"features"`
|
||||
Version string `json:"version"`
|
||||
Arch string `json:"arch"`
|
||||
RunAt time.Time `json:"run_at"`
|
||||
Connections []Connection `json:"conns"`
|
||||
}
|
||||
|
||||
type newTunnel struct {
|
||||
Name string `json:"name"`
|
||||
TunnelSecret []byte `json:"tunnel_secret"`
|
||||
}
|
||||
|
||||
type CleanupParams struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewCleanupParams() *CleanupParams {
|
||||
return &CleanupParams{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (cp *CleanupParams) ForClient(clientID uuid.UUID) {
|
||||
cp.queryParams.Set("client_id", clientID.String())
|
||||
}
|
||||
|
||||
func (cp CleanupParams) encode() string {
|
||||
return cp.queryParams.Encode()
|
||||
}
|
||||
|
||||
func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("tunnel name required")
|
||||
}
|
||||
if _, err := uuid.Parse(name); err == nil {
|
||||
return nil, errors.New("you cannot use UUIDs as tunnel names")
|
||||
}
|
||||
body := &newTunnel{
|
||||
Name: name,
|
||||
TunnelSecret: tunnelSecret,
|
||||
}
|
||||
|
||||
resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
var tunnel TunnelWithToken
|
||||
if serdeErr := parseResponse(resp.Body, &tunnel); err != nil {
|
||||
return nil, serdeErr
|
||||
}
|
||||
return &tunnel, nil
|
||||
case http.StatusConflict:
|
||||
return nil, ErrTunnelNameConflict
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("create tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return unmarshalTunnel(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("get tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) GetTunnelToken(tunnelID uuid.UUID) (token string, err error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/token", tunnelID))
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
err = parseResponse(resp.Body, &token)
|
||||
return token, err
|
||||
}
|
||||
|
||||
return "", r.statusCodeToError("get tunnel token", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return r.statusCodeToError("delete tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.RawQuery = filter.encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListTunnels(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list tunnels", resp)
|
||||
}
|
||||
|
||||
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
|
||||
var tunnels []*Tunnel
|
||||
err := parseResponse(body, &tunnels)
|
||||
return tunnels, err
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseConnectionsDetails(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list connection details", resp)
|
||||
}
|
||||
|
||||
func parseConnectionsDetails(reader io.Reader) ([]*ActiveClient, error) {
|
||||
var clients []*ActiveClient
|
||||
err := parseResponse(reader, &clients)
|
||||
return clients, err
|
||||
}
|
||||
|
||||
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.RawQuery = params.encode()
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return r.statusCodeToError("cleanup connections", resp)
|
||||
}
|
||||
|
||||
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
|
||||
var tunnel Tunnel
|
||||
err := parseResponse(reader, &tunnel)
|
||||
return &tunnel, err
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
TimeLayout = time.RFC3339
|
||||
)
|
||||
|
||||
type TunnelFilter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewTunnelFilter() *TunnelFilter {
|
||||
return &TunnelFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) ByName(name string) {
|
||||
f.queryParams.Set("name", name)
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) ByNamePrefix(namePrefix string) {
|
||||
f.queryParams.Set("name_prefix", namePrefix)
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) ExcludeNameWithPrefix(excludePrefix string) {
|
||||
f.queryParams.Set("exclude_prefix", excludePrefix)
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) NoDeleted() {
|
||||
f.queryParams.Set("is_deleted", "false")
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) ByExistedAt(existedAt time.Time) {
|
||||
f.queryParams.Set("existed_at", existedAt.Format(TimeLayout))
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) ByTunnelID(tunnelID uuid.UUID) {
|
||||
f.queryParams.Set("uuid", tunnelID.String())
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f TunnelFilter) encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var loc, _ = time.LoadLocation("UTC")
|
||||
|
||||
func Test_parseListTunnels(t *testing.T) {
|
||||
type args struct {
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []*Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
args: args{body: `{"success": true, "result": []}`},
|
||||
want: []*Tunnel{},
|
||||
},
|
||||
{
|
||||
name: "success is false",
|
||||
args: args{body: `{"success": false, "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors are present",
|
||||
args: args{body: `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid response",
|
||||
args: args{body: `abc`},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body)))
|
||||
got, err := parseListTunnels(body)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_unmarshalTunnel(t *testing.T) {
|
||||
type args struct {
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
args: args{body: `{"success": true, "result": {"id":"b34cc7ce-925b-46ee-bc23-4cb5c18d8292","created_at":"2021-07-29T13:46:14.090955Z","deleted_at":"2021-07-29T14:07:27.559047Z","name":"qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV","account_id":6946212,"account_tag":"5ab4e9dfbd435d24068829fda0077963","conns_active_at":null,"conns_inactive_at":"2021-07-29T13:47:22.548482Z","tun_type":"cfd_tunnel","metadata":{"qtid":"a6fJROgkXutNruBGaJjD"}}}`},
|
||||
want: &Tunnel{
|
||||
ID: uuid.MustParse("b34cc7ce-925b-46ee-bc23-4cb5c18d8292"),
|
||||
Name: "qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV",
|
||||
CreatedAt: time.Date(2021, 07, 29, 13, 46, 14, 90955000, loc),
|
||||
DeletedAt: time.Date(2021, 07, 29, 14, 7, 27, 559047000, loc),
|
||||
Connections: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := unmarshalTunnel(strings.NewReader(tt.args.body))
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("unmarshalTunnel() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("unmarshalTunnel() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelOk(t *testing.T) {
|
||||
|
||||
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
|
||||
expected := Tunnel{
|
||||
ID: uuid.Nil,
|
||||
Name: "test",
|
||||
CreatedAt: time.Time{},
|
||||
Connections: []Connection{},
|
||||
}
|
||||
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &expected, actual)
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelErr(t *testing.T) {
|
||||
|
||||
tests := []string{
|
||||
`abc`,
|
||||
`{"success": true, "result": abc}`,
|
||||
`{"success": false, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
|
||||
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalConnections(t *testing.T) {
|
||||
jsonBody := `{"success":true,"messages":[],"errors":[],"result":[{"id":"d4041254-91e3-4deb-bd94-b46e11680b1e","features":["ha-origin"],"version":"2021.2.5","arch":"darwin_amd64","conns":[{"colo_name":"LIS","id":"ac2286e5-c708-4588-a6a0-ba6b51940019","is_pending_reconnect":false,"origin_ip":"148.38.28.2","opened_at":"0001-01-01T00:00:00Z"}],"run_at":"0001-01-01T00:00:00Z"}]}`
|
||||
expected := ActiveClient{
|
||||
ID: uuid.MustParse("d4041254-91e3-4deb-bd94-b46e11680b1e"),
|
||||
Features: []string{"ha-origin"},
|
||||
Version: "2021.2.5",
|
||||
Arch: "darwin_amd64",
|
||||
RunAt: time.Time{},
|
||||
Connections: []Connection{{
|
||||
ID: uuid.MustParse("ac2286e5-c708-4588-a6a0-ba6b51940019"),
|
||||
ColoName: "LIS",
|
||||
IsPendingReconnect: false,
|
||||
OriginIP: net.ParseIP("148.38.28.2"),
|
||||
OpenedAt: time.Time{},
|
||||
}},
|
||||
}
|
||||
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*ActiveClient{&expected}, actual)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type NewVirtualNetwork struct {
|
||||
Name string `json:"name"`
|
||||
Comment string `json:"comment"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
type VirtualNetwork struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Comment string `json:"comment"`
|
||||
Name string `json:"name"`
|
||||
IsDefault bool `json:"is_default_network"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type UpdateVirtualNetwork struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
IsDefault *bool `json:"is_default_network,omitempty"`
|
||||
}
|
||||
|
||||
func (virtualNetwork VirtualNetwork) TableString() string {
|
||||
deletedColumn := "-"
|
||||
if !virtualNetwork.DeletedAt.IsZero() {
|
||||
deletedColumn = virtualNetwork.DeletedAt.Format(time.RFC3339)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||
virtualNetwork.ID,
|
||||
virtualNetwork.Name,
|
||||
strconv.FormatBool(virtualNetwork.IsDefault),
|
||||
virtualNetwork.Comment,
|
||||
virtualNetwork.CreatedAt.Format(time.RFC3339),
|
||||
deletedColumn,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *RESTClient) CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error) {
|
||||
resp, err := r.sendRequest("POST", r.baseEndpoints.accountVnets, newVnet)
|
||||
if err != nil {
|
||||
return VirtualNetwork{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseVnet(resp.Body)
|
||||
}
|
||||
|
||||
return VirtualNetwork{}, r.statusCodeToError("add virtual network", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error) {
|
||||
endpoint := r.baseEndpoints.accountVnets
|
||||
endpoint.RawQuery = filter.Encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListVnets(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list virtual networks", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) DeleteVirtualNetwork(id uuid.UUID) error {
|
||||
endpoint := r.baseEndpoints.accountVnets
|
||||
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
_, err := parseVnet(resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
return r.statusCodeToError("delete virtual network", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error {
|
||||
endpoint := r.baseEndpoints.accountVnets
|
||||
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
||||
resp, err := r.sendRequest("PATCH", endpoint, updates)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
_, err := parseVnet(resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
return r.statusCodeToError("update virtual network", resp)
|
||||
}
|
||||
|
||||
func parseListVnets(body io.ReadCloser) ([]*VirtualNetwork, error) {
|
||||
var vnets []*VirtualNetwork
|
||||
err := parseResponse(body, &vnets)
|
||||
return vnets, err
|
||||
}
|
||||
|
||||
func parseVnet(body io.ReadCloser) (VirtualNetwork, error) {
|
||||
var vnet VirtualNetwork
|
||||
err := parseResponse(body, &vnet)
|
||||
return vnet, err
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
filterVnetId = cli.StringFlag{
|
||||
Name: "id",
|
||||
Usage: "List virtual networks with the given `ID`",
|
||||
}
|
||||
filterVnetByName = cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "List virtual networks with the given `NAME`",
|
||||
}
|
||||
filterDefaultVnet = cli.BoolFlag{
|
||||
Name: "is-default",
|
||||
Usage: "If true, lists the virtual network that is the default one. If false, lists all non-default virtual networks for the account. If absent, all are included in the results regardless of their default status.",
|
||||
}
|
||||
filterDeletedVnet = cli.BoolFlag{
|
||||
Name: "show-deleted",
|
||||
Usage: "If false (default), only show non-deleted virtual networks. If true, only show deleted virtual networks.",
|
||||
}
|
||||
VnetFilterFlags = []cli.Flag{
|
||||
&filterVnetId,
|
||||
&filterVnetByName,
|
||||
&filterDefaultVnet,
|
||||
&filterDeletedVnet,
|
||||
}
|
||||
)
|
||||
|
||||
// VnetFilter which virtual networks get queried.
|
||||
type VnetFilter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewVnetFilter() *VnetFilter {
|
||||
return &VnetFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *VnetFilter) ById(vnetId uuid.UUID) {
|
||||
f.queryParams.Set("id", vnetId.String())
|
||||
}
|
||||
|
||||
func (f *VnetFilter) ByName(name string) {
|
||||
f.queryParams.Set("name", name)
|
||||
}
|
||||
|
||||
func (f *VnetFilter) ByDefaultStatus(isDefault bool) {
|
||||
f.queryParams.Set("is_default", strconv.FormatBool(isDefault))
|
||||
}
|
||||
|
||||
func (f *VnetFilter) WithDeleted(isDeleted bool) {
|
||||
f.queryParams.Set("is_deleted", strconv.FormatBool(isDeleted))
|
||||
}
|
||||
|
||||
func (f *VnetFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f VnetFilter) Encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
|
||||
// NewFromCLI parses CLI flags to discover which filters should get applied to list virtual networks.
|
||||
func NewFromCLI(c *cli.Context) (*VnetFilter, error) {
|
||||
f := NewVnetFilter()
|
||||
|
||||
if id := c.String("id"); id != "" {
|
||||
vnetId, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "%s is not a valid virtual network ID", id)
|
||||
}
|
||||
f.ById(vnetId)
|
||||
}
|
||||
|
||||
if name := c.String("name"); name != "" {
|
||||
f.ByName(name)
|
||||
}
|
||||
|
||||
if c.IsSet("is-default") {
|
||||
f.ByDefaultStatus(c.Bool("is-default"))
|
||||
}
|
||||
|
||||
f.WithDeleted(c.Bool("show-deleted"))
|
||||
|
||||
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
|
||||
f.MaxFetchSize(uint(maxFetch))
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVirtualNetworkJsonRoundtrip(t *testing.T) {
|
||||
data := `{
|
||||
"id":"74fce949-351b-4752-b261-81a56cfd3130",
|
||||
"comment":"New York DC1",
|
||||
"name":"us-east-1",
|
||||
"is_default_network":true,
|
||||
"created_at":"2021-11-26T14:40:02.600673Z",
|
||||
"deleted_at":"2021-12-01T10:23:13.102645Z"
|
||||
}`
|
||||
var v VirtualNetwork
|
||||
err := json.Unmarshal([]byte(data), &v)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uuid.MustParse("74fce949-351b-4752-b261-81a56cfd3130"), v.ID)
|
||||
require.Equal(t, "us-east-1", v.Name)
|
||||
require.Equal(t, "New York DC1", v.Comment)
|
||||
require.Equal(t, true, v.IsDefault)
|
||||
|
||||
bytes, err := json.Marshal(v)
|
||||
require.NoError(t, err)
|
||||
obtainedJson := string(bytes)
|
||||
data = strings.Replace(data, "\t", "", -1)
|
||||
data = strings.Replace(data, "\n", "", -1)
|
||||
require.Equal(t, data, obtainedJson)
|
||||
}
|
||||
|
||||
func TestMarshalNewVnet(t *testing.T) {
|
||||
newVnet := NewVirtualNetwork{
|
||||
Name: "eu-west-1",
|
||||
Comment: "London office",
|
||||
IsDefault: true,
|
||||
}
|
||||
|
||||
serialized, err := json.Marshal(newVnet)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.Contains(string(serialized), newVnet.Name))
|
||||
}
|
||||
|
||||
func TestMarshalUpdateVnet(t *testing.T) {
|
||||
newName := "bulgaria-1"
|
||||
updates := UpdateVirtualNetwork{
|
||||
Name: &newName,
|
||||
}
|
||||
|
||||
// Test where receiver is struct
|
||||
serialized, err := json.Marshal(updates)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.Contains(string(serialized), newName))
|
||||
}
|
||||
|
||||
func TestVnetTableString(t *testing.T) {
|
||||
virtualNet := VirtualNetwork{
|
||||
ID: uuid.New(),
|
||||
Name: "us-east-1",
|
||||
Comment: "New York DC1",
|
||||
IsDefault: true,
|
||||
CreatedAt: time.Now(),
|
||||
DeletedAt: time.Time{},
|
||||
}
|
||||
|
||||
row := virtualNet.TableString()
|
||||
require.True(t, strings.HasPrefix(row, virtualNet.ID.String()))
|
||||
require.True(t, strings.Contains(row, virtualNet.Name))
|
||||
require.True(t, strings.Contains(row, virtualNet.Comment))
|
||||
require.True(t, strings.Contains(row, "true"))
|
||||
require.True(t, strings.HasSuffix(row, "-\t"))
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cfio
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const defaultBufferSize = 16 * 1024
|
||||
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, defaultBufferSize)
|
||||
},
|
||||
}
|
||||
|
||||
func Copy(dst io.Writer, src io.Reader) (written int64, err error) {
|
||||
_, okWriteTo := src.(io.WriterTo)
|
||||
_, okReadFrom := dst.(io.ReaderFrom)
|
||||
var buffer []byte = nil
|
||||
|
||||
if !(okWriteTo || okReadFrom) {
|
||||
buffer = bufferPool.Get().([]byte)
|
||||
defer bufferPool.Put(buffer)
|
||||
}
|
||||
|
||||
return io.CopyBuffer(dst, src, buffer)
|
||||
}
|
||||
+222
-56
@@ -1,6 +1,8 @@
|
||||
pinned_go: &pinned_go go=1.12.7-1
|
||||
pinned_go: &pinned_go go=1.18.5-1
|
||||
pinned_go_fips: &pinned_go_fips go-boring=1.18.5-2
|
||||
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: stretch
|
||||
default-flavor: bullseye
|
||||
stretch: &stretch
|
||||
build:
|
||||
build_dir: *build_dir
|
||||
@@ -11,9 +13,74 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared
|
||||
build-deb:
|
||||
build-fips:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared
|
||||
# except FIPS (handled in github-fips-release-pkgs) and macos (handled in github-release-macos-amd64)
|
||||
github-release-pkgs:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- rpm
|
||||
- wget
|
||||
# libmsi and libgcab are libraries the wixl binary depends on.
|
||||
- libmsi-dev
|
||||
- libgcab-dev
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
- reprepro
|
||||
- createrepo
|
||||
pre-cache: &github_release_pkgs_pre_cache
|
||||
- wget https://github.com/sudarshan-reddy/msitools/releases/download/v0.101b/wixl -P /usr/local/bin
|
||||
- chmod a+x /usr/local/bin/wixl
|
||||
- pip3 install pynacl==1.4.0
|
||||
- pip3 install pygithub==1.55
|
||||
- pip3 install boto3==1.22.9
|
||||
- pip3 install python-gnupg==0.4.9
|
||||
post-cache:
|
||||
# build all packages (except macos and FIPS) and move them to /cfsetup/built_artifacts
|
||||
- ./build-packages.sh
|
||||
# release the packages built and moved to /cfsetup/built_artifacts
|
||||
- make github-release-built-pkgs
|
||||
# publish packages to linux repos
|
||||
- make release-pkgs-linux
|
||||
# handle FIPS separately so that we built with gofips compiler
|
||||
github-fips-release-pkgs:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- rpm
|
||||
- wget
|
||||
# libmsi and libgcab are libraries the wixl binary depends on.
|
||||
- libmsi-dev
|
||||
- libgcab-dev
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *github_release_pkgs_pre_cache
|
||||
post-cache:
|
||||
# same logic as above, but for FIPS packages only
|
||||
- ./build-packages-fips.sh
|
||||
- make github-release-built-pkgs
|
||||
build-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_deb_deps
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
@@ -22,77 +89,176 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-deb
|
||||
release-linux-amd64:
|
||||
build-fips-internal-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_fips_deb_deps
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-fips-internal-deb-nightly:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_fips_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export NIGHTLY=true
|
||||
- export FIPS=true
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-deb-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
- make cloudflared-deb
|
||||
publish-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- openssh-client
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make release
|
||||
release-linux-armv6:
|
||||
- make publish-deb
|
||||
github-release-macos-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- crossbuild-essential-armhf
|
||||
- gcc-arm-linux-gnueabihf
|
||||
- build-essential
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: &install_pygithub
|
||||
- pip3 install pynacl==1.4.0
|
||||
- pip3 install pygithub==1.55
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm
|
||||
- export CC=arm-linux-gnueabihf-gcc
|
||||
- make release
|
||||
release-linux-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-multilib
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make release
|
||||
release-windows-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
post-cache:
|
||||
- export GOOS=windows
|
||||
- export GOARCH=amd64
|
||||
- export CC=x86_64-w64-mingw32-gcc
|
||||
- make release
|
||||
release-windows-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
post-cache:
|
||||
- export GOOS=windows
|
||||
- export GOARCH=386
|
||||
- export CC=i686-w64-mingw32-gcc-win32
|
||||
- make release
|
||||
- make github-mac-upload
|
||||
test:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
pre-cache: &test_pre_cache
|
||||
- go install golang.org/x/tools/cmd/goimports@latest
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
# cd to a non-module directory: https://github.com/golang/go/issues/24250
|
||||
- (cd / && go get github.com/BurntSushi/go-sumtype)
|
||||
- export PATH="$HOME/go/bin:$PATH"
|
||||
- make test
|
||||
- ./fmt-check.sh
|
||||
- make test | gotest-to-teamcity
|
||||
test-fips:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
pre-cache: *test_pre_cache
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- export PATH="$HOME/go/bin:$PATH"
|
||||
- ./fmt-check.sh
|
||||
- make test | gotest-to-teamcity
|
||||
component-test:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- python3.7
|
||||
- python3-pip
|
||||
- python3-setuptools
|
||||
# procps installs the ps command which is needed in test_sysv_service because the init script
|
||||
# uses ps pid to determine if the agent is running
|
||||
- procps
|
||||
pre-cache-copy-paths:
|
||||
- component-tests/requirements.txt
|
||||
pre-cache:
|
||||
- sudo pip3 install --upgrade -r component-tests/requirements.txt
|
||||
post-cache:
|
||||
# Creates and routes a Named Tunnel for this build. Also constructs config file from env vars.
|
||||
- python3 component-tests/setup.py --type create
|
||||
- pytest component-tests -o log_cli=true --log-cli-level=INFO
|
||||
# The Named Tunnel is deleted and its route unprovisioned here.
|
||||
- python3 component-tests/setup.py --type cleanup
|
||||
update-homebrew:
|
||||
builddeps:
|
||||
- openssh-client
|
||||
- s3cmd
|
||||
- jq
|
||||
- build-essential
|
||||
post-cache:
|
||||
- .teamcity/update-homebrew.sh
|
||||
- .teamcity/update-homebrew-core.sh
|
||||
github-message-release:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- make github-message
|
||||
build-junos:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3
|
||||
- genisoimage
|
||||
- jetez
|
||||
pre-cache:
|
||||
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
|
||||
post-cache:
|
||||
- export GOOS=freebsd
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-junos
|
||||
publish-junos:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3
|
||||
- genisoimage
|
||||
- jetez
|
||||
- s4cmd
|
||||
pre-cache:
|
||||
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
|
||||
post-cache:
|
||||
- export GOOS=freebsd
|
||||
- export GOARCH=amd64
|
||||
- make publish-cloudflared-junos
|
||||
|
||||
jessie: *stretch
|
||||
|
||||
# cfsetup compose
|
||||
default-stack: test_dbconnect
|
||||
test_dbconnect:
|
||||
compose:
|
||||
up-args:
|
||||
- --renew-anon-volumes
|
||||
- --abort-on-container-exit
|
||||
- --exit-code-from=cloudflared
|
||||
files:
|
||||
- dbconnect_tests/dbconnect.yaml
|
||||
buster: *stretch
|
||||
bullseye: *stretch
|
||||
bookworm: *stretch
|
||||
centos-7:
|
||||
publish-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: &el7_builddeps
|
||||
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- yum upgrade -y binutils-2.27-44.base.el7.x86_64
|
||||
- wget https://go.dev/dl/go1.18.5.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.18.5.linux-amd64.tar.gz
|
||||
post-cache:
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make publish-rpm
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
# Pass the path to the executable to check for FIPS compliance
|
||||
exe=$1
|
||||
|
||||
if [ "$(go tool nm "${exe}" | grep -c '_Cfunc__goboringcrypto_')" -eq 0 ]; then
|
||||
# Asserts that executable is using FIPS-compliant boringcrypto
|
||||
echo "${exe}: missing goboring symbols" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(go tool nm "${exe}" | grep -c 'crypto/internal/boring/sig.FIPSOnly')" -eq 0 ]; then
|
||||
# Asserts that executable is using FIPS-only schemes
|
||||
echo "${exe}: missing fipsonly symbols" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${exe} is FIPS-compliant"
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<?if $(var.Platform)="x86"?>
|
||||
<?define Program_Files="ProgramFilesFolder"?>
|
||||
<?else?>
|
||||
<?define Program_Files="ProgramFiles64Folder"?>
|
||||
<?endif?>
|
||||
<?ifndef var.Version?>
|
||||
<?error Undefined Version variable?>
|
||||
<?endif?>
|
||||
<?ifndef var.Path?>
|
||||
<?error Undefined Path variable?>
|
||||
<?endif?>
|
||||
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product Id="35e5e858-9372-4449-bf73-1cd6f7267128"
|
||||
UpgradeCode="23f90fdd-9328-47ea-ab52-5380855a4b12"
|
||||
Name="cloudflared"
|
||||
Version="$(var.Version)"
|
||||
Manufacturer="cloudflare"
|
||||
Language="1033">
|
||||
|
||||
<Package InstallerVersion="200" Compressed="yes" Comments="Windows Installer Package" InstallScope="perMachine"/>
|
||||
|
||||
<Media Id="1" Cabinet="product.cab" EmbedCab="yes"/>
|
||||
|
||||
<Upgrade Id="23f90fdd-9328-47ea-ab52-5380855a4b12">
|
||||
<UpgradeVersion Minimum="$(var.Version)" OnlyDetect="yes" Property="NEWERVERSIONDETECTED"/>
|
||||
<UpgradeVersion Minimum="2020.8.0" Maximum="$(var.Version)" IncludeMinimum="yes" IncludeMaximum="no"
|
||||
Property="OLDERVERSIONBEINGUPGRADED"/>
|
||||
</Upgrade>
|
||||
<Condition Message="A newer version of this software is already installed.">NOT NEWERVERSIONDETECTED</Condition>
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<!--This specifies where the cloudflared.exe is moved to in the windows Operation System-->
|
||||
<Directory Id="$(var.Program_Files)">
|
||||
<Directory Id="INSTALLDIR" Name="cloudflared">
|
||||
<Component Id="ApplicationFiles" Guid="35e5e858-9372-4449-bf73-1cd6f7267128">
|
||||
<File Id="ApplicationFile0" Source="$(var.Path)"/>
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
<Component Id="ENVS" Guid="6bb74449-d10d-4f4a-933e-6fc9fa006eae">
|
||||
<!--Set the cloudflared bin location to the Path Environment Variable-->
|
||||
<Environment Id="ENV0"
|
||||
Name="PATH"
|
||||
Value="[INSTALLDIR]."
|
||||
Permanent="no"
|
||||
Part="last"
|
||||
Action="create"
|
||||
System="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
|
||||
|
||||
<Feature Id='Complete' Level='1'>
|
||||
<ComponentRef Id="ENVS"/>
|
||||
<ComponentRef Id='ApplicationFiles' />
|
||||
</Feature>
|
||||
|
||||
</Product>
|
||||
</Wix>
|
||||
@@ -0,0 +1,6 @@
|
||||
.\" Manpage for cloudflared.
|
||||
.TH man 1 ${DATE} "${VERSION}" "cloudflared man page"
|
||||
.SH NAME
|
||||
cloudflared \- creates a connection to the cloudflare edge network
|
||||
.SH DESCRIPTION
|
||||
cloudflared creates a persistent connection between a local service and the Cloudflare network. Once the daemon is running and the Tunnel has been configured, the local service can be locked down to only allow connections from Cloudflare.
|
||||
@@ -1,22 +1,65 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
||||
const (
|
||||
LogFieldHost = "host"
|
||||
cfAccessClientIDHeader = "Cf-Access-Client-Id"
|
||||
cfAccessClientSecretHeader = "Cf-Access-Client-Secret"
|
||||
)
|
||||
|
||||
// StartForwarder starts a client side websocket forward
|
||||
func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *zerolog.Logger) error {
|
||||
validURL, err := validation.ValidateUrl(forwarder.Listener)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
|
||||
// get the headers from the config file and add to the request
|
||||
headers := make(http.Header)
|
||||
if forwarder.TokenClientID != "" {
|
||||
headers.Set(cfAccessClientIDHeader, forwarder.TokenClientID)
|
||||
}
|
||||
|
||||
if forwarder.TokenSecret != "" {
|
||||
headers.Set(cfAccessClientSecretHeader, forwarder.TokenSecret)
|
||||
}
|
||||
|
||||
carrier.SetBastionDest(headers, forwarder.Destination)
|
||||
|
||||
options := &carrier.StartOptions{
|
||||
OriginURL: forwarder.URL,
|
||||
Headers: headers, //TODO: TUN-2688 support custom headers from config file
|
||||
}
|
||||
|
||||
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
|
||||
wsConn := carrier.NewWSConnection(log)
|
||||
|
||||
log.Info().Str(LogFieldHost, validURL.Host).Msg("Start Websocket listener")
|
||||
return carrier.StartForwarder(wsConn, validURL.Host, shutdown, options)
|
||||
}
|
||||
|
||||
// ssh will start a WS proxy server for server mode
|
||||
// or copy from stdin/stdout for client mode
|
||||
// useful for proxying other protocols (like ssh) over websockets
|
||||
// (which you can put Access in front of)
|
||||
func ssh(c *cli.Context) error {
|
||||
log := logger.CreateSSHLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
hostname, err := validation.ValidateHostname(rawHostName)
|
||||
@@ -28,37 +71,57 @@ func ssh(c *cli.Context) error {
|
||||
// get the headers from the cmdline and add them
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag))
|
||||
headers.Set(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
}
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag))
|
||||
headers.Set(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
}
|
||||
|
||||
destination := c.String(sshDestinationFlag)
|
||||
if destination != "" {
|
||||
headers.Add("CF-Access-SSH-Destination", destination)
|
||||
}
|
||||
carrier.SetBastionDest(headers, c.String(sshDestinationFlag))
|
||||
|
||||
options := &carrier.StartOptions{
|
||||
OriginURL: originURL,
|
||||
Headers: headers,
|
||||
Host: hostname,
|
||||
}
|
||||
|
||||
if connectTo := c.String(sshConnectTo); connectTo != "" {
|
||||
parts := strings.Split(connectTo, ":")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
options.OriginURL = fmt.Sprintf("https://%s", parts[0])
|
||||
case 2:
|
||||
options.OriginURL = fmt.Sprintf("https://%s:%s", parts[0], parts[1])
|
||||
case 3:
|
||||
options.OriginURL = fmt.Sprintf("https://%s:%s", parts[2], parts[1])
|
||||
options.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: parts[0],
|
||||
}
|
||||
log.Warn().Msgf("Using insecure SSL connection because SNI overridden to %s", parts[0])
|
||||
default:
|
||||
return fmt.Errorf("invalid connection override: %s", connectTo)
|
||||
}
|
||||
}
|
||||
|
||||
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
|
||||
wsConn := carrier.NewWSConnection(log)
|
||||
|
||||
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
|
||||
localForwarder, err := config.ValidateUrl(c)
|
||||
forwarder, err := config.ValidateUrl(c, true)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating origin URL")
|
||||
log.Err(err).Msg("Error validating origin URL")
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
forwarder, err := url.Parse(localForwarder)
|
||||
log.Info().Str(LogFieldHost, forwarder.Host).Msg("Start Websocket listener")
|
||||
err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating origin URL")
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
log.Err(err).Msg("Error on Websocket listener")
|
||||
}
|
||||
return carrier.StartServer(logger, forwarder.Host, shutdownC, options)
|
||||
return err
|
||||
}
|
||||
|
||||
return carrier.StartClient(logger, &carrier.StdinoutStream{}, options)
|
||||
return carrier.StartClient(wsConn, &carrier.StdinoutStream{}, options)
|
||||
}
|
||||
|
||||
func buildRequestHeaders(values []string) http.Header {
|
||||
|
||||
+158
-85
@@ -2,24 +2,27 @@ package access
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/sshgen"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
raven "github.com/getsentry/raven-go"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/sshgen"
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,12 +33,13 @@ const (
|
||||
sshTokenIDFlag = "service-token-id"
|
||||
sshTokenSecretFlag = "service-token-secret"
|
||||
sshGenCertFlag = "short-lived-cert"
|
||||
sshConnectTo = "connect-to"
|
||||
sshConfigTemplate = `
|
||||
Add to your {{.Home}}/.ssh/config:
|
||||
|
||||
Host {{.Hostname}}
|
||||
{{- if .ShortLivedCerts}}
|
||||
ProxyCommand bash -c '{{.Cloudflared}} access ssh-gen --hostname %h; ssh -tt %r@cfpipe-{{.Hostname}} >&2 <&1'
|
||||
ProxyCommand bash -c '{{.Cloudflared}} access ssh-gen --hostname %h; ssh -tt %r@cfpipe-{{.Hostname}} >&2 <&1'
|
||||
|
||||
Host cfpipe-{{.Hostname}}
|
||||
HostName {{.Hostname}}
|
||||
@@ -51,14 +55,14 @@ Host cfpipe-{{.Hostname}}
|
||||
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
|
||||
|
||||
var (
|
||||
logger = log.CreateLogger()
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
shutdownC chan struct{}
|
||||
userAgent = "DEV"
|
||||
)
|
||||
|
||||
// Init will initialize and store vars from the main program
|
||||
func Init(s, g chan struct{}) {
|
||||
shutdownC, graceShutdownC = s, g
|
||||
func Init(shutdown chan struct{}, version string) {
|
||||
shutdownC = shutdown
|
||||
userAgent = fmt.Sprintf("cloudflared/%s", version)
|
||||
}
|
||||
|
||||
// Flags return the global flags for Access related commands (hopefully none)
|
||||
@@ -71,33 +75,27 @@ func Commands() []*cli.Command {
|
||||
return []*cli.Command{
|
||||
{
|
||||
Name: "access",
|
||||
Category: "Access (BETA)",
|
||||
Aliases: []string{"forward"},
|
||||
Category: "Access",
|
||||
Usage: "access <subcommand>",
|
||||
Description: `(BETA) Cloudflare Access protects internal resources by securing, authenticating and monitoring access
|
||||
per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
|
||||
able to reach sensitive resources. The commands provided here allow you to interact with Access protected
|
||||
applications from the command line. This feature is considered beta. Your feedback is greatly appreciated!
|
||||
https://cfl.re/CLIAuthBeta`,
|
||||
Description: `Cloudflare Access protects internal resources by securing, authenticating and monitoring access
|
||||
per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
|
||||
able to reach sensitive resources. The commands provided here allow you to interact with Access protected
|
||||
applications from the command line.`,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "login",
|
||||
Action: login,
|
||||
Action: cliutil.Action(login),
|
||||
Usage: "login <url of access application>",
|
||||
Description: `The login subcommand initiates an authentication flow with your identity provider.
|
||||
The subcommand will launch a browser. For headless systems, a url is provided.
|
||||
Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT)
|
||||
scoped to your identity, the application you intend to reach, and valid for a session duration set by your
|
||||
scoped to your identity, the application you intend to reach, and valid for a session duration set by your
|
||||
administrator. cloudflared stores the token in local storage.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "url",
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "curl",
|
||||
Action: curl,
|
||||
Action: cliutil.Action(curl),
|
||||
Usage: "curl [--allow-request, -ar] <url> [<curl args>...]",
|
||||
Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-access-token
|
||||
header when using curl to reach an application behind Access.`,
|
||||
@@ -106,7 +104,7 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "token",
|
||||
Action: generateToken,
|
||||
Action: cliutil.Action(generateToken),
|
||||
Usage: "token -app=<url of access application>",
|
||||
ArgsUsage: "url of Access application",
|
||||
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
|
||||
@@ -117,45 +115,64 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh",
|
||||
Action: ssh,
|
||||
Aliases: []string{"rdp"},
|
||||
Name: "tcp",
|
||||
Action: cliutil.Action(ssh),
|
||||
Aliases: []string{"rdp", "ssh", "smb"},
|
||||
Usage: "",
|
||||
ArgsUsage: "",
|
||||
Description: `The ssh subcommand sends data over a proxy to the Cloudflare edge.`,
|
||||
Description: `The tcp subcommand sends data over a proxy to the Cloudflare edge.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: sshHostnameFlag,
|
||||
Usage: "specify the hostname of your application.",
|
||||
Name: sshHostnameFlag,
|
||||
Aliases: []string{"tunnel-host", "T"},
|
||||
Usage: "specify the hostname of your application.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshDestinationFlag,
|
||||
Usage: "specify the destination address of your SSH server.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshURLFlag,
|
||||
Usage: "specify the host:port to forward data to Cloudflare edge.",
|
||||
Name: sshURLFlag,
|
||||
Aliases: []string{"listener", "L"},
|
||||
Usage: "specify the host:port to forward data to Cloudflare edge.",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: sshHeaderFlag,
|
||||
Aliases: []string{"H"},
|
||||
Usage: "specify additional headers you wish to send.",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
&cli.StringFlag{
|
||||
Name: sshTokenIDFlag,
|
||||
Aliases: []string{"id"},
|
||||
Usage: "specify an Access service token ID you wish to use.",
|
||||
EnvVars: []string{"TUNNEL_SERVICE_TOKEN_ID"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
&cli.StringFlag{
|
||||
Name: sshTokenSecretFlag,
|
||||
Aliases: []string{"secret"},
|
||||
Usage: "specify an Access service token secret you wish to use.",
|
||||
EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logger.LogSSHDirectoryFlag,
|
||||
Aliases: []string{"logfile"}, //added to match the tunnel side
|
||||
Usage: "Save application log to this directory for reporting issues.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logger.LogSSHLevelFlag,
|
||||
Aliases: []string{"loglevel"}, //added to match the tunnel side
|
||||
Usage: "Application logging level {debug, info, warn, error, fatal}. ",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshConnectTo,
|
||||
Hidden: true,
|
||||
Usage: "Connect to alternate location for testing, value is host, host:port, or sni:port:host",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh-config",
|
||||
Action: sshConfig,
|
||||
Action: cliutil.Action(sshConfig),
|
||||
Usage: "",
|
||||
Description: `Prints an example configuration ~/.ssh/config`,
|
||||
Flags: []cli.Flag{
|
||||
@@ -171,7 +188,7 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "ssh-gen",
|
||||
Action: sshGen,
|
||||
Action: cliutil.Action(sshGen),
|
||||
Usage: "",
|
||||
Description: `Generates a short lived certificate for given hostname`,
|
||||
Flags: []cli.Flag{
|
||||
@@ -188,31 +205,44 @@ func Commands() []*cli.Command {
|
||||
|
||||
// login pops up the browser window to do the actual login and JWT generation
|
||||
func login(c *cli.Context) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
logger := log.CreateLogger()
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
args := c.Args()
|
||||
rawURL := ensureURLScheme(args.First())
|
||||
appURL, err := url.Parse(rawURL)
|
||||
if args.Len() < 1 || err != nil {
|
||||
logger.Errorf("Please provide the url of the Access application\n")
|
||||
return err
|
||||
}
|
||||
if err := verifyTokenAtEdge(appURL, c); err != nil {
|
||||
logger.WithError(err).Error("Could not verify token")
|
||||
log.Error().Msg("Please provide the url of the Access application")
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := token.GetTokenIfExists(appURL)
|
||||
if err != nil || token == "" {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
|
||||
appInfo, err := token.GetAppInfo(appURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", string(token))
|
||||
|
||||
if err := verifyTokenAtEdge(appURL, appInfo, c, log); err != nil {
|
||||
log.Err(err).Msg("Could not verify token")
|
||||
return err
|
||||
}
|
||||
|
||||
cfdToken, err := token.GetAppTokenIfExists(appInfo)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
|
||||
return err
|
||||
} else if cfdToken == "" {
|
||||
fmt.Fprintln(os.Stderr, "token for provided application was empty.")
|
||||
return errors.New("empty application token")
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", cfdToken)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureURLScheme prepends a URL with https:// if it doesnt have a scheme. http:// URLs will not be converted.
|
||||
// ensureURLScheme prepends a URL with https:// if it doesn't have a scheme. http:// URLs will not be converted.
|
||||
func ensureURLScheme(url string) string {
|
||||
url = strings.Replace(strings.ToLower(url), "http://", "https://", 1)
|
||||
if !strings.HasPrefix(url, "https://") {
|
||||
@@ -224,49 +254,85 @@ func ensureURLScheme(url string) string {
|
||||
|
||||
// curl provides a wrapper around curl, passing Access JWT along in request
|
||||
func curl(c *cli.Context) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
logger := log.CreateLogger()
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
return err
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
args := c.Args()
|
||||
if args.Len() < 1 {
|
||||
logger.Error("Please provide the access app and command you wish to run.")
|
||||
log.Error().Msg("Please provide the access app and command you wish to run.")
|
||||
return errors.New("incorrect args")
|
||||
}
|
||||
|
||||
cmdArgs, allowRequest := parseAllowRequest(args.Slice())
|
||||
appURL, err := getAppURL(cmdArgs)
|
||||
appURL, err := getAppURL(cmdArgs, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
appInfo, err := token.GetAppInfo(appURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tok, err := token.GetAppTokenIfExists(appInfo)
|
||||
if err != nil || tok == "" {
|
||||
if allowRequest {
|
||||
logger.Warn("You don't have an Access token set. Please run access token <access application> to fetch one.")
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
log.Info().Msg("You don't have an Access token set. Please run access token <access application> to fetch one.")
|
||||
return run("curl", cmdArgs...)
|
||||
}
|
||||
tok, err = token.FetchToken(appURL)
|
||||
tok, err = token.FetchToken(appURL, appInfo, log)
|
||||
if err != nil {
|
||||
logger.Error("Failed to refresh token: ", err)
|
||||
log.Err(err).Msg("Failed to refresh token")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmdArgs = append(cmdArgs, "-H")
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("cf-access-token: %s", tok))
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", carrier.CFAccessTokenHeader, tok))
|
||||
return run("curl", cmdArgs...)
|
||||
}
|
||||
|
||||
// run kicks off a shell task and pipe the results to the respective std pipes
|
||||
func run(cmd string, args ...string) error {
|
||||
c := exec.Command(cmd, args...)
|
||||
c.Stdin = os.Stdin
|
||||
stderr, err := c.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
io.Copy(os.Stderr, stderr)
|
||||
}()
|
||||
|
||||
stdout, err := c.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
io.Copy(os.Stdout, stdout)
|
||||
}()
|
||||
return c.Run()
|
||||
}
|
||||
|
||||
// token dumps provided token to stdout
|
||||
func generateToken(c *cli.Context) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
appURL, err := url.Parse(c.String("app"))
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
return err
|
||||
}
|
||||
appURL, err := url.Parse(ensureURLScheme(c.String("app")))
|
||||
if err != nil || c.NumFlags() < 1 {
|
||||
fmt.Fprintln(os.Stderr, "Please provide a url.")
|
||||
return err
|
||||
}
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
|
||||
appInfo, err := token.GetAppInfo(appURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tok, err := token.GetAppTokenIfExists(appInfo)
|
||||
if err != nil || tok == "" {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run token command to generate token.")
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run login command to generate token.")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -298,6 +364,8 @@ func sshConfig(c *cli.Context) error {
|
||||
|
||||
// sshGen generates a short lived certificate for provided hostname
|
||||
func sshGen(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
hostname, err := validation.ValidateHostname(rawHostName)
|
||||
@@ -313,28 +381,33 @@ func sshGen(c *cli.Context) error {
|
||||
// this fetchToken function mutates the appURL param. We should refactor that
|
||||
fetchTokenURL := &url.URL{}
|
||||
*fetchTokenURL = *originURL
|
||||
token, err := token.FetchToken(fetchTokenURL)
|
||||
|
||||
appInfo, err := token.GetAppInfo(fetchTokenURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sshgen.GenerateShortLivedCertificate(originURL, token); err != nil {
|
||||
if err := sshgen.GenerateShortLivedCertificate(originURL, cfdToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAppURL will pull the appURL needed for fetching a user's Access token
|
||||
func getAppURL(cmdArgs []string) (*url.URL, error) {
|
||||
// getAppURL will pull the request URL needed for fetching a user's Access token
|
||||
func getAppURL(cmdArgs []string, log *zerolog.Logger) (*url.URL, error) {
|
||||
if len(cmdArgs) < 1 {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
log.Error().Msg("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, errors.New("not a valid url")
|
||||
}
|
||||
|
||||
u, err := processURL(cmdArgs[0])
|
||||
if err != nil {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
log.Error().Msg("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -400,27 +473,27 @@ func isFileThere(candidate string) bool {
|
||||
// verifyTokenAtEdge checks for a token on disk, or generates a new one.
|
||||
// Then makes a request to to the origin with the token to ensure it is valid.
|
||||
// Returns nil if token is valid.
|
||||
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error {
|
||||
func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, log *zerolog.Logger) error {
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag))
|
||||
headers.Add(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
}
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag))
|
||||
headers.Add(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
}
|
||||
options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers}
|
||||
options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers}
|
||||
|
||||
if valid, err := isTokenValid(options); err != nil {
|
||||
if valid, err := isTokenValid(options, log); err != nil {
|
||||
return err
|
||||
} else if valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := token.RemoveTokenIfExists(appUrl); err != nil {
|
||||
if err := token.RemoveTokenIfExists(appInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if valid, err := isTokenValid(options); err != nil {
|
||||
if valid, err := isTokenValid(options, log); err != nil {
|
||||
return err
|
||||
} else if !valid {
|
||||
return errors.New("failed to verify token")
|
||||
@@ -430,12 +503,12 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error {
|
||||
}
|
||||
|
||||
// isTokenValid makes a request to the origin and returns true if the response was not a 302.
|
||||
func isTokenValid(options *carrier.StartOptions) (bool, error) {
|
||||
req, err := carrier.BuildAccessRequest(options)
|
||||
func isTokenValid(options *carrier.StartOptions, log *zerolog.Logger) (bool, error) {
|
||||
req, err := carrier.BuildAccessRequest(options, log)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "Could not create access request")
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
// Do not follow redirects
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
)
|
||||
|
||||
// ForwardServiceType is used to identify what kind of overwatch service this is
|
||||
const ForwardServiceType = "forward"
|
||||
|
||||
// ForwarderService is used to wrap the access package websocket forwarders
|
||||
// into a service model for the overwatch package.
|
||||
// it also holds a reference to the config object that represents its state
|
||||
type ForwarderService struct {
|
||||
forwarder config.Forwarder
|
||||
shutdown chan struct{}
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewForwardService creates a new forwarder service
|
||||
func NewForwardService(f config.Forwarder, log *zerolog.Logger) *ForwarderService {
|
||||
return &ForwarderService{forwarder: f, shutdown: make(chan struct{}, 1), log: log}
|
||||
}
|
||||
|
||||
// Name is used to figure out this service is related to the others (normally the addr it binds to)
|
||||
// e.g. localhost:78641 or 127.0.0.1:2222 since this is a websocket forwarder
|
||||
func (s *ForwarderService) Name() string {
|
||||
return s.forwarder.Listener
|
||||
}
|
||||
|
||||
// Type is used to identify what kind of overwatch service this is
|
||||
func (s *ForwarderService) Type() string {
|
||||
return ForwardServiceType
|
||||
}
|
||||
|
||||
// Hash is used to figure out if this forwarder is the unchanged or not from the config file updates
|
||||
func (s *ForwarderService) Hash() string {
|
||||
return s.forwarder.Hash()
|
||||
}
|
||||
|
||||
// Shutdown stops the websocket listener
|
||||
func (s *ForwarderService) Shutdown() {
|
||||
s.shutdown <- struct{}{}
|
||||
}
|
||||
|
||||
// Run is the run loop that is started by the overwatch service
|
||||
func (s *ForwarderService) Run() error {
|
||||
return access.StartForwarder(s.forwarder, s.shutdown, s.log)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
|
||||
const (
|
||||
// ResolverServiceType is used to identify what kind of overwatch service this is
|
||||
ResolverServiceType = "resolver"
|
||||
|
||||
LogFieldResolverAddress = "resolverAddress"
|
||||
LogFieldResolverPort = "resolverPort"
|
||||
LogFieldResolverMaxUpstreamConns = "resolverMaxUpstreamConns"
|
||||
)
|
||||
|
||||
// ResolverService is used to wrap the tunneldns package's DNS over HTTP
|
||||
// into a service model for the overwatch package.
|
||||
// it also holds a reference to the config object that represents its state
|
||||
type ResolverService struct {
|
||||
resolver config.DNSResolver
|
||||
shutdown chan struct{}
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewResolverService creates a new resolver service
|
||||
func NewResolverService(r config.DNSResolver, log *zerolog.Logger) *ResolverService {
|
||||
return &ResolverService{resolver: r,
|
||||
shutdown: make(chan struct{}),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Name is used to figure out this service is related to the others (normally the addr it binds to)
|
||||
// this is just "resolver" since there can only be one DNS resolver running
|
||||
func (s *ResolverService) Name() string {
|
||||
return ResolverServiceType
|
||||
}
|
||||
|
||||
// Type is used to identify what kind of overwatch service this is
|
||||
func (s *ResolverService) Type() string {
|
||||
return ResolverServiceType
|
||||
}
|
||||
|
||||
// Hash is used to figure out if this forwarder is the unchanged or not from the config file updates
|
||||
func (s *ResolverService) Hash() string {
|
||||
return s.resolver.Hash()
|
||||
}
|
||||
|
||||
// Shutdown stops the tunneldns listener
|
||||
func (s *ResolverService) Shutdown() {
|
||||
s.shutdown <- struct{}{}
|
||||
}
|
||||
|
||||
// Run is the run loop that is started by the overwatch service
|
||||
func (s *ResolverService) Run() error {
|
||||
// create a listener
|
||||
l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(),
|
||||
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.resolver.MaxUpstreamConnectionsOrDefault(), s.log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start the listener.
|
||||
readySignal := make(chan struct{})
|
||||
err = l.Start(readySignal)
|
||||
if err != nil {
|
||||
_ = l.Stop()
|
||||
return err
|
||||
}
|
||||
<-readySignal
|
||||
|
||||
resolverLog := s.log.With().
|
||||
Str(LogFieldResolverAddress, s.resolver.AddressOrDefault()).
|
||||
Uint16(LogFieldResolverPort, s.resolver.PortOrDefault()).
|
||||
Int(LogFieldResolverMaxUpstreamConns, s.resolver.MaxUpstreamConnectionsOrDefault()).
|
||||
Logger()
|
||||
|
||||
resolverLog.Info().Msg("Starting resolver")
|
||||
|
||||
// wait for shutdown signal
|
||||
<-s.shutdown
|
||||
resolverLog.Info().Msg("Shutting down resolver")
|
||||
return l.Stop()
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/overwatch"
|
||||
)
|
||||
|
||||
// AppService is the main service that runs when no command lines flags are passed to cloudflared
|
||||
// it manages all the running services such as tunnels, forwarders, DNS resolver, etc
|
||||
type AppService struct {
|
||||
configManager config.Manager
|
||||
serviceManager overwatch.Manager
|
||||
shutdownC chan struct{}
|
||||
configUpdateChan chan config.Root
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewAppService creates a new AppService with needed supporting services
|
||||
func NewAppService(configManager config.Manager, serviceManager overwatch.Manager, shutdownC chan struct{}, log *zerolog.Logger) *AppService {
|
||||
return &AppService{
|
||||
configManager: configManager,
|
||||
serviceManager: serviceManager,
|
||||
shutdownC: shutdownC,
|
||||
configUpdateChan: make(chan config.Root),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the run loop to handle config updates and run forwarders, tunnels, etc
|
||||
func (s *AppService) Run() error {
|
||||
go s.actionLoop()
|
||||
return s.configManager.Start(s)
|
||||
}
|
||||
|
||||
// Shutdown kills all the running services
|
||||
func (s *AppService) Shutdown() error {
|
||||
s.configManager.Shutdown()
|
||||
s.shutdownC <- struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigDidUpdate is a delegate notification from the config manager
|
||||
// it is trigger when the config file has been updated and now the service needs
|
||||
// to update its services accordingly
|
||||
func (s *AppService) ConfigDidUpdate(c config.Root) {
|
||||
s.configUpdateChan <- c
|
||||
}
|
||||
|
||||
// actionLoop handles the actions from running processes
|
||||
func (s *AppService) actionLoop() {
|
||||
for {
|
||||
select {
|
||||
case c := <-s.configUpdateChan:
|
||||
s.handleConfigUpdate(c)
|
||||
case <-s.shutdownC:
|
||||
for _, service := range s.serviceManager.Services() {
|
||||
service.Shutdown()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AppService) handleConfigUpdate(c config.Root) {
|
||||
// handle the client forward listeners
|
||||
activeServices := map[string]struct{}{}
|
||||
for _, f := range c.Forwarders {
|
||||
service := NewForwardService(f, s.log)
|
||||
s.serviceManager.Add(service)
|
||||
activeServices[service.Name()] = struct{}{}
|
||||
}
|
||||
|
||||
// handle resolver changes
|
||||
if c.Resolver.Enabled {
|
||||
service := NewResolverService(c.Resolver, s.log)
|
||||
s.serviceManager.Add(service)
|
||||
activeServices[service.Name()] = struct{}{}
|
||||
|
||||
}
|
||||
|
||||
// TODO: TUN-1451 - tunnels
|
||||
|
||||
// remove any services that are no longer active
|
||||
for _, service := range s.serviceManager.Services() {
|
||||
if _, ok := activeServices[service.Name()]; !ok {
|
||||
s.serviceManager.Remove(service.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package buildinfo
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type BuildInfo struct {
|
||||
GoOS string `json:"go_os"`
|
||||
GoVersion string `json:"go_version"`
|
||||
GoArch string `json:"go_arch"`
|
||||
CloudflaredVersion string `json:"cloudflared_version"`
|
||||
}
|
||||
|
||||
func GetBuildInfo(cloudflaredVersion string) *BuildInfo {
|
||||
return &BuildInfo{
|
||||
GoOS: runtime.GOOS,
|
||||
GoVersion: runtime.Version(),
|
||||
GoArch: runtime.GOARCH,
|
||||
CloudflaredVersion: cloudflaredVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Log(logger *logrus.Logger) {
|
||||
logger.Infof("Version %s", bi.CloudflaredVersion)
|
||||
logger.Infof("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type BuildInfo struct {
|
||||
GoOS string `json:"go_os"`
|
||||
GoVersion string `json:"go_version"`
|
||||
GoArch string `json:"go_arch"`
|
||||
BuildType string `json:"build_type"`
|
||||
CloudflaredVersion string `json:"cloudflared_version"`
|
||||
}
|
||||
|
||||
func GetBuildInfo(buildType, version string) *BuildInfo {
|
||||
return &BuildInfo{
|
||||
GoOS: runtime.GOOS,
|
||||
GoVersion: runtime.Version(),
|
||||
GoArch: runtime.GOARCH,
|
||||
BuildType: buildType,
|
||||
CloudflaredVersion: version,
|
||||
}
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Log(log *zerolog.Logger) {
|
||||
log.Info().Msgf("Version %s", bi.CloudflaredVersion)
|
||||
if bi.BuildType != "" {
|
||||
log.Info().Msgf("Built%s", bi.GetBuildTypeMsg())
|
||||
}
|
||||
log.Info().Msgf("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) OSArch() string {
|
||||
return fmt.Sprintf("%s_%s", bi.GoOS, bi.GoArch)
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Version() string {
|
||||
return bi.CloudflaredVersion
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) GetBuildTypeMsg() string {
|
||||
if bi.BuildType == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(" with %s", bi.BuildType)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func RemovedCommand(name string) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: name,
|
||||
Action: func(context *cli.Context) error {
|
||||
return cli.Exit(
|
||||
fmt.Sprintf("%s command is no longer supported by cloudflared. Consult Cloudflare Tunnel documentation for possible alternative solutions.", name),
|
||||
-1,
|
||||
)
|
||||
},
|
||||
Description: fmt.Sprintf("%s is deprecated", name),
|
||||
Hidden: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type usageError string
|
||||
|
||||
func (ue usageError) Error() string {
|
||||
return string(ue)
|
||||
}
|
||||
|
||||
func UsageError(format string, args ...interface{}) error {
|
||||
if len(args) == 0 {
|
||||
return usageError(format)
|
||||
} else {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
return usageError(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensures exit with error code if actionFunc returns an error
|
||||
func WithErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
err := actionFunc(ctx)
|
||||
if err != nil {
|
||||
if _, ok := err.(usageError); ok {
|
||||
msg := fmt.Sprintf("%s\nSee 'cloudflared %s --help'.", err.Error(), ctx.Command.FullName())
|
||||
err = cli.Exit(msg, -1)
|
||||
} else if _, ok := err.(cli.ExitCoder); !ok {
|
||||
err = cli.Exit(err.Error(), 1)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
func Action(actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||
return WithErrorHandler(actionFunc)
|
||||
}
|
||||
|
||||
func ConfiguredAction(actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||
// Adapt actionFunc to the type signature required by ConfiguredActionWithWarnings
|
||||
f := func(context *cli.Context, _ string) error {
|
||||
return actionFunc(context)
|
||||
}
|
||||
|
||||
return ConfiguredActionWithWarnings(f)
|
||||
}
|
||||
|
||||
// Just like ConfiguredAction, but accepts a second parameter with configuration warnings.
|
||||
func ConfiguredActionWithWarnings(actionFunc func(*cli.Context, string) error) cli.ActionFunc {
|
||||
return WithErrorHandler(func(c *cli.Context) error {
|
||||
warnings, err := setFlagsFromConfigFile(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return actionFunc(c, warnings)
|
||||
})
|
||||
}
|
||||
|
||||
func setFlagsFromConfigFile(c *cli.Context) (configWarnings string, err error) {
|
||||
const errorExitCode = 1
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
inputSource, warnings, err := config.ReadConfigFile(c, log)
|
||||
if err != nil {
|
||||
if err == config.ErrNoConfigFile {
|
||||
return "", nil
|
||||
}
|
||||
return "", cli.Exit(err, errorExitCode)
|
||||
}
|
||||
|
||||
if err := altsrc.ApplyInputSource(c, inputSource); err != nil {
|
||||
return "", cli.Exit(err, errorExitCode)
|
||||
}
|
||||
return warnings, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
)
|
||||
|
||||
func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) {
|
||||
token := c.Args().First()
|
||||
if _, err := tunnel.ParseToken(token); err != nil {
|
||||
return nil, cliutil.UsageError("Provided tunnel token is not valid (%s).", err)
|
||||
}
|
||||
|
||||
return []string{
|
||||
"tunnel", "run", "--token", token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getServiceExtraArgsFromCliArgs(c *cli.Context, log *zerolog.Logger) ([]string, error) {
|
||||
if c.NArg() > 0 {
|
||||
// currently, we only support extra args for token
|
||||
return buildArgsForToken(c, log)
|
||||
} else {
|
||||
// empty extra args
|
||||
return make([]string, 0), nil
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
)
|
||||
|
||||
var (
|
||||
// File names from which we attempt to read configuration.
|
||||
DefaultConfigFiles = []string{"config.yml", "config.yaml"}
|
||||
|
||||
// Launchd doesn't set root env variables, so there is default
|
||||
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
|
||||
DefaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/usr/local/etc/cloudflared", "/etc/cloudflared"}
|
||||
)
|
||||
|
||||
const DefaultCredentialFile = "cert.pem"
|
||||
|
||||
// FileExists checks to see if a file exist at the provided path.
|
||||
func FileExists(path string) (bool, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// ignore missing files
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
f.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FindInputSourceContext pulls the input source from the config flag.
|
||||
func FindInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
|
||||
if context.String("config") != "" {
|
||||
return altsrc.NewYamlSourceFromFile(context.String("config"))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindDefaultConfigPath returns the first path that contains a config file.
|
||||
// If none of the combination of DefaultConfigDirs and DefaultConfigFiles
|
||||
// contains a config file, return empty string.
|
||||
func FindDefaultConfigPath() string {
|
||||
for _, configDir := range DefaultConfigDirs {
|
||||
for _, configFile := range DefaultConfigFiles {
|
||||
dirPath, err := homedir.Expand(configDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dirPath, configFile)
|
||||
if ok, _ := FileExists(path); ok {
|
||||
return path
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ValidateUnixSocket ensures --unix-socket param is used exclusively
|
||||
// i.e. it fails if a user specifies both --url and --unix-socket
|
||||
func ValidateUnixSocket(c *cli.Context) (string, error) {
|
||||
if c.IsSet("unix-socket") && (c.IsSet("url") || c.NArg() > 0) {
|
||||
return "", errors.New("--unix-socket must be used exclusivly.")
|
||||
}
|
||||
return c.String("unix-socket"), nil
|
||||
}
|
||||
|
||||
// ValidateUrl will validate url flag correctness. It can be either from --url or argument
|
||||
// Notice ValidateUnixSocket, it will enforce --unix-socket is not used with --url or argument
|
||||
func ValidateUrl(c *cli.Context) (string, error) {
|
||||
var url = c.String("url")
|
||||
if c.NArg() > 0 {
|
||||
if c.IsSet("url") {
|
||||
return "", errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
|
||||
}
|
||||
url = c.Args().Get(0)
|
||||
}
|
||||
validUrl, err := validation.ValidateUrl(url)
|
||||
return validUrl, err
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows && !darwin && !linux
|
||||
// +build !windows,!darwin,!linux
|
||||
|
||||
package main
|
||||
@@ -5,9 +6,9 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
cli "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package main
|
||||
@@ -5,26 +6,30 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Commands = append(app.Commands, &cli.Command{
|
||||
Name: "service",
|
||||
Usage: "Manages the Argo Tunnel system service",
|
||||
Usage: "Manages the cloudflared system service",
|
||||
Subcommands: []*cli.Command{
|
||||
&cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as a system service",
|
||||
Action: installLinuxService,
|
||||
Usage: "Install cloudflared as a system service",
|
||||
Action: cliutil.ConfiguredAction(installLinuxService),
|
||||
},
|
||||
&cli.Command{
|
||||
{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel service",
|
||||
Action: uninstallLinuxService,
|
||||
Usage: "Uninstall the cloudflared service",
|
||||
Action: cliutil.ConfiguredAction(uninstallLinuxService),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -37,19 +42,21 @@ const (
|
||||
serviceConfigDir = "/etc/cloudflared"
|
||||
serviceConfigFile = "config.yml"
|
||||
serviceCredentialFile = "cert.pem"
|
||||
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
|
||||
cloudflaredService = "cloudflared.service"
|
||||
)
|
||||
|
||||
var systemdTemplates = []ServiceTemplate{
|
||||
{
|
||||
Path: "/etc/systemd/system/cloudflared.service",
|
||||
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredService),
|
||||
Content: `[Unit]
|
||||
Description=Argo Tunnel
|
||||
Description=cloudflared
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
TimeoutStartSec=0
|
||||
Type=notify
|
||||
ExecStart={{ .Path }} --config /etc/cloudflared/config.yml --origincert /etc/cloudflared/cert.pem --no-autoupdate
|
||||
ExecStart={{ .Path }} --no-autoupdate{{ range .ExtraArgs }} {{ . }}{{ end }}
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
@@ -60,17 +67,17 @@ WantedBy=multi-user.target
|
||||
{
|
||||
Path: "/etc/systemd/system/cloudflared-update.service",
|
||||
Content: `[Unit]
|
||||
Description=Update Argo Tunnel
|
||||
Description=Update cloudflared
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 64 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
|
||||
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
|
||||
`,
|
||||
},
|
||||
{
|
||||
Path: "/etc/systemd/system/cloudflared-update.timer",
|
||||
Content: `[Unit]
|
||||
Description=Update Argo Tunnel
|
||||
Description=Update cloudflared
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
@@ -87,7 +94,7 @@ var sysvTemplate = ServiceTemplate{
|
||||
Content: `#!/bin/sh
|
||||
# For RedHat and cousins:
|
||||
# chkconfig: 2345 99 01
|
||||
# description: Argo Tunnel agent
|
||||
# description: cloudflared
|
||||
# processname: {{.Path}}
|
||||
### BEGIN INIT INFO
|
||||
# Provides: {{.Path}}
|
||||
@@ -95,11 +102,11 @@ var sysvTemplate = ServiceTemplate{
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Argo Tunnel
|
||||
# Description: Argo Tunnel agent
|
||||
# Short-Description: cloudflared
|
||||
# Description: cloudflared agent
|
||||
### END INIT INFO
|
||||
name=$(basename $(readlink -f $0))
|
||||
cmd="{{.Path}} --config /etc/cloudflared/config.yml --origincert /etc/cloudflared/cert.pem --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s"
|
||||
cmd="{{.Path}} --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
|
||||
pid_file="/var/run/$name.pid"
|
||||
stdout_log="/var/log/$name.log"
|
||||
stderr_log="/var/log/$name.err"
|
||||
@@ -118,10 +125,6 @@ case "$1" in
|
||||
echo "Starting $name"
|
||||
$cmd >> "$stdout_log" 2>> "$stderr_log" &
|
||||
echo $! > "$pid_file"
|
||||
if ! is_running; then
|
||||
echo "Unable to start, see $stdout_log and $stderr_log"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
@@ -182,77 +185,113 @@ func isSystemd() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string) error {
|
||||
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
|
||||
return err
|
||||
}
|
||||
srcCredentialPath := filepath.Join(userConfigDir, userCredentialFile)
|
||||
destCredentialPath := filepath.Join(serviceConfigDir, serviceCredentialFile)
|
||||
if err := copyCredential(srcCredentialPath, destCredentialPath); err != nil {
|
||||
return err
|
||||
}
|
||||
srcConfigPath := filepath.Join(userConfigDir, userConfigFile)
|
||||
destConfigPath := filepath.Join(serviceConfigDir, serviceConfigFile)
|
||||
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func installLinuxService(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error determining executable path: %v", err)
|
||||
}
|
||||
templateArgs := ServiceTemplateArgs{Path: etPath}
|
||||
templateArgs := ServiceTemplateArgs{
|
||||
Path: etPath,
|
||||
}
|
||||
|
||||
userConfigDir := filepath.Dir(c.String("config"))
|
||||
userConfigFile := filepath.Base(c.String("config"))
|
||||
userCredentialFile := config.DefaultCredentialFile
|
||||
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile); err != nil {
|
||||
logger.WithError(err).Infof("Failed to copy user configuration. Before running the service, ensure that %s contains two files, %s and %s",
|
||||
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
|
||||
var extraArgsFunc func(c *cli.Context, log *zerolog.Logger) ([]string, error)
|
||||
if c.NArg() == 0 {
|
||||
extraArgsFunc = buildArgsForConfig
|
||||
} else {
|
||||
extraArgsFunc = buildArgsForToken
|
||||
}
|
||||
|
||||
extraArgs, err := extraArgsFunc(c, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
templateArgs.ExtraArgs = extraArgs
|
||||
|
||||
switch {
|
||||
case isSystemd():
|
||||
logger.Infof("Using Systemd")
|
||||
return installSystemd(&templateArgs)
|
||||
log.Info().Msgf("Using Systemd")
|
||||
err = installSystemd(&templateArgs, log)
|
||||
default:
|
||||
logger.Infof("Using Sysv")
|
||||
return installSysv(&templateArgs)
|
||||
log.Info().Msgf("Using SysV")
|
||||
err = installSysv(&templateArgs, log)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
log.Info().Msg("Linux service for cloudflared installed successfully")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs) error {
|
||||
func buildArgsForConfig(c *cli.Context, log *zerolog.Logger) ([]string, error) {
|
||||
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
src, _, err := config.ReadConfigFile(c, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// can't use context because this command doesn't define "credentials-file" flag
|
||||
configPresent := func(s string) bool {
|
||||
val, err := src.String(s)
|
||||
return err == nil && val != ""
|
||||
}
|
||||
if src.TunnelID == "" || !configPresent(tunnel.CredFileFlag) {
|
||||
return nil, fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
|
||||
tunnel: TUNNEL-UUID
|
||||
credentials-file: CREDENTIALS-FILE
|
||||
`, src.Source())
|
||||
}
|
||||
if src.Source() != serviceConfigPath {
|
||||
if exists, err := config.FileExists(serviceConfigPath); err != nil || exists {
|
||||
return nil, fmt.Errorf("Possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
|
||||
}
|
||||
|
||||
if err := copyFile(src.Source(), serviceConfigPath); err != nil {
|
||||
return nil, fmt.Errorf("failed to copy %s to %s: %w", src.Source(), serviceConfigPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return []string{
|
||||
"--config", "/etc/cloudflared/config.yml", "tunnel", "run",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
err := serviceTemplate.Generate(templateArgs)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error generating service template")
|
||||
log.Err(err).Msg("error generating service template")
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := runCommand("systemctl", "enable", "cloudflared.service"); err != nil {
|
||||
logger.WithError(err).Infof("systemctl enable cloudflared.service error")
|
||||
if err := runCommand("systemctl", "enable", cloudflaredService); err != nil {
|
||||
log.Err(err).Msgf("systemctl enable %s error", cloudflaredService)
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "start", "cloudflared-update.timer"); err != nil {
|
||||
logger.WithError(err).Infof("systemctl start cloudflared-update.timer error")
|
||||
log.Err(err).Msg("systemctl start cloudflared-update.timer error")
|
||||
return err
|
||||
}
|
||||
logger.Infof("systemctl daemon-reload")
|
||||
return runCommand("systemctl", "daemon-reload")
|
||||
if err := runCommand("systemctl", "daemon-reload"); err != nil {
|
||||
log.Err(err).Msg("systemctl daemon-reload error")
|
||||
return err
|
||||
}
|
||||
return runCommand("systemctl", "start", cloudflaredService)
|
||||
}
|
||||
|
||||
func installSysv(templateArgs *ServiceTemplateArgs) error {
|
||||
func installSysv(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
|
||||
confPath, err := sysvTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error resolving system path")
|
||||
log.Err(err).Msg("error resolving system path")
|
||||
return err
|
||||
}
|
||||
if err := sysvTemplate.Generate(templateArgs); err != nil {
|
||||
logger.WithError(err).Infof("error generating system template")
|
||||
log.Err(err).Msg("error generating system template")
|
||||
return err
|
||||
}
|
||||
for _, i := range [...]string{"2", "3", "4", "5"} {
|
||||
@@ -265,42 +304,61 @@ func installSysv(templateArgs *ServiceTemplateArgs) error {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return runCommand("service", "cloudflared", "start")
|
||||
}
|
||||
|
||||
func uninstallLinuxService(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
var err error
|
||||
switch {
|
||||
case isSystemd():
|
||||
logger.Infof("Using Systemd")
|
||||
return uninstallSystemd()
|
||||
log.Info().Msg("Using Systemd")
|
||||
err = uninstallSystemd(log)
|
||||
default:
|
||||
logger.Infof("Using Sysv")
|
||||
return uninstallSysv()
|
||||
log.Info().Msg("Using SysV")
|
||||
err = uninstallSysv(log)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
log.Info().Msg("Linux service for cloudflared uninstalled successfully")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func uninstallSystemd() error {
|
||||
if err := runCommand("systemctl", "disable", "cloudflared.service"); err != nil {
|
||||
logger.WithError(err).Infof("systemctl disable cloudflared.service error")
|
||||
func uninstallSystemd(log *zerolog.Logger) error {
|
||||
if err := runCommand("systemctl", "disable", cloudflaredService); err != nil {
|
||||
log.Err(err).Msgf("systemctl disable %s error", cloudflaredService)
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "stop", cloudflaredService); err != nil {
|
||||
log.Err(err).Msgf("systemctl stop %s error", cloudflaredService)
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "stop", "cloudflared-update.timer"); err != nil {
|
||||
logger.WithError(err).Infof("systemctl stop cloudflared-update.timer error")
|
||||
log.Err(err).Msg("systemctl stop cloudflared-update.timer error")
|
||||
return err
|
||||
}
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
if err := serviceTemplate.Remove(); err != nil {
|
||||
logger.WithError(err).Infof("error removing service template")
|
||||
log.Err(err).Msg("error removing service template")
|
||||
return err
|
||||
}
|
||||
}
|
||||
logger.Infof("Successfully uninstall cloudflared service")
|
||||
if err := runCommand("systemctl", "daemon-reload"); err != nil {
|
||||
log.Err(err).Msg("systemctl daemon-reload error")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallSysv() error {
|
||||
func uninstallSysv(log *zerolog.Logger) error {
|
||||
if err := runCommand("service", "cloudflared", "stop"); err != nil {
|
||||
log.Err(err).Msg("service cloudflared stop error")
|
||||
return err
|
||||
}
|
||||
if err := sysvTemplate.Remove(); err != nil {
|
||||
logger.WithError(err).Infof("error removing service template")
|
||||
log.Err(err).Msg("error removing service template")
|
||||
return err
|
||||
}
|
||||
for _, i := range [...]string{"2", "3", "4", "5"} {
|
||||
@@ -313,6 +371,5 @@ func uninstallSysv() error {
|
||||
continue
|
||||
}
|
||||
}
|
||||
logger.Infof("Successfully uninstall cloudflared service")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package main
|
||||
@@ -6,33 +7,35 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
launchdIdentifier = "com.cloudflare.cloudflared"
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Commands = append(app.Commands, &cli.Command{
|
||||
Name: "service",
|
||||
Usage: "Manages the Argo Tunnel launch agent",
|
||||
Usage: "Manages the cloudflared launch agent",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as an user launch agent",
|
||||
Action: installLaunchd,
|
||||
Usage: "Install cloudflared as an user launch agent",
|
||||
Action: cliutil.ConfiguredAction(installLaunchd),
|
||||
},
|
||||
{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel launch agent",
|
||||
Action: uninstallLaunchd,
|
||||
Usage: "Uninstall the cloudflared launch agent",
|
||||
Action: cliutil.ConfiguredAction(uninstallLaunchd),
|
||||
},
|
||||
},
|
||||
})
|
||||
app.Run(os.Args)
|
||||
_ = app.Run(os.Args)
|
||||
}
|
||||
|
||||
func newLaunchdTemplate(installPath, stdoutPath, stderrPath string) *ServiceTemplate {
|
||||
@@ -47,6 +50,9 @@ func newLaunchdTemplate(installPath, stdoutPath, stderrPath string) *ServiceTemp
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ .Path }}</string>
|
||||
{{- range $i, $item := .ExtraArgs}}
|
||||
<string>{{ $item }}</string>
|
||||
{{- end}}
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
@@ -60,7 +66,7 @@ func newLaunchdTemplate(installPath, stdoutPath, stderrPath string) *ServiceTemp
|
||||
<false/>
|
||||
</dict>
|
||||
<key>ThrottleInterval</key>
|
||||
<integer>20</integer>
|
||||
<integer>5</integer>
|
||||
</dict>
|
||||
</plist>`, launchdIdentifier, stdoutPath, stderrPath),
|
||||
}
|
||||
@@ -105,58 +111,72 @@ func stderrPath() (string, error) {
|
||||
}
|
||||
|
||||
func installLaunchd(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
if isRootUser() {
|
||||
logger.Infof("Installing Argo Tunnel client as a system launch daemon. " +
|
||||
"Argo Tunnel client will run at boot")
|
||||
log.Info().Msg("Installing cloudflared client as a system launch daemon. " +
|
||||
"cloudflared client will run at boot")
|
||||
} else {
|
||||
logger.Infof("Installing Argo Tunnel client as an user launch agent. " +
|
||||
"Note that Argo Tunnel client will only run when the user is logged in. " +
|
||||
"If you want to run Argo Tunnel client at boot, install with root permission. " +
|
||||
"For more information, visit https://developers.cloudflare.com/argo-tunnel/reference/service/")
|
||||
log.Info().Msg("Installing cloudflared client as an user launch agent. " +
|
||||
"Note that cloudflared client will only run when the user is logged in. " +
|
||||
"If you want to run cloudflared client at boot, install with root permission. " +
|
||||
"For more information, visit https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/run-as-service")
|
||||
}
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Error determining executable path")
|
||||
log.Err(err).Msg("Error determining executable path")
|
||||
return fmt.Errorf("Error determining executable path: %v", err)
|
||||
}
|
||||
installPath, err := installPath()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error determining install path")
|
||||
return errors.Wrap(err, "Error determining install path")
|
||||
}
|
||||
extraArgs, err := getServiceExtraArgsFromCliArgs(c, log)
|
||||
if err != nil {
|
||||
errMsg := "Unable to determine extra arguments for launch daemon"
|
||||
log.Err(err).Msg(errMsg)
|
||||
return errors.Wrap(err, errMsg)
|
||||
}
|
||||
|
||||
stdoutPath, err := stdoutPath()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error determining stdout path")
|
||||
return errors.Wrap(err, "error determining stdout path")
|
||||
}
|
||||
stderrPath, err := stderrPath()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error determining stderr path")
|
||||
return errors.Wrap(err, "error determining stderr path")
|
||||
}
|
||||
launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("error creating launchd template")
|
||||
return errors.Wrap(err, "error creating launchd template")
|
||||
}
|
||||
templateArgs := ServiceTemplateArgs{Path: etPath}
|
||||
templateArgs := ServiceTemplateArgs{Path: etPath, ExtraArgs: extraArgs}
|
||||
err = launchdTemplate.Generate(&templateArgs)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("error generating launchd template")
|
||||
log.Err(err).Msg("error generating launchd template")
|
||||
return err
|
||||
}
|
||||
plistPath, err := launchdTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error resolving launchd template path")
|
||||
log.Err(err).Msg("error resolving launchd template path")
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("Outputs are logged to %s and %s", stderrPath, stdoutPath)
|
||||
return runCommand("launchctl", "load", plistPath)
|
||||
log.Info().Msgf("Outputs are logged to %s and %s", stderrPath, stdoutPath)
|
||||
err = runCommand("launchctl", "load", plistPath)
|
||||
if err == nil {
|
||||
log.Info().Msg("MacOS service for cloudflared installed successfully")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func uninstallLaunchd(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
if isRootUser() {
|
||||
logger.Infof("Uninstalling Argo Tunnel as a system launch daemon")
|
||||
log.Info().Msg("Uninstalling cloudflared as a system launch daemon")
|
||||
} else {
|
||||
logger.Infof("Uninstalling Argo Tunnel as an user launch agent")
|
||||
log.Info().Msg("Uninstalling cloudflared as a user launch agent")
|
||||
}
|
||||
installPath, err := installPath()
|
||||
if err != nil {
|
||||
@@ -171,20 +191,20 @@ func uninstallLaunchd(c *cli.Context) error {
|
||||
return errors.Wrap(err, "error determining stderr path")
|
||||
}
|
||||
launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating launchd template")
|
||||
}
|
||||
plistPath, err := launchdTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error resolving launchd template path")
|
||||
log.Err(err).Msg("error resolving launchd template path")
|
||||
return err
|
||||
}
|
||||
err = runCommand("launchctl", "unload", plistPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error unloading")
|
||||
log.Err(err).Msg("error unloading launchd")
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("Outputs are logged to %s and %s", stderrPath, stdoutPath)
|
||||
return launchdTemplate.Remove()
|
||||
err = launchdTemplate.Remove()
|
||||
if err == nil {
|
||||
log.Info().Msg("Launchd for cloudflared was uninstalled successfully")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
+102
-40
@@ -2,20 +2,28 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
|
||||
raven "github.com/getsentry/raven-go"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/cloudflare/cloudflared/overwatch"
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/tracing"
|
||||
"github.com/cloudflare/cloudflared/watcher"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -25,7 +33,7 @@ const (
|
||||
var (
|
||||
Version = "DEV"
|
||||
BuildTime = "unknown"
|
||||
logger = log.CreateLogger()
|
||||
BuildType = ""
|
||||
// Mostly network errors that we don't want reported back to Sentry, this is done by substring match.
|
||||
ignoredErrors = []string{
|
||||
"connection reset by peer",
|
||||
@@ -40,13 +48,13 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
metrics.RegisterBuildInfo(BuildTime, Version)
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
metrics.RegisterBuildInfo(BuildType, BuildTime, Version)
|
||||
raven.SetRelease(Version)
|
||||
maxprocs.Set()
|
||||
bInfo := cliutil.GetBuildInfo(BuildType, Version)
|
||||
|
||||
// Force shutdown channel used by the app. When closed, app must terminate.
|
||||
// Windows service manager closes this channel when it receives shutdown command.
|
||||
shutdownC := make(chan struct{})
|
||||
// Graceful shutdown channel used by the app. When closed, app must terminate.
|
||||
// Graceful shutdown channel used by the app. When closed, app must terminate gracefully.
|
||||
// Windows service manager closes this channel when it receives stop command.
|
||||
graceShutdownC := make(chan struct{})
|
||||
|
||||
@@ -59,40 +67,64 @@ func main() {
|
||||
app := &cli.App{}
|
||||
app.Name = "cloudflared"
|
||||
app.Usage = "Cloudflare's command-line tool and agent"
|
||||
app.ArgsUsage = "origin-url"
|
||||
app.UsageText = "cloudflared [global options] [command] [command options]"
|
||||
app.Copyright = fmt.Sprintf(
|
||||
`(c) %d Cloudflare Inc.
|
||||
Your installation of cloudflared software constitutes a symbol of your signature indicating that you accept
|
||||
the terms of the Cloudflare License (https://developers.cloudflare.com/argo-tunnel/license/),
|
||||
the terms of the Apache License Version 2.0 (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/license),
|
||||
Terms (https://www.cloudflare.com/terms/) and Privacy Policy (https://www.cloudflare.com/privacypolicy/).`,
|
||||
time.Now().Year(),
|
||||
)
|
||||
app.Version = fmt.Sprintf("%s (built %s)", Version, BuildTime)
|
||||
app.Version = fmt.Sprintf("%s (built %s%s)", Version, BuildTime, bInfo.GetBuildTypeMsg())
|
||||
app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network.
|
||||
You can use it to authenticate a session to reach an API behind Access, route web traffic to this machine,
|
||||
and configure access control.`
|
||||
and configure access control.
|
||||
|
||||
See https://developers.cloudflare.com/cloudflare-one/connections/connect-apps for more in-depth documentation.`
|
||||
app.Flags = flags()
|
||||
app.Action = action(Version, shutdownC, graceShutdownC)
|
||||
app.Before = tunnel.Before
|
||||
app.Action = action(graceShutdownC)
|
||||
app.Commands = commands(cli.ShowVersion)
|
||||
|
||||
tunnel.Init(Version, shutdownC, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
access.Init(shutdownC, graceShutdownC)
|
||||
runApp(app, shutdownC, graceShutdownC)
|
||||
tunnel.Init(bInfo, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
access.Init(graceShutdownC, Version)
|
||||
updater.Init(Version)
|
||||
tracing.Init(Version)
|
||||
token.Init(Version)
|
||||
runApp(app, graceShutdownC)
|
||||
}
|
||||
|
||||
func commands(version func(c *cli.Context)) []*cli.Command {
|
||||
cmds := []*cli.Command{
|
||||
{
|
||||
Name: "update",
|
||||
Action: updater.Update,
|
||||
Usage: "Update the agent if a new version exists",
|
||||
ArgsUsage: " ",
|
||||
Name: "update",
|
||||
Action: cliutil.ConfiguredAction(updater.Update),
|
||||
Usage: "Update the agent if a new version exists",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "beta",
|
||||
Usage: "specify if you wish to update to the latest beta version",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force",
|
||||
Usage: "specify if you wish to force an upgrade to the latest version regardless of the current version",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "staging",
|
||||
Usage: "specify if you wish to use the staging url for updating",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "version",
|
||||
Usage: "specify a version you wish to upgrade or downgrade to",
|
||||
Hidden: false,
|
||||
},
|
||||
},
|
||||
Description: `Looks for a new version on the official download server.
|
||||
If a new version exists, updates the agent binary and quits.
|
||||
Otherwise, does nothing.
|
||||
|
||||
To determine if an update happened in a script, check for error code 64.`,
|
||||
To determine if an update happened in a script, check for error code 11.`,
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
@@ -105,6 +137,7 @@ To determine if an update happened in a script, check for error code 64.`,
|
||||
},
|
||||
}
|
||||
cmds = append(cmds, tunnel.Commands()...)
|
||||
cmds = append(cmds, proxydns.Command(false))
|
||||
cmds = append(cmds, access.Commands()...)
|
||||
return cmds
|
||||
}
|
||||
@@ -118,24 +151,20 @@ func isEmptyInvocation(c *cli.Context) bool {
|
||||
return c.NArg() == 0 && c.NumFlags() == 0
|
||||
}
|
||||
|
||||
func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
|
||||
return func(c *cli.Context) (err error) {
|
||||
func action(graceShutdownC chan struct{}) cli.ActionFunc {
|
||||
return cliutil.ConfiguredAction(func(c *cli.Context) (err error) {
|
||||
if isEmptyInvocation(c) {
|
||||
cli.ShowAppHelpAndExit(c, 1)
|
||||
return handleServiceMode(c, graceShutdownC)
|
||||
}
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC) }, nil)
|
||||
exitCode := 0
|
||||
raven.CapturePanic(func() { err = tunnel.TunnelCommand(c) }, nil)
|
||||
if err != nil {
|
||||
handleError(err)
|
||||
exitCode = 1
|
||||
captureError(err)
|
||||
}
|
||||
// we already handle error printing, so we pass an empty string so we
|
||||
// don't have to print again.
|
||||
return cli.Exit("", exitCode)
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func userHomeDir() (string, error) {
|
||||
@@ -145,14 +174,13 @@ func userHomeDir() (string, error) {
|
||||
// use with sudo.
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot determine home directory for the user")
|
||||
return "", errors.Wrap(err, "Cannot determine home directory for the user")
|
||||
}
|
||||
return homeDir, nil
|
||||
}
|
||||
|
||||
// In order to keep the amount of noise sent to Sentry low, typical network errors can be filtered out here by a substring match.
|
||||
func handleError(err error) {
|
||||
func captureError(err error) {
|
||||
errorMessage := err.Error()
|
||||
for _, ignoredErrorMessage := range ignoredErrors {
|
||||
if strings.Contains(errorMessage, ignoredErrorMessage) {
|
||||
@@ -161,3 +189,37 @@ func handleError(err error) {
|
||||
}
|
||||
raven.CaptureError(err, nil)
|
||||
}
|
||||
|
||||
// cloudflared was started without any flags
|
||||
func handleServiceMode(c *cli.Context, shutdownC chan struct{}) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.DisableTerminalLog)
|
||||
|
||||
// start the main run loop that reads from the config file
|
||||
f, err := watcher.NewFile()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Cannot load config file")
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := config.FindOrCreateConfigPath()
|
||||
configManager, err := config.NewFileManager(f, configPath, log)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Cannot setup config file for monitoring")
|
||||
return err
|
||||
}
|
||||
log.Info().Msgf("monitoring config file at: %s", configPath)
|
||||
|
||||
serviceCallback := func(t string, name string, err error) {
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("%s service: %s encountered an error", t, name)
|
||||
}
|
||||
}
|
||||
serviceManager := overwatch.NewAppManager(serviceCallback)
|
||||
|
||||
appService := NewAppService(configManager, serviceManager, shutdownC, log)
|
||||
if err := appService.Run(); err != nil {
|
||||
log.Err(err).Msg("Failed to start app service")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
// GenerateFilePathFromURL will return a filepath for given access application url
|
||||
func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
|
||||
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ok, err := config.FileExists(configPath)
|
||||
if !ok && err == nil {
|
||||
// create config directory if doesn't already exist
|
||||
err = os.Mkdir(configPath, 0700)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1)
|
||||
return filepath.Join(configPath, name), nil
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package proxydns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
|
||||
func Command(hidden bool) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "proxy-dns",
|
||||
Action: cliutil.ConfiguredAction(Run),
|
||||
|
||||
Usage: "Run a DNS over HTTPS proxy server.",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "metrics",
|
||||
Value: "localhost:",
|
||||
Usage: "Listen address for metrics reporting.",
|
||||
EnvVars: []string{"TUNNEL_METRICS"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "address",
|
||||
Usage: "Listen address for the DNS over HTTPS proxy server.",
|
||||
Value: "localhost",
|
||||
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
|
||||
},
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
&cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
|
||||
Value: 53,
|
||||
EnvVars: []string{"TUNNEL_DNS_PORT"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "upstream",
|
||||
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
|
||||
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "bootstrap",
|
||||
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"),
|
||||
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-upstream-conns",
|
||||
Usage: "Maximum concurrent connections to upstream. Setting to 0 means unlimited.",
|
||||
Value: tunneldns.MaxUpstreamConnsDefault,
|
||||
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
|
||||
},
|
||||
},
|
||||
ArgsUsage: " ", // can't be the empty string or we get the default output
|
||||
Hidden: hidden,
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements a foreground runner
|
||||
func Run(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
metricsListener, err := net.Listen("tcp", c.String("metrics"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to open the metrics listener")
|
||||
}
|
||||
|
||||
go metrics.ServeMetrics(metricsListener, nil, nil, "", nil, log)
|
||||
|
||||
listener, err := tunneldns.CreateListener(
|
||||
c.String("address"),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
uint16(c.Int("port")),
|
||||
c.StringSlice("upstream"),
|
||||
c.StringSlice("bootstrap"),
|
||||
c.Int("max-upstream-conns"),
|
||||
log,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to create the listeners")
|
||||
return err
|
||||
}
|
||||
|
||||
// Try to start the server
|
||||
readySignal := make(chan struct{})
|
||||
err = listener.Start(readySignal)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to start the listeners")
|
||||
return listener.Stop()
|
||||
}
|
||||
<-readySignal
|
||||
|
||||
// Wait for signal
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
<-signals
|
||||
|
||||
// Shut down server
|
||||
err = listener.Stop()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to stop")
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -8,10 +8,12 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"text/template"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
)
|
||||
|
||||
type ServiceTemplate struct {
|
||||
@@ -21,7 +23,8 @@ type ServiceTemplate struct {
|
||||
}
|
||||
|
||||
type ServiceTemplateArgs struct {
|
||||
Path string
|
||||
Path string
|
||||
ExtraArgs []string
|
||||
}
|
||||
|
||||
func (st *ServiceTemplate) ResolvePath() (string, error) {
|
||||
@@ -41,15 +44,26 @@ func (st *ServiceTemplate) Generate(args *ServiceTemplateArgs) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = os.Stat(resolvedPath); err == nil {
|
||||
return fmt.Errorf(serviceAlreadyExistsWarn(resolvedPath))
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
err = tmpl.Execute(&buffer, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating %s: %v", st.Path, err)
|
||||
}
|
||||
fileMode := os.FileMode(0644)
|
||||
fileMode := os.FileMode(0o644)
|
||||
if st.FileMode != 0 {
|
||||
fileMode = st.FileMode
|
||||
}
|
||||
|
||||
plistFolder := path.Dir(resolvedPath)
|
||||
err = os.MkdirAll(plistFolder, 0o755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating %s: %v", plistFolder, err)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(resolvedPath, buffer.Bytes(), fileMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing %s: %v", resolvedPath, err)
|
||||
@@ -69,26 +83,30 @@ func (st *ServiceTemplate) Remove() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func serviceAlreadyExistsWarn(service string) string {
|
||||
return fmt.Sprintf("cloudflared service is already installed at %s; if you are running a cloudflared tunnel, you "+
|
||||
"can point it to multiple origins, avoiding the need to run more than one cloudflared service in the "+
|
||||
"same machine; otherwise if you are really sure, you can do `cloudflared service uninstall` to clean "+
|
||||
"up the existing service and then try again this command",
|
||||
service,
|
||||
)
|
||||
}
|
||||
|
||||
func runCommand(command string, args ...string) error {
|
||||
cmd := exec.Command(command, args...)
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error getting stderr pipe")
|
||||
return fmt.Errorf("error getting stderr pipe: %v", err)
|
||||
}
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("error starting %s", command)
|
||||
return fmt.Errorf("error starting %s: %v", command, err)
|
||||
}
|
||||
commandErr, _ := ioutil.ReadAll(stderr)
|
||||
if len(commandErr) > 0 {
|
||||
logger.Errorf("%s: %s", command, commandErr)
|
||||
}
|
||||
|
||||
output, _ := ioutil.ReadAll(stderr)
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("%s returned error", command)
|
||||
return fmt.Errorf("%s returned with error: %v", command, err)
|
||||
return fmt.Errorf("%s %v returned with error code %v due to: %v", command, args, err, string(output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -144,12 +162,38 @@ func copyCredential(srcCredentialPath, destCredentialPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(src, dest string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
destFile, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok := false
|
||||
defer func() {
|
||||
destFile.Close()
|
||||
if !ok {
|
||||
_ = os.Remove(dest)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(destFile, srcFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ok = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyConfig(srcConfigPath, destConfigPath string) error {
|
||||
// Copy or create config
|
||||
destFile, exists, err := openFile(destConfigPath, true)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("cannot open %s", destConfigPath)
|
||||
return err
|
||||
return fmt.Errorf("cannot open %s with error: %s", destConfigPath, err)
|
||||
} else if exists {
|
||||
// config already exists, do nothing
|
||||
return nil
|
||||
@@ -173,7 +217,6 @@ func copyConfig(srcConfigPath, destConfigPath string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to copy %s to %s: %v", srcConfigPath, destConfigPath, err)
|
||||
}
|
||||
logger.Infof("Copied %s to %s", srcConfigPath, destConfigPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
//+build darwin
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return exec.Command("open", url)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//+build !windows,!darwin,!linux,!netbsd,!freebsd,!openbsd
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//+build linux freebsd openbsd netbsd
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return exec.Command("xdg-open", url)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// OpenBrowser opens the specified URL in the default browser of the user
|
||||
func OpenBrowser(url string) error {
|
||||
return getBrowserCmd(url).Start()
|
||||
}
|
||||
|
||||
// Run will kick off a shell task and pipe the results to the respective std pipes
|
||||
func Run(cmd string, args ...string) error {
|
||||
c := exec.Command(cmd, args...)
|
||||
stderr, err := c.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
io.Copy(os.Stderr, stderr)
|
||||
}()
|
||||
|
||||
stdout, err := c.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
io.Copy(os.Stdout, stdout)
|
||||
}()
|
||||
return c.Run()
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/path"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
const (
|
||||
keyName = "token"
|
||||
)
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
type lock struct {
|
||||
lockFilePath string
|
||||
backoff *origin.BackoffHandler
|
||||
sigHandler *signalHandler
|
||||
}
|
||||
|
||||
type signalHandler struct {
|
||||
sigChannel chan os.Signal
|
||||
signals []os.Signal
|
||||
}
|
||||
|
||||
func (s *signalHandler) register(handler func()) {
|
||||
s.sigChannel = make(chan os.Signal, 1)
|
||||
signal.Notify(s.sigChannel, s.signals...)
|
||||
go func(s *signalHandler) {
|
||||
for range s.sigChannel {
|
||||
handler()
|
||||
}
|
||||
}(s)
|
||||
}
|
||||
|
||||
func (s *signalHandler) deregister() {
|
||||
signal.Stop(s.sigChannel)
|
||||
close(s.sigChannel)
|
||||
}
|
||||
|
||||
func errDeleteTokenFailed(lockFilePath string) error {
|
||||
return fmt.Errorf("failed to acquire a new Access token. Please try to delete %s", lockFilePath)
|
||||
}
|
||||
|
||||
// newLock will get a new file lock
|
||||
func newLock(path string) *lock {
|
||||
lockPath := path + ".lock"
|
||||
return &lock{
|
||||
lockFilePath: lockPath,
|
||||
backoff: &origin.BackoffHandler{MaxRetries: 7},
|
||||
sigHandler: &signalHandler{
|
||||
signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lock) Acquire() error {
|
||||
// Intercept SIGINT and SIGTERM to release lock before exiting
|
||||
l.sigHandler.register(func() {
|
||||
l.deleteLockFile()
|
||||
os.Exit(0)
|
||||
})
|
||||
|
||||
// Check for a path.lock file
|
||||
// if the lock file exists; start polling
|
||||
// if not, create the lock file and go through the normal flow.
|
||||
// See AUTH-1736 for the reason why we do all this
|
||||
for isTokenLocked(l.lockFilePath) {
|
||||
if l.backoff.Backoff(context.Background()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := l.deleteLockFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create a lock file so other processes won't also try to get the token at
|
||||
// the same time
|
||||
if err := ioutil.WriteFile(l.lockFilePath, []byte{}, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *lock) deleteLockFile() error {
|
||||
if err := os.Remove(l.lockFilePath); err != nil && !os.IsNotExist(err) {
|
||||
return errDeleteTokenFailed(l.lockFilePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *lock) Release() error {
|
||||
defer l.sigHandler.deregister()
|
||||
return l.deleteLockFile()
|
||||
}
|
||||
|
||||
// isTokenLocked checks to see if there is another process attempting to get the token already
|
||||
func isTokenLocked(lockFilePath string) bool {
|
||||
exists, err := config.FileExists(lockFilePath)
|
||||
return exists && err == nil
|
||||
}
|
||||
|
||||
// FetchToken will either load a stored token or generate a new one
|
||||
func FetchToken(appURL *url.URL) (string, error) {
|
||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
path, err := path.GenerateFilePathFromURL(appURL, keyName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fileLock := newLock(path)
|
||||
|
||||
err = fileLock.Acquire()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer fileLock.Release()
|
||||
|
||||
// check to see if another process has gotten a token while we waited for the lock
|
||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// this weird parameter is the resource name (token) and the key/value
|
||||
// we want to send to the transfer service. the key is token and the value
|
||||
// is blank (basically just the id generated in the transfer service)
|
||||
token, err := transfer.Run(appURL, keyName, keyName, "", path, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
// GetTokenIfExists will return the token from local storage if it exists
|
||||
func GetTokenIfExists(url *url.URL) (string, error) {
|
||||
path, err := path.GenerateFilePathFromURL(url, keyName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token, err := jose.ParseJWT(string(content))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token.Encode(), nil
|
||||
}
|
||||
|
||||
// RemoveTokenIfExists removes the a token from local storage if it exists
|
||||
func RemoveTokenIfExists(url *url.URL) error {
|
||||
path, err := path.GenerateFilePathFromURL(url, keyName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
)
|
||||
|
||||
const (
|
||||
baseStoreURL = "https://login.argotunnel.com/"
|
||||
clientTimeout = time.Second * 60
|
||||
)
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
// Run does the transfer "dance" with the end result downloading the supported resource.
|
||||
// The expanded description is run is encapsulation of shared business logic needed
|
||||
// to request a resource (token/cert/etc) from the transfer service (loginhelper).
|
||||
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
|
||||
// the user to complete an action, while it long polls in the background waiting for an
|
||||
// action to be completed to download the resource.
|
||||
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool) ([]byte, error) {
|
||||
encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// See AUTH-1423 for why we use stderr (the way git wraps ssh)
|
||||
err = shell.OpenBrowser(requestURL)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Please open the following URL and log in with your Cloudflare account:\n\n%s\n\nLeave cloudflared running to download the %s automatically.\n", requestURL, resourceName)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "A browser window should have opened at the following URL:\n\n%s\n\nIf the browser failed to open, please visit the URL above directly in your browser.\n", requestURL)
|
||||
}
|
||||
|
||||
var resourceData []byte
|
||||
|
||||
if shouldEncrypt {
|
||||
buf, key, err := transferRequest(baseStoreURL + "transfer/" + encrypterClient.PublicKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decodedBuf, err := base64.StdEncoding.DecodeString(string(buf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decrypted, err := encrypterClient.Decrypt(decodedBuf, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resourceData = decrypted
|
||||
} else {
|
||||
buf, _, err := transferRequest(baseStoreURL + encrypterClient.PublicKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resourceData = buf
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resourceData, nil
|
||||
}
|
||||
|
||||
// BuildRequestURL creates a request suitable for a resource transfer.
|
||||
// it will return a constructed url based off the base url and query key/value provided.
|
||||
// cli will build a url for cli transfer request.
|
||||
func buildRequestURL(baseURL *url.URL, key, value string, cli bool) (string, error) {
|
||||
q := baseURL.Query()
|
||||
q.Set(key, value)
|
||||
baseURL.RawQuery = q.Encode()
|
||||
if !cli {
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url
|
||||
baseURL.RawQuery = q.Encode() // and this actual baseURL.
|
||||
baseURL.Path = "cdn-cgi/access/cli"
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
// transferRequest downloads the requested resource from the request URL
|
||||
func transferRequest(requestURL string) ([]byte, string, error) {
|
||||
client := &http.Client{Timeout: clientTimeout}
|
||||
const pollAttempts = 10
|
||||
// we do "long polling" on the endpoint to get the resource.
|
||||
for i := 0; i < pollAttempts; i++ {
|
||||
buf, key, err := poll(client, requestURL)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
} else if len(buf) > 0 {
|
||||
if err := putSuccess(client, requestURL); err != nil {
|
||||
logger.WithError(err).Error("Failed to update resource success")
|
||||
}
|
||||
return buf, key, nil
|
||||
}
|
||||
}
|
||||
return nil, "", errors.New("Failed to fetch resource")
|
||||
}
|
||||
|
||||
// poll the endpoint for the request resource, waiting for the user interaction
|
||||
func poll(client *http.Client, requestURL string) ([]byte, string, error) {
|
||||
resp, err := client.Get(requestURL)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// ignore everything other than server errors as the resource
|
||||
// may not exist until the user does the interaction
|
||||
if resp.StatusCode >= 500 {
|
||||
return nil, "", fmt.Errorf("error on request %d", resp.StatusCode)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
logger.Info("Waiting for login...")
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := io.Copy(buf, resp.Body); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return buf.Bytes(), resp.Header.Get("service-public-key"), nil
|
||||
}
|
||||
|
||||
// putSuccess tells the server we successfully downloaded the resource
|
||||
func putSuccess(client *http.Client, requestURL string) error {
|
||||
req, err := http.NewRequest("PUT", requestURL+"/ok", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("HTTP Response Status Code: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+700
-694
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDedup(t *testing.T) {
|
||||
expected := []string{"a", "b"}
|
||||
actual := dedup([]string{"a", "b", "a"})
|
||||
require.ElementsMatch(t, expected, actual)
|
||||
}
|
||||
@@ -1,44 +1,57 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/google/uuid"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/orchestration"
|
||||
"github.com/cloudflare/cloudflared/supervisor"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const LogFieldOriginCertPath = "originCertPath"
|
||||
const secretValue = "*****"
|
||||
|
||||
var (
|
||||
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
|
||||
quickStartUrl = developerPortal + "/quickstart/quickstart/"
|
||||
serviceUrl = developerPortal + "/reference/service/"
|
||||
argumentsUrl = developerPortal + "/reference/arguments/"
|
||||
|
||||
LogFieldHostname = "hostname"
|
||||
|
||||
secretFlags = [2]*altsrc.StringFlag{credentialsContentsFlag, tunnelTokenFlag}
|
||||
defaultFeatures = []string{supervisor.FeatureAllowRemoteConfig, supervisor.FeatureSerializedHeaders}
|
||||
|
||||
configFlags = []string{"autoupdate-freq", "no-autoupdate", "retries", "protocol", "loglevel", "transport-loglevel", "origincert", "metrics", "metrics-update-freq", "edge-ip-version"}
|
||||
)
|
||||
|
||||
// returns the first path that contains a cert.pem file. If none of the DefaultConfigDirs
|
||||
// returns the first path that contains a cert.pem file. If none of the DefaultConfigSearchDirectories
|
||||
// contains a cert.pem file, return empty string
|
||||
func findDefaultOriginCertPath() string {
|
||||
for _, defaultConfigDir := range config.DefaultConfigDirs {
|
||||
for _, defaultConfigDir := range config.DefaultConfigSearchDirectories() {
|
||||
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, config.DefaultCredentialFile))
|
||||
if ok, _ := config.FileExists(originCertPath); ok {
|
||||
return originCertPath
|
||||
@@ -47,39 +60,27 @@ func findDefaultOriginCertPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func generateRandomClientID(logger *logrus.Logger) (string, error) {
|
||||
func generateRandomClientID(log *zerolog.Logger) (string, error) {
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("couldn't create UUID for client ID")
|
||||
log.Error().Msgf("couldn't create UUID for client ID %s", err)
|
||||
return "", err
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func handleDeprecatedOptions(c *cli.Context) error {
|
||||
// Fail if the user provided an old authentication method
|
||||
if c.IsSet("api-key") || c.IsSet("api-email") || c.IsSet("api-ca-key") {
|
||||
logger.Error("You don't need to give us your api-key anymore. Please use the new login method. Just run cloudflared login")
|
||||
return fmt.Errorf("Client provided deprecated options")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func logClientOptions(c *cli.Context) {
|
||||
func logClientOptions(c *cli.Context, log *zerolog.Logger) {
|
||||
flags := make(map[string]interface{})
|
||||
for _, flag := range c.LocalFlagNames() {
|
||||
flags[flag] = c.Generic(flag)
|
||||
}
|
||||
|
||||
sliceFlags := []string{"header", "tag", "proxy-dns-upstream", "upstream", "edge"}
|
||||
for _, sliceFlag := range sliceFlags {
|
||||
if len(c.StringSlice(sliceFlag)) > 0 {
|
||||
flags[sliceFlag] = strings.Join(c.StringSlice(sliceFlag), ", ")
|
||||
for _, flag := range c.FlagNames() {
|
||||
if isSecretFlag(flag) {
|
||||
flags[flag] = secretValue
|
||||
} else {
|
||||
flags[flag] = c.Generic(flag)
|
||||
}
|
||||
}
|
||||
|
||||
if len(flags) > 0 {
|
||||
logger.WithFields(flags).Info("Flags")
|
||||
log.Info().Msgf("Settings: %v", flags)
|
||||
}
|
||||
|
||||
envs := make(map[string]string)
|
||||
@@ -89,43 +90,68 @@ func logClientOptions(c *cli.Context) {
|
||||
if strings.Contains(env, "TUNNEL_") {
|
||||
vars := strings.Split(env, "=")
|
||||
if len(vars) == 2 {
|
||||
envs[vars[0]] = vars[1]
|
||||
if isSecretEnvVar(vars[0]) {
|
||||
envs[vars[0]] = secretValue
|
||||
} else {
|
||||
envs[vars[0]] = vars[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(envs) > 0 {
|
||||
logger.Infof("Environmental variables %v", envs)
|
||||
log.Info().Msgf("Environmental variables %v", envs)
|
||||
}
|
||||
}
|
||||
|
||||
func dnsProxyStandAlone(c *cli.Context) bool {
|
||||
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
|
||||
}
|
||||
|
||||
func getOriginCert(c *cli.Context) ([]byte, error) {
|
||||
if c.String("origincert") == "" {
|
||||
logger.Warnf("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
|
||||
if isRunningFromTerminal() {
|
||||
logger.Errorf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
|
||||
return nil, fmt.Errorf("Client didn't specify origincert path when running from terminal")
|
||||
} else {
|
||||
logger.Errorf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
|
||||
return nil, fmt.Errorf("Client didn't specify origincert path")
|
||||
func isSecretFlag(key string) bool {
|
||||
for _, flag := range secretFlags {
|
||||
if flag.Name == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Check that the user has acquired a certificate using the login command
|
||||
originCertPath, err := homedir.Expand(c.String("origincert"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot resolve path %s", c.String("origincert"))
|
||||
return nil, fmt.Errorf("Cannot resolve path %s", c.String("origincert"))
|
||||
return false
|
||||
}
|
||||
|
||||
func isSecretEnvVar(key string) bool {
|
||||
for _, flag := range secretFlags {
|
||||
for _, secretEnvVar := range flag.EnvVars {
|
||||
if secretEnvVar == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.NamedTunnelProperties) bool {
|
||||
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world") && namedTunnel == nil)
|
||||
}
|
||||
|
||||
func findOriginCert(originCertPath string, log *zerolog.Logger) (string, error) {
|
||||
if originCertPath == "" {
|
||||
log.Info().Msgf("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigSearchDirectories())
|
||||
if isRunningFromTerminal() {
|
||||
log.Error().Msgf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
|
||||
return "", fmt.Errorf("client didn't specify origincert path when running from terminal")
|
||||
} else {
|
||||
log.Error().Msgf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
|
||||
return "", fmt.Errorf("client didn't specify origincert path")
|
||||
}
|
||||
}
|
||||
var err error
|
||||
originCertPath, err = homedir.Expand(originCertPath)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Cannot resolve origin certificate path")
|
||||
return "", fmt.Errorf("cannot resolve path %s", originCertPath)
|
||||
}
|
||||
// Check that the user has acquired a certificate using the login command
|
||||
ok, err := config.FileExists(originCertPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
|
||||
return nil, fmt.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
|
||||
log.Error().Err(err).Msgf("Cannot check if origin cert exists at path %s", originCertPath)
|
||||
return "", fmt.Errorf("cannot check if origin cert exists at path %s", originCertPath)
|
||||
}
|
||||
if !ok {
|
||||
logger.Errorf(`Cannot find a valid certificate for your origin at the path:
|
||||
log.Error().Msgf(`Cannot find a valid certificate for your origin at the path:
|
||||
|
||||
%s
|
||||
|
||||
@@ -134,161 +160,268 @@ If you don't have a certificate signed by Cloudflare, run the command:
|
||||
|
||||
%s login
|
||||
`, originCertPath, os.Args[0])
|
||||
return nil, fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
|
||||
return "", fmt.Errorf("cannot find a valid certificate at the path %s", originCertPath)
|
||||
}
|
||||
|
||||
return originCertPath, nil
|
||||
}
|
||||
|
||||
func readOriginCert(originCertPath string) ([]byte, error) {
|
||||
// Easier to send the certificate as []byte via RPC than decoding it at this point
|
||||
originCert, err := ioutil.ReadFile(originCertPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||||
return nil, fmt.Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||||
return nil, fmt.Errorf("cannot read %s to load origin certificate", originCertPath)
|
||||
}
|
||||
return originCert, nil
|
||||
}
|
||||
|
||||
func getOriginCert(originCertPath string, log *zerolog.Logger) ([]byte, error) {
|
||||
if originCertPath, err := findOriginCert(originCertPath, log); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return readOriginCert(originCertPath)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareTunnelConfig(
|
||||
c *cli.Context,
|
||||
buildInfo *buildinfo.BuildInfo,
|
||||
version string, logger,
|
||||
transportLogger *logrus.Logger,
|
||||
) (*origin.TunnelConfig, error) {
|
||||
hostname, err := validation.ValidateHostname(c.String("hostname"))
|
||||
info *cliutil.BuildInfo,
|
||||
log, logTransport *zerolog.Logger,
|
||||
observer *connection.Observer,
|
||||
namedTunnel *connection.NamedTunnelProperties,
|
||||
) (*supervisor.TunnelConfig, *orchestration.Config, error) {
|
||||
isNamedTunnel := namedTunnel != nil
|
||||
|
||||
configHostname := c.String("hostname")
|
||||
hostname, err := validation.ValidateHostname(configHostname)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Invalid hostname")
|
||||
return nil, errors.Wrap(err, "Invalid hostname")
|
||||
log.Err(err).Str(LogFieldHostname, configHostname).Msg("Invalid hostname")
|
||||
return nil, nil, errors.Wrap(err, "Invalid hostname")
|
||||
}
|
||||
isFreeTunnel := hostname == ""
|
||||
clientID := c.String("id")
|
||||
if !c.IsSet("id") {
|
||||
clientID, err = generateRandomClientID(logger)
|
||||
clientID, err = generateRandomClientID(log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Tag parse failure")
|
||||
return nil, errors.Wrap(err, "Tag parse failure")
|
||||
log.Err(err).Msg("Tag parse failure")
|
||||
return nil, nil, errors.Wrap(err, "Tag parse failure")
|
||||
}
|
||||
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
|
||||
|
||||
originURL, err := config.ValidateUrl(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating origin URL")
|
||||
return nil, errors.Wrap(err, "Error validating origin URL")
|
||||
}
|
||||
var (
|
||||
ingressRules ingress.Ingress
|
||||
classicTunnel *connection.ClassicTunnelProperties
|
||||
)
|
||||
|
||||
var originCert []byte
|
||||
if !isFreeTunnel {
|
||||
originCert, err = getOriginCert(c)
|
||||
transportProtocol := c.String("protocol")
|
||||
protocolFetcher := edgediscovery.ProtocolPercentage
|
||||
|
||||
cfg := config.GetConfiguration()
|
||||
if isNamedTunnel {
|
||||
clientUUID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error getting origin cert")
|
||||
return nil, nil, errors.Wrap(err, "can't generate connector UUID")
|
||||
}
|
||||
}
|
||||
|
||||
originCertPool, err := tlsconfig.LoadOriginCA(c, logger)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error loading cert pool")
|
||||
return nil, errors.Wrap(err, "Error loading cert pool")
|
||||
}
|
||||
|
||||
tunnelMetrics := origin.NewTunnelMetrics()
|
||||
httpTransport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
MaxIdleConns: c.Int("proxy-keepalive-connections"),
|
||||
MaxIdleConnsPerHost: c.Int("proxy-keepalive-connections"),
|
||||
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
|
||||
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")},
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: c.Duration("proxy-connect-timeout"),
|
||||
KeepAlive: c.Duration("proxy-tcp-keepalive"),
|
||||
}
|
||||
if c.Bool("proxy-no-happy-eyeballs") {
|
||||
dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs"
|
||||
}
|
||||
dialContext := dialer.DialContext
|
||||
|
||||
if c.IsSet("unix-socket") {
|
||||
unixSocket, err := config.ValidateUnixSocket(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating --unix-socket")
|
||||
return nil, errors.Wrap(err, "Error validating --unix-socket")
|
||||
log.Info().Msgf("Generated Connector ID: %s", clientUUID)
|
||||
features := append(c.StringSlice("features"), defaultFeatures...)
|
||||
if c.IsSet(TunnelTokenFlag) {
|
||||
if transportProtocol == connection.AutoSelectFlag {
|
||||
protocolFetcher = func() (edgediscovery.ProtocolPercents, error) {
|
||||
// If the Tunnel is remotely managed and no protocol is set, we prefer QUIC, but still allow fall-back.
|
||||
preferQuic := []edgediscovery.ProtocolPercent{
|
||||
{
|
||||
Protocol: connection.QUIC.String(),
|
||||
Percentage: 100,
|
||||
},
|
||||
{
|
||||
Protocol: connection.HTTP2.String(),
|
||||
Percentage: 100,
|
||||
},
|
||||
}
|
||||
return preferQuic, nil
|
||||
}
|
||||
}
|
||||
log.Info().Msg("Will be fetching remotely managed configuration from Cloudflare API. Defaulting to protocol: quic")
|
||||
}
|
||||
|
||||
logger.Infof("Proxying tunnel requests to unix:%s", unixSocket)
|
||||
httpTransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
// if --unix-socket specified, enforce network type "unix"
|
||||
return dialContext(ctx, "unix", unixSocket)
|
||||
namedTunnel.Client = tunnelpogs.ClientInfo{
|
||||
ClientID: clientUUID[:],
|
||||
Features: dedup(features),
|
||||
Version: info.Version(),
|
||||
Arch: info.OSArch(),
|
||||
}
|
||||
ingressRules, err = ingress.ParseIngress(cfg)
|
||||
if err != nil && err != ingress.ErrNoIngressRules {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !ingressRules.IsEmpty() && c.IsSet("url") {
|
||||
return nil, nil, ingress.ErrURLIncompatibleWithIngress
|
||||
}
|
||||
} else {
|
||||
logger.Infof("Proxying tunnel requests to %s", originURL)
|
||||
httpTransport.DialContext = dialContext
|
||||
|
||||
originCertPath := c.String("origincert")
|
||||
originCertLog := log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
originCert, err := getOriginCert(originCertPath, &originCertLog)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "Error getting origin cert")
|
||||
}
|
||||
|
||||
classicTunnel = &connection.ClassicTunnelProperties{
|
||||
Hostname: hostname,
|
||||
OriginCert: originCert,
|
||||
// turn off use of reconnect token and auth refresh when using named tunnels
|
||||
UseReconnectToken: !isNamedTunnel && c.Bool("use-reconnect-token"),
|
||||
}
|
||||
}
|
||||
|
||||
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
|
||||
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
|
||||
// Convert single-origin configuration into multi-origin configuration.
|
||||
if ingressRules.IsEmpty() {
|
||||
ingressRules, err = ingress.NewSingleOrigin(c, !isNamedTunnel)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = validation.ValidateHTTPService(originURL, hostname, httpTransport)
|
||||
warpRoutingEnabled := isWarpRoutingEnabled(cfg.WarpRouting, isNamedTunnel)
|
||||
protocolSelector, err := connection.NewProtocolSelector(transportProtocol, warpRoutingEnabled, namedTunnel, protocolFetcher, supervisor.ResolveTTL, log)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to connect to the origin")
|
||||
return nil, errors.Wrap(err, "unable to connect to the origin")
|
||||
return nil, nil, err
|
||||
}
|
||||
log.Info().Msgf("Initial protocol %s", protocolSelector.Current())
|
||||
|
||||
edgeTLSConfigs := make(map[connection.Protocol]*tls.Config, len(connection.ProtocolList))
|
||||
for _, p := range connection.ProtocolList {
|
||||
tlsSettings := p.TLSSettings()
|
||||
if tlsSettings == nil {
|
||||
return nil, nil, fmt.Errorf("%s has unknown TLS settings", p)
|
||||
}
|
||||
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, tlsSettings.ServerName)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
|
||||
}
|
||||
if len(tlsSettings.NextProtos) > 0 {
|
||||
edgeTLSConfig.NextProtos = tlsSettings.NextProtos
|
||||
}
|
||||
edgeTLSConfigs[p] = edgeTLSConfig
|
||||
}
|
||||
|
||||
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
|
||||
gracePeriod, err := gracePeriod(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to create TLS config to connect with edge")
|
||||
return nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
|
||||
return nil, nil, err
|
||||
}
|
||||
muxerConfig := &connection.MuxerConfig{
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
MaxHeartbeats: uint64(c.Int("heartbeat-count")),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
CompressionSetting: h2mux.CompressionSetting(uint64(c.Int("compression-quality"))),
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
}
|
||||
edgeIPVersion, err := parseConfigIPVersion(c.String("edge-ip-version"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &origin.TunnelConfig{
|
||||
BuildInfo: buildInfo,
|
||||
ClientID: clientID,
|
||||
ClientTlsConfig: httpTransport.TLSClientConfig,
|
||||
CompressionQuality: c.Uint64("compression-quality"),
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
HTTPTransport: httpTransport,
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
Hostname: hostname,
|
||||
HTTPHostHeader: c.String("http-host-header"),
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
IsFreeTunnel: isFreeTunnel,
|
||||
LBPool: c.String("lb-pool"),
|
||||
Logger: logger,
|
||||
MaxHeartbeats: c.Uint64("heartbeat-count"),
|
||||
Metrics: tunnelMetrics,
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
|
||||
OriginCert: originCert,
|
||||
OriginUrl: originURL,
|
||||
ReportedVersion: version,
|
||||
Retries: c.Uint("retries"),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
Tags: tags,
|
||||
TlsConfig: toEdgeTLSConfig,
|
||||
TransportLogger: transportLogger,
|
||||
UseDeclarativeTunnel: c.Bool("use-declarative-tunnels"),
|
||||
UseReconnectToken: c.Bool("use-reconnect-token"),
|
||||
}, nil
|
||||
tunnelConfig := &supervisor.TunnelConfig{
|
||||
GracePeriod: gracePeriod,
|
||||
ReplaceExisting: c.Bool("force"),
|
||||
OSArch: info.OSArch(),
|
||||
ClientID: clientID,
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
Region: c.String("region"),
|
||||
EdgeIPVersion: edgeIPVersion,
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
IncidentLookup: supervisor.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
LBPool: c.String("lb-pool"),
|
||||
Tags: tags,
|
||||
Log: log,
|
||||
LogTransport: logTransport,
|
||||
Observer: observer,
|
||||
ReportedVersion: info.Version(),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
Retries: uint(c.Int("retries")),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
NamedTunnel: namedTunnel,
|
||||
ClassicTunnel: classicTunnel,
|
||||
MuxerConfig: muxerConfig,
|
||||
ProtocolSelector: protocolSelector,
|
||||
EdgeTLSConfigs: edgeTLSConfigs,
|
||||
}
|
||||
orchestratorConfig := &orchestration.Config{
|
||||
Ingress: &ingressRules,
|
||||
WarpRouting: ingress.NewWarpRoutingConfig(&cfg.WarpRouting),
|
||||
ConfigurationFlags: parseConfigFlags(c),
|
||||
}
|
||||
return tunnelConfig, orchestratorConfig, nil
|
||||
}
|
||||
|
||||
func serviceDiscoverer(c *cli.Context, logger *logrus.Logger) (*edgediscovery.Edge, error) {
|
||||
// If --edge is specfied, resolve edge server addresses
|
||||
if len(c.StringSlice("edge")) > 0 {
|
||||
return edgediscovery.StaticEdge(logger, c.StringSlice("edge"))
|
||||
func parseConfigFlags(c *cli.Context) map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
for _, flag := range configFlags {
|
||||
if v := c.String(flag); c.IsSet(flag) && v != "" {
|
||||
result[flag] = v
|
||||
}
|
||||
}
|
||||
// Otherwise lookup edge server addresses through service discovery
|
||||
return edgediscovery.ResolveEdge(logger)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func gracePeriod(c *cli.Context) (time.Duration, error) {
|
||||
period := c.Duration("grace-period")
|
||||
if period > connection.MaxGracePeriod {
|
||||
return time.Duration(0), fmt.Errorf("grace-period must be equal or less than %v", connection.MaxGracePeriod)
|
||||
}
|
||||
return period, nil
|
||||
}
|
||||
|
||||
func isWarpRoutingEnabled(warpConfig config.WarpRoutingConfig, isNamedTunnel bool) bool {
|
||||
return warpConfig.Enabled && isNamedTunnel
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
// Remove any duplicates from the slice
|
||||
func dedup(slice []string) []string {
|
||||
|
||||
// Convert the slice into a set
|
||||
set := make(map[string]bool, 0)
|
||||
for _, str := range slice {
|
||||
set[str] = true
|
||||
}
|
||||
|
||||
// Convert the set back into a slice
|
||||
keys := make([]string, len(set))
|
||||
i := 0
|
||||
for str := range set {
|
||||
keys[i] = str
|
||||
i++
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// ParseConfigIPVersion returns the IP version from possible expected values from config
|
||||
func parseConfigIPVersion(version string) (v allregions.ConfigIPVersion, err error) {
|
||||
switch version {
|
||||
case "4":
|
||||
v = allregions.IPv4Only
|
||||
case "6":
|
||||
v = allregions.IPv6Only
|
||||
case "auto":
|
||||
v = allregions.Auto
|
||||
default: // unspecified or invalid
|
||||
err = fmt.Errorf("invalid value for edge-ip-version: %s", version)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+
|
||||
|
||||
package tunnel
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// CredFinder can find the tunnel credentials file.
|
||||
type CredFinder interface {
|
||||
Path() (string, error)
|
||||
}
|
||||
|
||||
// Implements CredFinder and looks for the credentials file at the given
|
||||
// filepath.
|
||||
type staticPath struct {
|
||||
filePath string
|
||||
fs fileSystem
|
||||
}
|
||||
|
||||
func newStaticPath(filePath string, fs fileSystem) CredFinder {
|
||||
return staticPath{
|
||||
filePath: filePath,
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
func (a staticPath) Path() (string, error) {
|
||||
if a.filePath != "" && a.fs.validFilePath(a.filePath) {
|
||||
return a.filePath, nil
|
||||
}
|
||||
return "", fmt.Errorf("Tunnel credentials file '%s' doesn't exist or is not a file", a.filePath)
|
||||
}
|
||||
|
||||
// Implements CredFinder and looks for the credentials file in several directories
|
||||
// searching for a file named <id>.json
|
||||
type searchByID struct {
|
||||
id uuid.UUID
|
||||
c *cli.Context
|
||||
log *zerolog.Logger
|
||||
fs fileSystem
|
||||
}
|
||||
|
||||
func newSearchByID(id uuid.UUID, c *cli.Context, log *zerolog.Logger, fs fileSystem) CredFinder {
|
||||
return searchByID{
|
||||
id: id,
|
||||
c: c,
|
||||
log: log,
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
func (s searchByID) Path() (string, error) {
|
||||
originCertPath := s.c.String("origincert")
|
||||
originCertLog := s.log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
// Fallback to look for tunnel credentials in the origin cert directory
|
||||
if originCertPath, err := findOriginCert(originCertPath, &originCertLog); err == nil {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
if filePath, err := tunnelFilePath(s.id, originCertDir); err == nil {
|
||||
if s.fs.validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort look under default config directories
|
||||
for _, configDir := range config.DefaultConfigSearchDirectories() {
|
||||
if filePath, err := tunnelFilePath(s.id, configDir); err == nil {
|
||||
if s.fs.validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("tunnel credentials file not found")
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Abstract away details of reading files, so that SubcommandContext can read
|
||||
// from either the real filesystem, or a mock (when running unit tests).
|
||||
type fileSystem interface {
|
||||
readFile(filePath string) ([]byte, error)
|
||||
validFilePath(path string) bool
|
||||
}
|
||||
|
||||
type realFileSystem struct{}
|
||||
|
||||
func (fs realFileSystem) validFilePath(path string) bool {
|
||||
fileStat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !fileStat.IsDir()
|
||||
}
|
||||
|
||||
func (fs realFileSystem) readFile(filePath string) ([]byte, error) {
|
||||
return ioutil.ReadFile(filePath)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Connectors []*cfapi.ActiveClient `json:"conns"`
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const ingressDataJSONFlagName = "json"
|
||||
|
||||
var ingressDataJSON = &cli.StringFlag{
|
||||
Name: ingressDataJSONFlagName,
|
||||
Aliases: []string{"j"},
|
||||
Usage: `Accepts data in the form of json as an input rather than read from a file`,
|
||||
EnvVars: []string{"TUNNEL_INGRESS_VALIDATE_JSON"},
|
||||
}
|
||||
|
||||
func buildIngressSubcommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ingress",
|
||||
Category: "Tunnel",
|
||||
Usage: "Validate and test cloudflared tunnel's ingress configuration",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] ingress COMMAND [arguments...]",
|
||||
Hidden: true,
|
||||
Description: ` Cloudflared lets you route traffic from the internet to multiple different addresses on your
|
||||
origin. Multiple-origin routing is configured by a set of rules. Each rule matches traffic
|
||||
by its hostname or path, and routes it to an address. These rules are configured under the
|
||||
'ingress' key of your config.yaml, for example:
|
||||
|
||||
ingress:
|
||||
- hostname: www.example.com
|
||||
service: https://localhost:8000
|
||||
- hostname: *.example.xyz
|
||||
path: /[a-zA-Z]+.html
|
||||
service: https://localhost:8001
|
||||
- hostname: *
|
||||
service: https://localhost:8002
|
||||
|
||||
To ensure cloudflared can route all incoming requests, the last rule must be a catch-all
|
||||
rule that matches all traffic. You can validate these rules with the 'ingress validate'
|
||||
command, and test which rule matches a particular URL with 'ingress rule <URL>'.
|
||||
|
||||
Multiple-origin routing is incompatible with the --url flag.`,
|
||||
Subcommands: []*cli.Command{buildValidateIngressCommand(), buildTestURLCommand()},
|
||||
}
|
||||
}
|
||||
|
||||
func buildValidateIngressCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "validate",
|
||||
Action: cliutil.ConfiguredActionWithWarnings(validateIngressCommand),
|
||||
Usage: "Validate the ingress configuration ",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
|
||||
Description: "Validates the configuration file, ensuring your ingress rules are OK.",
|
||||
Flags: []cli.Flag{ingressDataJSON},
|
||||
}
|
||||
}
|
||||
|
||||
func buildTestURLCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "rule",
|
||||
Action: cliutil.ConfiguredAction(testURLCommand),
|
||||
Usage: "Check which ingress rule matches a given request URL",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] ingress rule URL",
|
||||
ArgsUsage: "URL",
|
||||
Description: "Check which ingress rule matches a given request URL. " +
|
||||
"Ingress rules match a request's hostname and path. Hostname is " +
|
||||
"optional and is either a full hostname like `www.example.com` or a " +
|
||||
"hostname with a `*` for its subdomains, e.g. `*.example.com`. Path " +
|
||||
"is optional and matches a regular expression, like `/[a-zA-Z0-9_]+.html`",
|
||||
}
|
||||
}
|
||||
|
||||
// validateIngressCommand check the syntax of the ingress rules in the cloudflared config file
|
||||
func validateIngressCommand(c *cli.Context, warnings string) error {
|
||||
conf, err := getConfiguration(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ingress.ParseIngress(conf); err != nil {
|
||||
return errors.Wrap(err, "Validation failed")
|
||||
}
|
||||
if c.IsSet("url") {
|
||||
return ingress.ErrURLIncompatibleWithIngress
|
||||
}
|
||||
if warnings != "" {
|
||||
fmt.Println("Warning: unused keys detected in your config file. Here is a list of unused keys:")
|
||||
fmt.Println(warnings)
|
||||
return nil
|
||||
}
|
||||
fmt.Println("OK")
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConfiguration(c *cli.Context) (*config.Configuration, error) {
|
||||
var conf *config.Configuration
|
||||
if c.IsSet(ingressDataJSONFlagName) {
|
||||
ingressJSON := c.String(ingressDataJSONFlagName)
|
||||
fmt.Println("Validating rules from cmdline flag --json")
|
||||
err := json.Unmarshal([]byte(ingressJSON), &conf)
|
||||
return conf, err
|
||||
}
|
||||
conf = config.GetConfiguration()
|
||||
if conf.Source() == "" {
|
||||
return nil, errors.New("No configuration file was found. Please create one, or use the --config flag to specify its filepath. You can use the help command to learn more about configuration files")
|
||||
}
|
||||
fmt.Println("Validating rules from", conf.Source())
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// testURLCommand checks which ingress rule matches the given URL.
|
||||
func testURLCommand(c *cli.Context) error {
|
||||
requestArg := c.Args().First()
|
||||
if requestArg == "" {
|
||||
return errors.New("cloudflared tunnel rule expects a single argument, the URL to test")
|
||||
}
|
||||
|
||||
requestURL, err := url.Parse(requestArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s is not a valid URL", requestArg)
|
||||
}
|
||||
if requestURL.Hostname() == "" && requestURL.Scheme == "" {
|
||||
return fmt.Errorf("%s doesn't have a hostname, consider adding a scheme", requestArg)
|
||||
}
|
||||
|
||||
conf := config.GetConfiguration()
|
||||
fmt.Println("Using rules from", conf.Source())
|
||||
ing, err := ingress.ParseIngress(conf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Validation failed")
|
||||
}
|
||||
|
||||
_, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path)
|
||||
fmt.Printf("Matched rule #%d\n", i+1)
|
||||
fmt.Println(ing.Rules[i].MultiLineString())
|
||||
return nil
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/rifflock/lfshook"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const debugLevelWarning = "At debug level, request URL, method, protocol, content legnth and header will be logged. " +
|
||||
"Response status, content length and header will also be logged in debug level."
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
func configMainLogger(c *cli.Context) error {
|
||||
logLevel, err := logrus.ParseLevel(c.String("loglevel"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Unknown logging level specified")
|
||||
return errors.Wrap(err, "Unknown logging level specified")
|
||||
}
|
||||
logger.SetLevel(logLevel)
|
||||
if logLevel == logrus.DebugLevel {
|
||||
logger.Warn(debugLevelWarning)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func configTransportLogger(c *cli.Context) (*logrus.Logger, error) {
|
||||
transportLogLevel, err := logrus.ParseLevel(c.String("transport-loglevel"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Fatal("Unknown transport logging level specified")
|
||||
return nil, errors.Wrap(err, "Unknown transport logging level specified")
|
||||
}
|
||||
transportLogger := logrus.New()
|
||||
transportLogger.Level = transportLogLevel
|
||||
return transportLogger, nil
|
||||
}
|
||||
|
||||
func initLogFile(c *cli.Context, loggers ...*logrus.Logger) error {
|
||||
filePath, err := homedir.Expand(c.String("logfile"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot resolve logfile path")
|
||||
}
|
||||
|
||||
fileMode := os.O_WRONLY | os.O_APPEND | os.O_CREATE | os.O_TRUNC
|
||||
// do not truncate log file if the client has been autoupdated
|
||||
if c.Bool("is-autoupdated") {
|
||||
fileMode = os.O_WRONLY | os.O_APPEND | os.O_CREATE
|
||||
}
|
||||
f, err := os.OpenFile(filePath, fileMode, 0664)
|
||||
if err != nil {
|
||||
errors.Wrap(err, fmt.Sprintf("Cannot open file %s", filePath))
|
||||
}
|
||||
defer f.Close()
|
||||
pathMap := lfshook.PathMap{
|
||||
logrus.DebugLevel: filePath,
|
||||
logrus.WarnLevel: filePath,
|
||||
logrus.InfoLevel: filePath,
|
||||
logrus.ErrorLevel: filePath,
|
||||
logrus.FatalLevel: filePath,
|
||||
logrus.PanicLevel: filePath,
|
||||
}
|
||||
|
||||
for _, l := range loggers {
|
||||
l.Hooks.Add(lfshook.NewHook(pathMap, &logrus.JSONFormatter{}))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,23 +2,40 @@ package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
)
|
||||
|
||||
const (
|
||||
baseLoginURL = "https://dash.cloudflare.com/argotunnel"
|
||||
callbackStoreURL = "https://login.argotunnel.com/"
|
||||
callbackStoreURL = "https://login.cloudflareaccess.org/"
|
||||
)
|
||||
|
||||
func buildLoginSubcommand(hidden bool) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "login",
|
||||
Action: cliutil.ConfiguredAction(login),
|
||||
Usage: "Generate a configuration file with your login details",
|
||||
ArgsUsage: " ",
|
||||
Hidden: hidden,
|
||||
}
|
||||
}
|
||||
|
||||
func login(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
path, ok, err := checkForExistingCert()
|
||||
if ok {
|
||||
fmt.Fprintf(os.Stdout, "You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path)
|
||||
@@ -33,18 +50,30 @@ func login(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false)
|
||||
resourceData, err := token.RunTransfer(
|
||||
loginURL,
|
||||
"cert",
|
||||
"callback",
|
||||
callbackStoreURL,
|
||||
false,
|
||||
false,
|
||||
log,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stdout, "You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkForExistingCert() (string, bool, error) {
|
||||
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
|
||||
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
)
|
||||
|
||||
const httpTimeout = 15 * time.Second
|
||||
|
||||
const disclaimer = "Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to" +
|
||||
" experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee. If you " +
|
||||
"intend to use Tunnels in production you should use a pre-created named tunnel by following: " +
|
||||
"https://developers.cloudflare.com/cloudflare-one/connections/connect-apps"
|
||||
|
||||
// RunQuickTunnel requests a tunnel from the specified service.
|
||||
// We use this to power quick tunnels on trycloudflare.com, but the
|
||||
// service is open-source and could be used by anyone.
|
||||
func RunQuickTunnel(sc *subcommandContext) error {
|
||||
sc.log.Info().Msg(disclaimer)
|
||||
sc.log.Info().Msg("Requesting new quick Tunnel on trycloudflare.com...")
|
||||
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSHandshakeTimeout: httpTimeout,
|
||||
ResponseHeaderTimeout: httpTimeout,
|
||||
},
|
||||
Timeout: httpTimeout,
|
||||
}
|
||||
|
||||
resp, err := client.Post(fmt.Sprintf("%s/tunnel", sc.c.String("quick-service")), "application/json", nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to request quick Tunnel")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var data QuickTunnelResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return errors.Wrap(err, "failed to unmarshal quick Tunnel")
|
||||
}
|
||||
|
||||
tunnelID, err := uuid.Parse(data.Result.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse quick Tunnel ID")
|
||||
}
|
||||
|
||||
credentials := connection.Credentials{
|
||||
AccountTag: data.Result.AccountTag,
|
||||
TunnelSecret: data.Result.Secret,
|
||||
TunnelID: tunnelID,
|
||||
}
|
||||
|
||||
url := data.Result.Hostname
|
||||
if !strings.HasPrefix(url, "https://") {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
for _, line := range AsciiBox([]string{
|
||||
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
|
||||
url,
|
||||
}, 2) {
|
||||
sc.log.Info().Msg(line)
|
||||
}
|
||||
|
||||
if !sc.c.IsSet("protocol") {
|
||||
sc.c.Set("protocol", "quic")
|
||||
}
|
||||
|
||||
return StartServer(
|
||||
sc.c,
|
||||
buildInfo,
|
||||
&connection.NamedTunnelProperties{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
|
||||
sc.log,
|
||||
)
|
||||
}
|
||||
|
||||
type QuickTunnelResponse struct {
|
||||
Success bool
|
||||
Result QuickTunnel
|
||||
Errors []QuickTunnelError
|
||||
}
|
||||
|
||||
type QuickTunnelError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
type QuickTunnel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Hostname string `json:"hostname"`
|
||||
AccountTag string `json:"account_tag"`
|
||||
Secret []byte `json:"secret"`
|
||||
}
|
||||
|
||||
// Print out the given lines in a nice ASCII box.
|
||||
func AsciiBox(lines []string, padding int) (box []string) {
|
||||
maxLen := maxLen(lines)
|
||||
spacer := strings.Repeat(" ", padding)
|
||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||
box = append(box, border)
|
||||
for _, line := range lines {
|
||||
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
|
||||
}
|
||||
box = append(box, border)
|
||||
return
|
||||
}
|
||||
|
||||
func maxLen(lines []string) int {
|
||||
max := 0
|
||||
for _, line := range lines {
|
||||
if len(line) > max {
|
||||
max = len(line)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
@@ -1,33 +1,37 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{}) error {
|
||||
func runDNSProxyServer(c *cli.Context, dnsReadySignal chan struct{}, shutdownC <-chan struct{}, log *zerolog.Logger) error {
|
||||
port := c.Int("proxy-dns-port")
|
||||
if port <= 0 || port > 65535 {
|
||||
logger.Errorf("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
|
||||
return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
|
||||
}
|
||||
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"))
|
||||
maxUpstreamConnections := c.Int("proxy-dns-max-upstream-conns")
|
||||
if maxUpstreamConnections < 0 {
|
||||
return fmt.Errorf("'%s' must be 0 or higher", "proxy-dns-max-upstream-conns")
|
||||
}
|
||||
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), maxUpstreamConnections, log)
|
||||
if err != nil {
|
||||
close(dnsReadySignal)
|
||||
listener.Stop()
|
||||
logger.WithError(err).Error("Cannot create the DNS over HTTPS proxy server")
|
||||
return errors.Wrap(err, "Cannot create the DNS over HTTPS proxy server")
|
||||
}
|
||||
|
||||
err = listener.Start(dnsReadySignal)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot start the DNS over HTTPS proxy server")
|
||||
return errors.Wrap(err, "Cannot start the DNS over HTTPS proxy server")
|
||||
}
|
||||
<-shutdownC
|
||||
listener.Stop()
|
||||
_ = listener.Stop()
|
||||
log.Info().Msg("DNS server stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,76 +4,20 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// waitForSignal notifies all routines to shutdownC immediately by closing the
|
||||
// shutdownC when one of the routines in main exits, or when this process receives
|
||||
// SIGTERM/SIGINT
|
||||
func waitForSignal(errC chan error, shutdownC chan struct{}) error {
|
||||
// waitForSignal closes graceShutdownC to indicate that we should start graceful shutdown sequence
|
||||
func waitForSignal(graceShutdownC chan struct{}, logger *zerolog.Logger) {
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
|
||||
select {
|
||||
case err := <-errC:
|
||||
close(shutdownC)
|
||||
return err
|
||||
case <-signals:
|
||||
close(shutdownC)
|
||||
case <-shutdownC:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForSignalWithGraceShutdown notifies all routines to shutdown immediately
|
||||
// by closing the shutdownC when one of the routines in main exits.
|
||||
// When this process recieves SIGTERM/SIGINT, it closes the graceShutdownC to
|
||||
// notify certain routines to start graceful shutdown. When grace period is over,
|
||||
// or when some routine exits, it notifies the rest of the routines to shutdown
|
||||
// immediately by closing shutdownC.
|
||||
// In the case of handling commands from Windows Service Manager, closing graceShutdownC
|
||||
// initiate graceful shutdown.
|
||||
func waitForSignalWithGraceShutdown(errC chan error,
|
||||
shutdownC, graceShutdownC chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
) error {
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
|
||||
select {
|
||||
case err := <-errC:
|
||||
case s := <-signals:
|
||||
logger.Info().Msgf("Initiating graceful shutdown due to signal %s ...", s)
|
||||
close(graceShutdownC)
|
||||
close(shutdownC)
|
||||
return err
|
||||
case <-signals:
|
||||
close(graceShutdownC)
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod)
|
||||
case <-graceShutdownC:
|
||||
waitForGracePeriod(signals, errC, shutdownC, gracePeriod)
|
||||
case <-shutdownC:
|
||||
close(graceShutdownC)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForGracePeriod(signals chan os.Signal,
|
||||
errC chan error,
|
||||
shutdownC chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
) {
|
||||
logger.Infof("Initiating graceful shutdown...")
|
||||
// Unregister signal handler early, so the client can send a second SIGTERM/SIGINT
|
||||
// to force shutdown cloudflared
|
||||
signal.Stop(signals)
|
||||
graceTimerTick := time.Tick(gracePeriod)
|
||||
// send close signal via shutdownC when grace period expires or when an
|
||||
// error is encountered.
|
||||
select {
|
||||
case <-graceTimerTick:
|
||||
case <-errC:
|
||||
}
|
||||
close(shutdownC)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -17,136 +22,85 @@ var (
|
||||
graceShutdownErr = fmt.Errorf("receive grace shutdown")
|
||||
)
|
||||
|
||||
func testChannelClosed(t *testing.T, c chan struct{}) {
|
||||
func channelClosed(c chan struct{}) bool {
|
||||
select {
|
||||
case <-c:
|
||||
return
|
||||
return true
|
||||
default:
|
||||
t.Fatal("Channel should be closed")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForSignal(t *testing.T) {
|
||||
// Test handling server error
|
||||
errC := make(chan error)
|
||||
shutdownC := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
errC <- serverErr
|
||||
}()
|
||||
|
||||
// received error, shutdownC should be closed
|
||||
err := waitForSignal(errC, shutdownC)
|
||||
assert.Equal(t, serverErr, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
func TestSignalShutdown(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
|
||||
// Test handling SIGTERM & SIGINT
|
||||
for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} {
|
||||
errC = make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
|
||||
go func(shutdownC chan struct{}) {
|
||||
<-shutdownC
|
||||
errC <- shutdownErr
|
||||
}(shutdownC)
|
||||
graceShutdownC := make(chan struct{})
|
||||
|
||||
go func(sig syscall.Signal) {
|
||||
// sleep for a tick to prevent sending signal before calling waitForSignal
|
||||
time.Sleep(tick)
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
_ = syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignal(errC, shutdownC)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, shutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
time.AfterFunc(time.Second, func() {
|
||||
select {
|
||||
case <-graceShutdownC:
|
||||
default:
|
||||
close(graceShutdownC)
|
||||
t.Fatal("waitForSignal timed out")
|
||||
}
|
||||
})
|
||||
|
||||
waitForSignal(graceShutdownC, &log)
|
||||
assert.True(t, channelClosed(graceShutdownC))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForSignalWithGraceShutdown(t *testing.T) {
|
||||
// Test server returning error
|
||||
errC := make(chan error)
|
||||
shutdownC := make(chan struct{})
|
||||
graceshutdownC := make(chan struct{})
|
||||
func TestWaitForShutdown(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
|
||||
errC := make(chan error)
|
||||
graceShutdownC := make(chan struct{})
|
||||
const gracePeriod = 5 * time.Second
|
||||
|
||||
contextCancelled := false
|
||||
cancel := func() {
|
||||
contextCancelled = true
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// on, error stop immediately
|
||||
contextCancelled = false
|
||||
startTime := time.Now()
|
||||
go func() {
|
||||
errC <- serverErr
|
||||
}()
|
||||
|
||||
// received error, both shutdownC and graceshutdownC should be closed
|
||||
err := waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
err := waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, &log)
|
||||
assert.Equal(t, serverErr, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
assert.True(t, contextCancelled)
|
||||
assert.False(t, channelClosed(graceShutdownC))
|
||||
assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early
|
||||
|
||||
// shutdownC closed, graceshutdownC should also be closed and no error
|
||||
errC = make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
close(shutdownC)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
assert.NoError(t, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
// on graceful shutdown, ignore error but stop as soon as an error arrives
|
||||
contextCancelled = false
|
||||
startTime = time.Now()
|
||||
go func() {
|
||||
close(graceShutdownC)
|
||||
time.Sleep(tick)
|
||||
errC <- serverErr
|
||||
}()
|
||||
err = waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, &log)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, contextCancelled)
|
||||
assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early
|
||||
|
||||
// graceshutdownC closed, shutdownC should also be closed and no error
|
||||
errC = make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
close(graceshutdownC)
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
assert.NoError(t, err)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
|
||||
// Test handling SIGTERM & SIGINT
|
||||
for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} {
|
||||
errC := make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
|
||||
go func(shutdownC, graceshutdownC chan struct{}) {
|
||||
<-graceshutdownC
|
||||
<-shutdownC
|
||||
errC <- graceShutdownErr
|
||||
}(shutdownC, graceshutdownC)
|
||||
|
||||
go func(sig syscall.Signal) {
|
||||
// sleep for a tick to prevent sending signal before calling waitForSignalWithGraceShutdown
|
||||
time.Sleep(tick)
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, graceShutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
}
|
||||
|
||||
// Test handling SIGTERM & SIGINT, server send error before end of grace period
|
||||
for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} {
|
||||
errC := make(chan error)
|
||||
shutdownC = make(chan struct{})
|
||||
graceshutdownC = make(chan struct{})
|
||||
|
||||
go func(shutdownC, graceshutdownC chan struct{}) {
|
||||
<-graceshutdownC
|
||||
errC <- graceShutdownErr
|
||||
<-shutdownC
|
||||
errC <- shutdownErr
|
||||
}(shutdownC, graceshutdownC)
|
||||
|
||||
go func(sig syscall.Signal) {
|
||||
// sleep for a tick to prevent sending signal before calling waitForSignalWithGraceShutdown
|
||||
time.Sleep(tick)
|
||||
syscall.Kill(syscall.Getpid(), sig)
|
||||
}(sig)
|
||||
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, shutdownErr, <-errC)
|
||||
testChannelClosed(t, shutdownC)
|
||||
testChannelClosed(t, graceshutdownC)
|
||||
}
|
||||
// with graceShutdownC closed stop right away without grace period
|
||||
contextCancelled = false
|
||||
startTime = time.Now()
|
||||
err = waitToShutdown(&wg, cancel, errC, graceShutdownC, 0, &log)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, contextCancelled)
|
||||
assert.True(t, time.Now().Sub(startTime) < time.Second) // check that wait ended early
|
||||
}
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/certutil"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
type errInvalidJSONCredential struct {
|
||||
err error
|
||||
path string
|
||||
}
|
||||
|
||||
func (e errInvalidJSONCredential) Error() string {
|
||||
return "Invalid JSON when parsing tunnel credentials file"
|
||||
}
|
||||
|
||||
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to
|
||||
// pass between subcommands, and make sure they are only initialized once
|
||||
type subcommandContext struct {
|
||||
c *cli.Context
|
||||
log *zerolog.Logger
|
||||
fs fileSystem
|
||||
|
||||
// These fields should be accessed using their respective Getter
|
||||
tunnelstoreClient cfapi.Client
|
||||
userCredential *userCredential
|
||||
}
|
||||
|
||||
func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
|
||||
return &subcommandContext{
|
||||
c: c,
|
||||
log: logger.CreateLoggerFromContext(c, logger.EnableTerminalLog),
|
||||
fs: realFileSystem{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Returns something that can find the given tunnel's credentials file.
|
||||
func (sc *subcommandContext) credentialFinder(tunnelID uuid.UUID) CredFinder {
|
||||
if path := sc.c.String(CredFileFlag); path != "" {
|
||||
return newStaticPath(path, sc.fs)
|
||||
}
|
||||
return newSearchByID(tunnelID, sc.c, sc.log, sc.fs)
|
||||
}
|
||||
|
||||
type userCredential struct {
|
||||
cert *certutil.OriginCert
|
||||
certPath string
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) client() (cfapi.Client, error) {
|
||||
if sc.tunnelstoreClient != nil {
|
||||
return sc.tunnelstoreClient, nil
|
||||
}
|
||||
credential, err := sc.credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userAgent := fmt.Sprintf("cloudflared/%s", buildInfo.Version())
|
||||
client, err := cfapi.NewRESTClient(
|
||||
sc.c.String("api-url"),
|
||||
credential.cert.AccountID,
|
||||
credential.cert.ZoneID,
|
||||
credential.cert.ServiceKey,
|
||||
userAgent,
|
||||
sc.log,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sc.tunnelstoreClient = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) credential() (*userCredential, error) {
|
||||
if sc.userCredential == nil {
|
||||
originCertPath := sc.c.String("origincert")
|
||||
originCertLog := sc.log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
originCertPath, err := findOriginCert(originCertPath, &originCertLog)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
blocks, err := readOriginCert(originCertPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
|
||||
}
|
||||
|
||||
cert, err := certutil.DecodeOriginCert(blocks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error decoding origin cert")
|
||||
}
|
||||
|
||||
if cert.AccountID == "" {
|
||||
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
|
||||
}
|
||||
|
||||
sc.userCredential = &userCredential{
|
||||
cert: cert,
|
||||
certPath: originCertPath,
|
||||
}
|
||||
}
|
||||
return sc.userCredential, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (connection.Credentials, error) {
|
||||
filePath, err := credFinder.Path()
|
||||
if err != nil {
|
||||
return connection.Credentials{}, err
|
||||
}
|
||||
body, err := sc.fs.readFile(filePath)
|
||||
if err != nil {
|
||||
return connection.Credentials{}, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
|
||||
}
|
||||
|
||||
var credentials connection.Credentials
|
||||
if err = json.Unmarshal(body, &credentials); err != nil {
|
||||
if strings.HasSuffix(filePath, ".pem") {
|
||||
return connection.Credentials{}, fmt.Errorf("The tunnel credentials file should be .json but you gave a .pem. " +
|
||||
"The tunnel credentials file was originally created by `cloudflared tunnel create`. " +
|
||||
"You may have accidentally used the filepath to cert.pem, which is generated by `cloudflared tunnel " +
|
||||
"login`.")
|
||||
}
|
||||
return connection.Credentials{}, errInvalidJSONCredential{path: filePath, err: err}
|
||||
}
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) create(name string, credentialsFilePath string, secret string) (*cfapi.Tunnel, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't create client to talk to Cloudflare Tunnel backend")
|
||||
}
|
||||
|
||||
var tunnelSecret []byte
|
||||
if secret == "" {
|
||||
tunnelSecret, err = generateTunnelSecret()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't generate the secret for your new tunnel")
|
||||
}
|
||||
} else {
|
||||
decodedSecret, err := base64.StdEncoding.DecodeString(secret)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Couldn't decode tunnel secret from base64")
|
||||
}
|
||||
tunnelSecret = []byte(decodedSecret)
|
||||
if len(tunnelSecret) < 32 {
|
||||
return nil, errors.New("Decoded tunnel secret must be at least 32 bytes long")
|
||||
}
|
||||
}
|
||||
|
||||
tunnel, err := client.CreateTunnel(name, tunnelSecret)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Create Tunnel API call failed")
|
||||
}
|
||||
|
||||
credential, err := sc.credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tunnelCredentials := connection.Credentials{
|
||||
AccountTag: credential.cert.AccountID,
|
||||
TunnelSecret: tunnelSecret,
|
||||
TunnelID: tunnel.ID,
|
||||
}
|
||||
usedCertPath := false
|
||||
if credentialsFilePath == "" {
|
||||
originCertDir := filepath.Dir(credential.certPath)
|
||||
credentialsFilePath, err = tunnelFilePath(tunnelCredentials.TunnelID, originCertDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usedCertPath = true
|
||||
}
|
||||
writeFileErr := writeTunnelCredentials(credentialsFilePath, &tunnelCredentials)
|
||||
if writeFileErr != nil {
|
||||
var errorLines []string
|
||||
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write tunnel credentials to %s.", tunnel.Name, tunnel.ID, credentialsFilePath))
|
||||
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
|
||||
if deleteErr := client.DeleteTunnel(tunnel.ID); deleteErr != nil {
|
||||
errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID))
|
||||
errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr))
|
||||
} else {
|
||||
errorLines = append(errorLines, fmt.Sprintf("The tunnel was deleted, because the tunnel can't be run without the credentials file"))
|
||||
}
|
||||
errorMsg := strings.Join(errorLines, "\n")
|
||||
return nil, errors.New(errorMsg)
|
||||
}
|
||||
|
||||
if outputFormat := sc.c.String(outputFormatFlag.Name); outputFormat != "" {
|
||||
return nil, renderOutput(outputFormat, &tunnel)
|
||||
}
|
||||
|
||||
fmt.Printf("Tunnel credentials written to %v.", credentialsFilePath)
|
||||
if usedCertPath {
|
||||
fmt.Print(" cloudflared chose this file based on where your origin certificate was found.")
|
||||
}
|
||||
fmt.Println(" Keep this file secret. To revoke these credentials, delete the tunnel.")
|
||||
fmt.Printf("\nCreated tunnel %s with id %s\n", tunnel.Name, tunnel.ID)
|
||||
|
||||
return &tunnel.Tunnel, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.Tunnel, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.ListTunnels(filter)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
|
||||
forceFlagSet := sc.c.Bool("force")
|
||||
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range tunnelIDs {
|
||||
tunnel, err := client.GetTunnel(id)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Can't get tunnel information. Please check tunnel id: %s", id)
|
||||
}
|
||||
|
||||
// Check if tunnel DeletedAt field has already been set
|
||||
if !tunnel.DeletedAt.IsZero() {
|
||||
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
|
||||
}
|
||||
if forceFlagSet {
|
||||
if err := client.CleanupConnections(tunnel.ID, cfapi.NewCleanupParams()); err != nil {
|
||||
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.DeleteTunnel(tunnel.ID); err != nil {
|
||||
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
|
||||
}
|
||||
|
||||
credFinder := sc.credentialFinder(id)
|
||||
if tunnelCredentialsPath, err := credFinder.Path(); err == nil {
|
||||
if err = os.Remove(tunnelCredentialsPath); err != nil {
|
||||
sc.log.Info().Msgf("Tunnel %v was deleted, but we could not remove its credentials file %s: %s. Consider deleting this file manually.", id, tunnelCredentialsPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findCredentials will choose the right way to find the credentials file, find it,
|
||||
// and add the TunnelID into any old credentials (generated before TUN-3581 added the `TunnelID`
|
||||
// field to credentials files)
|
||||
func (sc *subcommandContext) findCredentials(tunnelID uuid.UUID) (connection.Credentials, error) {
|
||||
var credentials connection.Credentials
|
||||
var err error
|
||||
if credentialsContents := sc.c.String(CredContentsFlag); credentialsContents != "" {
|
||||
if err = json.Unmarshal([]byte(credentialsContents), &credentials); err != nil {
|
||||
err = errInvalidJSONCredential{path: "TUNNEL_CRED_CONTENTS", err: err}
|
||||
}
|
||||
} else {
|
||||
credFinder := sc.credentialFinder(tunnelID)
|
||||
credentials, err = sc.readTunnelCredentials(credFinder)
|
||||
}
|
||||
// This line ensures backwards compatibility with credentials files generated before
|
||||
// TUN-3581. Those old credentials files don't have a TunnelID field, so we enrich the struct
|
||||
// with the ID, which we have already resolved from the user input.
|
||||
credentials.TunnelID = tunnelID
|
||||
return credentials, err
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
|
||||
credentials, err := sc.findCredentials(tunnelID)
|
||||
if err != nil {
|
||||
if e, ok := err.(errInvalidJSONCredential); ok {
|
||||
sc.log.Error().Msgf("The credentials file at %s contained invalid JSON. This is probably caused by passing the wrong filepath. Reminder: the credentials file is a .json file created via `cloudflared tunnel create`.", e.path)
|
||||
sc.log.Error().Msgf("Invalid JSON when parsing credentials file: %s", e.err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return sc.runWithCredentials(credentials)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) runWithCredentials(credentials connection.Credentials) error {
|
||||
sc.log.Info().Str(LogFieldTunnelID, credentials.TunnelID.String()).Msg("Starting tunnel")
|
||||
|
||||
return StartServer(
|
||||
sc.c,
|
||||
buildInfo,
|
||||
&connection.NamedTunnelProperties{Credentials: credentials},
|
||||
sc.log,
|
||||
)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
|
||||
params := cfapi.NewCleanupParams()
|
||||
extraLog := ""
|
||||
if connector := sc.c.String("connector-id"); connector != "" {
|
||||
connectorID, err := uuid.Parse(connector)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "%s is not a valid client ID (must be a UUID)", connector)
|
||||
}
|
||||
params.ForClient(connectorID)
|
||||
extraLog = fmt.Sprintf(" for connector-id %s", connectorID.String())
|
||||
}
|
||||
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tunnelID := range tunnelIDs {
|
||||
sc.log.Info().Msgf("Cleanup connection for tunnel %s%s", tunnelID, extraLog)
|
||||
if err := client.CleanupConnections(tunnelID, params); err != nil {
|
||||
sc.log.Error().Msgf("Error cleaning up connections for tunnel %v, error :%v", tunnelID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) getTunnelTokenCredentials(tunnelID uuid.UUID) (*connection.TunnelToken, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := client.GetTunnelToken(tunnelID)
|
||||
if err != nil {
|
||||
sc.log.Err(err).Msgf("Could not get the Token for the given Tunnel %v", tunnelID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ParseToken(token)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) route(tunnelID uuid.UUID, r cfapi.HostnameRoute) (cfapi.HostnameRouteResult, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.RouteTunnel(tunnelID, r)
|
||||
}
|
||||
|
||||
// Query Tunnelstore to find the active tunnel with the given name.
|
||||
func (sc *subcommandContext) tunnelActive(name string) (*cfapi.Tunnel, bool, error) {
|
||||
filter := cfapi.NewTunnelFilter()
|
||||
filter.NoDeleted()
|
||||
filter.ByName(name)
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(tunnels) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
// There should only be 1 active tunnel for a given name
|
||||
return tunnels[0], true, nil
|
||||
}
|
||||
|
||||
// findID parses the input. If it's a UUID, return the UUID.
|
||||
// Otherwise, assume it's a name, and look up the ID of that tunnel.
|
||||
func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
|
||||
if u, err := uuid.Parse(input); err == nil {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Look up name in the credentials file.
|
||||
credFinder := newStaticPath(sc.c.String(CredFileFlag), sc.fs)
|
||||
if credentials, err := sc.readTunnelCredentials(credFinder); err == nil {
|
||||
if credentials.TunnelID != uuid.Nil {
|
||||
return credentials.TunnelID, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to querying Tunnelstore.
|
||||
if tunnel, found, err := sc.tunnelActive(input); err != nil {
|
||||
return uuid.Nil, err
|
||||
} else if found {
|
||||
return tunnel.ID, nil
|
||||
}
|
||||
|
||||
return uuid.Nil, fmt.Errorf("%s is neither the ID nor the name of any of your tunnels", input)
|
||||
}
|
||||
|
||||
// findIDs is just like mapping `findID` over a slice, but it only uses
|
||||
// one Tunnelstore API call per non-UUID input provided.
|
||||
func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
|
||||
uuids, names := splitUuids(inputs)
|
||||
|
||||
for _, name := range names {
|
||||
filter := cfapi.NewTunnelFilter()
|
||||
filter.NoDeleted()
|
||||
filter.ByName(name)
|
||||
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tunnels) != 1 {
|
||||
return nil, fmt.Errorf("there should only be 1 non-deleted Tunnel named %s", name)
|
||||
}
|
||||
|
||||
uuids = append(uuids, tunnels[0].ID)
|
||||
}
|
||||
|
||||
return uuids, nil
|
||||
}
|
||||
|
||||
func splitUuids(inputs []string) ([]uuid.UUID, []string) {
|
||||
uuids := make([]uuid.UUID, 0)
|
||||
names := make([]string, 0)
|
||||
|
||||
for _, input := range inputs {
|
||||
id, err := uuid.Parse(input)
|
||||
if err != nil {
|
||||
names = append(names, input)
|
||||
} else {
|
||||
uuids = append(uuids, id)
|
||||
}
|
||||
}
|
||||
|
||||
return uuids, names
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
const noClientMsg = "error while creating backend client"
|
||||
|
||||
func (sc *subcommandContext) listRoutes(filter *cfapi.IpRouteFilter) ([]*cfapi.DetailedRoute, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.ListRoutes(filter)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) addRoute(newRoute cfapi.NewRoute) (cfapi.Route, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return cfapi.Route{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.AddRoute(newRoute)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) deleteRoute(params cfapi.DeleteRouteParams) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.DeleteRoute(params)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfapi.DetailedRoute, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return cfapi.DetailedRoute{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.GetByIP(params)
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
)
|
||||
|
||||
type mockFileSystem struct {
|
||||
rf func(string) ([]byte, error)
|
||||
vfp func(string) bool
|
||||
}
|
||||
|
||||
func (fs mockFileSystem) validFilePath(path string) bool {
|
||||
return fs.vfp(path)
|
||||
}
|
||||
|
||||
func (fs mockFileSystem) readFile(filePath string) ([]byte, error) {
|
||||
return fs.rf(filePath)
|
||||
}
|
||||
|
||||
func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||
type fields struct {
|
||||
c *cli.Context
|
||||
log *zerolog.Logger
|
||||
fs fileSystem
|
||||
tunnelstoreClient cfapi.Client
|
||||
userCredential *userCredential
|
||||
}
|
||||
type args struct {
|
||||
tunnelID uuid.UUID
|
||||
}
|
||||
oldCertPath := "old_cert.json"
|
||||
newCertPath := "new_cert.json"
|
||||
accountTag := "0000d4d14e84bd4ae5a6a02e0000ac63"
|
||||
secret := []byte{211, 79, 177, 245, 179, 194, 152, 127, 140, 71, 18, 46, 183, 209, 10, 24, 192, 150, 55, 249, 211, 16, 167, 30, 113, 51, 152, 168, 72, 100, 205, 144}
|
||||
secretB64 := base64.StdEncoding.EncodeToString(secret)
|
||||
tunnelID := uuid.MustParse("df5ed608-b8b4-4109-89f3-9f2cf199df64")
|
||||
name := "mytunnel"
|
||||
|
||||
fs := mockFileSystem{
|
||||
rf: func(filePath string) ([]byte, error) {
|
||||
if filePath == oldCertPath {
|
||||
// An old credentials file created before TUN-3581 added the new fields
|
||||
return []byte(fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s"}`, accountTag, secretB64)), nil
|
||||
}
|
||||
if filePath == newCertPath {
|
||||
// A new credentials file created after TUN-3581 with its new fields.
|
||||
return []byte(fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s","TunnelID":"%s","TunnelName":"%s"}`, accountTag, secretB64, tunnelID, name)), nil
|
||||
}
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
vfp: func(string) bool { return true },
|
||||
}
|
||||
log := zerolog.Nop()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want connection.Credentials
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Filepath given leads to old credentials file",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: fs,
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredFileFlag, oldCertPath, "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredFileFlag, oldCertPath)
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
tunnelID: tunnelID,
|
||||
},
|
||||
want: connection.Credentials{
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Filepath given leads to new credentials file",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: fs,
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredFileFlag, newCertPath, "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredFileFlag, newCertPath)
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
tunnelID: tunnelID,
|
||||
},
|
||||
want: connection.Credentials{
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TUNNEL_CRED_CONTENTS given contains old credentials contents",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: fs,
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredContentsFlag, "", "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredContentsFlag, fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s"}`, accountTag, secretB64))
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
tunnelID: tunnelID,
|
||||
},
|
||||
want: connection.Credentials{
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TUNNEL_CRED_CONTENTS given contains new credentials contents",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: fs,
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredContentsFlag, "", "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredContentsFlag, fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s","TunnelID":"%s","TunnelName":"%s"}`, accountTag, secretB64, tunnelID, name))
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
tunnelID: tunnelID,
|
||||
},
|
||||
want: connection.Credentials{
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sc := &subcommandContext{
|
||||
c: tt.fields.c,
|
||||
log: tt.fields.log,
|
||||
fs: tt.fields.fs,
|
||||
tunnelstoreClient: tt.fields.tunnelstoreClient,
|
||||
userCredential: tt.fields.userCredential,
|
||||
}
|
||||
got, err := sc.findCredentials(tt.args.tunnelID)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("subcommandContext.findCredentials() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("subcommandContext.findCredentials() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type deleteMockTunnelStore struct {
|
||||
cfapi.Client
|
||||
mockTunnels map[uuid.UUID]mockTunnelBehaviour
|
||||
deletedTunnelIDs []uuid.UUID
|
||||
}
|
||||
|
||||
type mockTunnelBehaviour struct {
|
||||
tunnel cfapi.Tunnel
|
||||
deleteErr error
|
||||
cleanupErr error
|
||||
}
|
||||
|
||||
func newDeleteMockTunnelStore(tunnels ...mockTunnelBehaviour) *deleteMockTunnelStore {
|
||||
mockTunnels := make(map[uuid.UUID]mockTunnelBehaviour)
|
||||
for _, tunnel := range tunnels {
|
||||
mockTunnels[tunnel.tunnel.ID] = tunnel
|
||||
}
|
||||
return &deleteMockTunnelStore{
|
||||
mockTunnels: mockTunnels,
|
||||
deletedTunnelIDs: make([]uuid.UUID, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*cfapi.Tunnel, error) {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
}
|
||||
return &tunnel.tunnel, nil
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) GetTunnelToken(tunnelID uuid.UUID) (string, error) {
|
||||
return "token", nil
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
}
|
||||
|
||||
if tunnel.deleteErr != nil {
|
||||
return tunnel.deleteErr
|
||||
}
|
||||
|
||||
d.deletedTunnelIDs = append(d.deletedTunnelIDs, tunnelID)
|
||||
tunnel.tunnel.DeletedAt = time.Now()
|
||||
delete(d.mockTunnels, tunnelID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID, _ *cfapi.CleanupParams) error {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
}
|
||||
return tunnel.cleanupErr
|
||||
}
|
||||
|
||||
func Test_subcommandContext_Delete(t *testing.T) {
|
||||
type fields struct {
|
||||
c *cli.Context
|
||||
log *zerolog.Logger
|
||||
isUIEnabled bool
|
||||
fs fileSystem
|
||||
tunnelstoreClient *deleteMockTunnelStore
|
||||
userCredential *userCredential
|
||||
}
|
||||
type args struct {
|
||||
tunnelIDs []uuid.UUID
|
||||
}
|
||||
newCertPath := "new_cert.json"
|
||||
tunnelID1 := uuid.MustParse("df5ed608-b8b4-4109-89f3-9f2cf199df64")
|
||||
tunnelID2 := uuid.MustParse("af5ed608-b8b4-4109-89f3-9f2cf199df64")
|
||||
log := zerolog.Nop()
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []uuid.UUID
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "clean up continues if credentials are not found",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: mockFileSystem{
|
||||
rf: func(filePath string) ([]byte, error) {
|
||||
return nil, errors.New("file not found")
|
||||
},
|
||||
vfp: func(string) bool { return true },
|
||||
},
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredFileFlag, newCertPath, "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredFileFlag, newCertPath)
|
||||
return c
|
||||
}(),
|
||||
tunnelstoreClient: newDeleteMockTunnelStore(
|
||||
mockTunnelBehaviour{
|
||||
tunnel: cfapi.Tunnel{ID: tunnelID1},
|
||||
},
|
||||
mockTunnelBehaviour{
|
||||
tunnel: cfapi.Tunnel{ID: tunnelID2},
|
||||
},
|
||||
),
|
||||
},
|
||||
|
||||
args: args{
|
||||
tunnelIDs: []uuid.UUID{tunnelID1, tunnelID2},
|
||||
},
|
||||
want: []uuid.UUID{tunnelID1, tunnelID2},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sc := &subcommandContext{
|
||||
c: tt.fields.c,
|
||||
log: tt.fields.log,
|
||||
fs: tt.fields.fs,
|
||||
tunnelstoreClient: tt.fields.tunnelstoreClient,
|
||||
userCredential: tt.fields.userCredential,
|
||||
}
|
||||
err := sc.delete(tt.args.tunnelIDs)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("subcommandContext.findCredentials() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
got := tt.fields.tunnelstoreClient.deletedTunnelIDs
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("subcommandContext.findCredentials() = %v, want %v", got, tt.want)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_subcommandContext_ValidateIngressCommand(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
c *cli.Context
|
||||
wantErr bool
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "read a valid configuration from data",
|
||||
c: func() *cli.Context {
|
||||
data := `{ "warp-routing": {"enabled": true}, "originRequest" : {"connectTimeout": 10}, "ingress" : [ {"hostname": "test", "service": "https://localhost:8000" } , {"service": "http_status:404"} ]}`
|
||||
flagSet := flag.NewFlagSet("json", flag.PanicOnError)
|
||||
flagSet.String(ingressDataJSONFlagName, data, "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(ingressDataJSONFlagName, data)
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "read an invalid configuration with multiple mistakes",
|
||||
c: func() *cli.Context {
|
||||
data := `{ "ingress" : [ {"hostname": "test", "service": "localhost:8000" } , {"service": "http_status:invalid_status"} ]}`
|
||||
flagSet := flag.NewFlagSet("json", flag.PanicOnError)
|
||||
flagSet.String(ingressDataJSONFlagName, data, "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(ingressDataJSONFlagName, data)
|
||||
return c
|
||||
}(),
|
||||
wantErr: true,
|
||||
expectedErr: errors.New("Validation failed: localhost:8000 is an invalid address, please make sure it has a scheme and a hostname"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateIngressCommand(tt.c, "")
|
||||
if tt.wantErr {
|
||||
assert.Equal(t, tt.expectedErr.Error(), err.Error())
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user