Compare commits
1139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0889405d84 | |||
| edd160d112 | |||
| 09a833b317 | |||
| 206eb51961 | |||
| 75e32f4d87 | |||
| adc90b4d2d | |||
| 87cf988e03 | |||
| 109ff2169a | |||
| 82b7fdbec4 | |||
| fea8c911be | |||
| cf9629c41f | |||
| a5bef7af29 | |||
| 239dbfc27e | |||
| de2b3031f3 | |||
| 40c3ec0e32 | |||
| 139e70e880 | |||
| 94e83e57f8 | |||
| 39484560ea | |||
| 7690c41d5f | |||
| cae6fbbe76 | |||
| a32cdbc117 | |||
| baeb282cf1 | |||
| 2869cc16d1 | |||
| dbb66ec938 | |||
| 80345fe63d | |||
| 00a119496e | |||
| 3e25be984d | |||
| 4e8934c230 | |||
| a368be82cd | |||
| 43386d4ec1 | |||
| a84f92c907 | |||
| 751de9e7c0 | |||
| be1cf9c9d8 | |||
| d7699845bb | |||
| 520a0fa140 | |||
| 8a1591af0c | |||
| 1f1d9875a2 | |||
| cd4c881db5 | |||
| de03bbbf81 | |||
| ee4f656fa5 | |||
| 2cce3e5e1a | |||
| 0526a42efa | |||
| dc1e1eb45c | |||
| 06b31f545a | |||
| 8cda829320 | |||
| e755e4a823 | |||
| eef06fcf01 | |||
| 9e36df224b | |||
| 2da1cc0edd | |||
| 0ef0143537 | |||
| 30dc01523a | |||
| 4a0648536f | |||
| 01ed999b2a | |||
| acfb4e2639 | |||
| cd735aba56 | |||
| 8c995ea8ac | |||
| e9ce3487b6 | |||
| 79f5a7a7f6 | |||
| 48c310f70b | |||
| 5ffb711dfe | |||
| de5057e35d | |||
| 91d91eef1f | |||
| 48479228b1 | |||
| 5a07370970 | |||
| db9b1e8d79 | |||
| 3e05108882 | |||
| 85cd80a467 | |||
| bc40df0c9e | |||
| 66014eac68 | |||
| 26b9d56165 | |||
| 6574180b0d | |||
| e6c9f4fa39 | |||
| 1dde737b64 | |||
| 26adf49e55 | |||
| 7a9c445321 | |||
| 44cc31345d | |||
| aa2ed9aa91 | |||
| 3d6b6521ab | |||
| 7b0fa8b6ae | |||
| 059d317969 | |||
| e0af3b61a5 | |||
| 5bbc24abe1 | |||
| dfcaae7bf8 | |||
| 9070b3befd | |||
| e5cd820c71 | |||
| 479bd58c3f | |||
| 95540b7859 | |||
| be2559c555 | |||
| 6e53f541d3 | |||
| a6ede13f73 | |||
| 31d2d39f76 | |||
| ad73add529 | |||
| f99a205b2b | |||
| 3cf5540b0f | |||
| 2a140858a8 | |||
| 2d19a304fc | |||
| 7b48449a85 | |||
| 680987aeeb | |||
| ee9f3afe37 | |||
| 7e1bba2baf | |||
| dd0944bf68 | |||
| 88f38eaef6 | |||
| 0e45edf439 | |||
| 788dcd05b2 | |||
| 456cd7e092 | |||
| 3fd52c66e7 | |||
| 3c3ba55c45 | |||
| 1a8074467a | |||
| b9665e32c5 | |||
| 11b7eb0727 | |||
| 5a409f2607 | |||
| 8e771c57e1 | |||
| ddc34801ee | |||
| e12504ce3a | |||
| a03ab5f601 | |||
| 44ff301d2d | |||
| 37bb1a8ab6 | |||
| 1fe2638298 | |||
| 97275d3db9 | |||
| e29159e836 | |||
| caba761c7b | |||
| cd2b40ca4a | |||
| 7c04b7cfd6 | |||
| b4a87d05f7 | |||
| 0e47cbf1a2 | |||
| 12dfa797dc | |||
| 230505bf5a | |||
| 01b8394c81 | |||
| f9d08a14f3 | |||
| a997f738dd | |||
| 46db7df218 | |||
| 17c04258a4 | |||
| 8eb280126a | |||
| f51cd8d883 | |||
| ef4dc8b5b0 | |||
| 23b14123ea | |||
| 2388f662cf | |||
| 60474ed38f | |||
| 484eb3d15e | |||
| c51095a074 | |||
| 03782bc9b3 | |||
| 1040b7c58e | |||
| bd0e9ca7cb | |||
| f5399d56c3 | |||
| 0f4c238804 | |||
| 66c169076c | |||
| 280eb5b46d | |||
| 4ead4ff953 | |||
| 200a3cb0ce | |||
| 049dea6804 | |||
| 3fa2a38d76 | |||
| 65af48823f | |||
| 156a968253 | |||
| 517b2b0855 | |||
| 4702afe57d | |||
| e032a6aafd | |||
| 14a1791011 | |||
| b318b7f097 | |||
| 0433e98dbc | |||
| 1c6c1ff1a3 | |||
| 4c34451125 | |||
| 9a7cd8ce66 | |||
| dd0145fa18 | |||
| 72df27b7d1 | |||
| e9a52070e6 | |||
| 6e393f6abf | |||
| 90531337db | |||
| e756fd32e6 | |||
| 7c3f272001 | |||
| 66b3fd8893 | |||
| c64cb1cb03 | |||
| 8876002e57 | |||
| a3809afbdf | |||
| 563e0d265d | |||
| 3d5f87c01b | |||
| bac34aaec1 | |||
| db0fa9fe1f | |||
| 3676232df1 | |||
| 29d79d748d | |||
| 627066baf9 | |||
| d84da9a8eb | |||
| 6c0a073e33 | |||
| 12e8fcd057 | |||
| 4dcf2e58c0 | |||
| b56cfaf818 | |||
| ff52e9f5d0 | |||
| c05c11e0c3 | |||
| fc614aef2d | |||
| 1515db5240 | |||
| 4d5b3b084f | |||
| e9ceb66e32 | |||
| 610c00859f | |||
| e11395c8f5 | |||
| 63f2507944 | |||
| 0c6c907c58 | |||
| 53f691da30 | |||
| 8f43ae86dd | |||
| f9ce51c522 | |||
| 08d98adcd2 | |||
| 186b8351dc | |||
| b1e58a1fe8 | |||
| f1a8b39309 | |||
| 6e55bb1a0e | |||
| f28468a59c | |||
| 3a4790b3c5 | |||
| cbd45b12dc | |||
| 252385e39c | |||
| 68c026c5d8 | |||
| ca6190f5f2 | |||
| 674b9822e0 | |||
| 7392705af8 | |||
| 27c488712e | |||
| c3d7c65c88 | |||
| 384814c885 | |||
| 8a3a98a8b8 | |||
| 416f65e814 | |||
| 8020303017 | |||
| 67d48b205e | |||
| 31135d4696 | |||
| 1478a8e46d | |||
| ad8fca255b | |||
| a7522e7d6c | |||
| 0f9b2f00c9 | |||
| 7fe95e8732 | |||
| d44ed328d1 | |||
| d392e72021 | |||
| 683e2729b4 | |||
| 6f8930926f | |||
| 2117672933 | |||
| d7e692b5d1 | |||
| d949258b2f | |||
| d376656528 | |||
| 415b3e3a2f | |||
| e337eb2c3d | |||
| 521e6b644c | |||
| ad0b4f103d | |||
| 68a6425ad8 | |||
| 0a91ba7b5d | |||
| a17eaf64da | |||
| 6df085fd6d | |||
| 697ea1b21e | |||
| b442dbb56b | |||
| b988a78bae | |||
| a3980c2308 | |||
| c25dee7c8a | |||
| 24dfcef49a | |||
| 065f9714e9 | |||
| 010b1d9f11 | |||
| 85c0b0b8eb | |||
| 47e6a8d725 | |||
| 1db36357d5 | |||
| 57a0ef5a0f | |||
| 39e00f11a8 | |||
| 34b6434791 | |||
| a702a1b097 | |||
| d32c8bd0eb | |||
| d7d8123bc7 | |||
| f40cfdda82 | |||
| 51250bf248 | |||
| 63a9b94820 | |||
| adcc1761d0 | |||
| cc53b5e7d7 | |||
| 4d63d61c20 | |||
| 678c227061 | |||
| ec847fa60c | |||
| 616060876f | |||
| 641be04fc0 | |||
| 28f36e8fd6 | |||
| a8224e1c67 | |||
| 38293f479a | |||
| 48eae3b968 | |||
| e28f5b97f6 | |||
| 140f90b354 | |||
| 467c8a34e3 | |||
| 582e3f9310 | |||
| 98375c7ae5 | |||
| 7e903d80b6 | |||
| 6cf2b02f0f | |||
| b36949f2c1 | |||
| 995abaaea6 | |||
| 97d59fbf78 | |||
| ff1312438c | |||
| edaa353826 | |||
| 48d6650f28 | |||
| 3381a9f3db | |||
| 63d1f9de4a | |||
| e313863a2a | |||
| 73dc3723c7 | |||
| 667ee9b85e | |||
| b6b866c0b2 | |||
| 28298a30e4 | |||
| b884a68347 | |||
| bfcdfc3c7f | |||
| 392610022a | |||
| 34a51c9f29 | |||
| a5fb3fab66 | |||
| 96576a612a | |||
| 87c96bc4d0 | |||
| 65a2e4ed02 | |||
| 9960cd1d36 | |||
| 107bd74187 | |||
| 7a6a2b6373 | |||
| cf51ddeb26 | |||
| 13b76331ec | |||
| de0b456b46 | |||
| 072547201c | |||
| 8293988499 | |||
| 367771923c | |||
| 4ca75f3ab3 | |||
| 4a7c16419e | |||
| 1d396bdd64 | |||
| ca807e36c5 | |||
| cce02927e7 | |||
| 034ba9a0de | |||
| 1919048c8f | |||
| 62c2981baf | |||
| efa2c4f2d2 | |||
| 27b98c450e | |||
| e8604f065c | |||
| aaca7c27da | |||
| 664ee31b1a | |||
| 6dedee5d8c | |||
| d1806e2883 | |||
| 1f086a3002 | |||
| 73eda3346d | |||
| c6d352c832 | |||
| 356448017f | |||
| d6a74c1a8e | |||
| e5a35d38f9 | |||
| a2f6fac852 | |||
| dca3a3b553 | |||
| 4154ed6a80 | |||
| 69b4b5d1c6 | |||
| d47c2d984b | |||
| 91cfd18373 | |||
| 9e0be6c85d | |||
| 23f1de80c5 | |||
| 6bc97c16ee | |||
| a612e09a34 | |||
| 1e2d197c7d | |||
| 3a999a0db6 | |||
| b8c2eb2b28 | |||
| 6f15174a2f | |||
| 748fb39cac | |||
| 5f2251cfad | |||
| ef7528ebdb | |||
| 696a0ba714 | |||
| 267da35259 | |||
| d152870bdb | |||
| da41ab146e | |||
| 757d0fe343 | |||
| c8d94c44f0 | |||
| 5fa6e89f4f | |||
| 85f1c44000 | |||
| 3ba69f11ff | |||
| af5e70a1d6 | |||
| 5fe884e59f | |||
| 415016c6e4 | |||
| e0de23c546 | |||
| 7af0eb4b7a | |||
| 8e52761edb | |||
| 37de747d19 | |||
| a40f6e8fae | |||
| b00c8368b9 | |||
| 97f169c424 | |||
| 81244a41f1 | |||
| 229b369702 | |||
| c0e0b2b225 | |||
| 1e4dd94b71 | |||
| b25d744f5c | |||
| 685f9979a1 | |||
| 6fc5e45626 | |||
| 2c6515152a | |||
| 70d2bd8a6b | |||
| 90b3e4af0b | |||
| 4f1a571704 | |||
| 799b8d6a65 | |||
| 8e312801a1 | |||
| 335c4e54a1 | |||
| e6d17257b0 | |||
| 0c2041e439 | |||
| c5be9f560c | |||
| 62b985f49c | |||
| 2b9a6f9aeb | |||
| 3c70d23a1d | |||
| adaa98e6cf | |||
| ed90a65783 | |||
| 268278c53e | |||
| 5d5aee1f08 | |||
| ed690ed838 | |||
| e155029a4b | |||
| 880d751df4 | |||
| a3202b00ee | |||
| efcda9a48c | |||
| 2c73c7fcf3 | |||
| d2a51d05c4 | |||
| af3cfa0a4c | |||
| f96b48dad9 | |||
| 9edc569e68 | |||
| 29fefbad4e | |||
| 9f4770e7fd | |||
| e7dbda689e | |||
| 00f461141a | |||
| 8cad88edae | |||
| 62801c3d68 | |||
| 7136c61dbd | |||
| a87f9a808c | |||
| ca594b922c | |||
| aa4a87ed9a | |||
| 690facdb5c | |||
| f63e46a7d1 | |||
| 1cd7cfaa6c | |||
| d9a6faeb7a | |||
| 4cb6168780 | |||
| b8d7f87d17 | |||
| b2e17a89e5 | |||
| 4ae2528cbe | |||
| 043a82bce2 | |||
| 99e0b182b7 | |||
| ab1b10f13d | |||
| 86eabf4a4c | |||
| a99a1e9356 | |||
| 2ef523a7eb | |||
| a1da40d9aa | |||
| a315fc2dda | |||
| 6dc25053ea | |||
| 4612baa7f8 | |||
| 44ecd6a457 | |||
| 8c39b1e120 | |||
| 12eff1cc60 | |||
| 9deb715ebb | |||
| 3b60db0dcd | |||
| af22cbb108 | |||
| 9d7131a512 | |||
| bca9ab165d | |||
| d111f96993 | |||
| 2f223c276c | |||
| 33acb4a956 | |||
| b515785637 | |||
| 3dcdcdad32 | |||
| 916ed6b8ee | |||
| b8fbe87e1f | |||
| cb3db7dd83 | |||
| 066ce5045b | |||
| a65932ff82 | |||
| 5496086916 | |||
| efd4ef0e62 | |||
| 00d819a199 | |||
| ad532522ea | |||
| 4c7d2e28eb | |||
| d78d35078c | |||
| 22409c8ff3 | |||
| 6c3a5c161f | |||
| fa27250605 | |||
| ac21bb5b19 | |||
| 5b8d22a463 | |||
| 56d5143557 | |||
| 5772697752 | |||
| e23006b1a5 | |||
| 01b7b4409b | |||
| 0dd6afe128 | |||
| a36f52c6d2 | |||
| 440aa4a1ce | |||
| f77c91b7ae | |||
| a58b8883f8 | |||
| 270d910b73 | |||
| 77bf39f2cd | |||
| c45073a308 | |||
| 86de031adc | |||
| 80dbe51077 | |||
| f444dadd50 | |||
| e080791787 | |||
| a159598f36 | |||
| 993e2f55bd | |||
| ed9053ed94 | |||
| 05a299b76c | |||
| dd0a7746ff | |||
| 088ccb5001 | |||
| 4363678e48 | |||
| 9a77c06b44 | |||
| 78bea87690 | |||
| 49eccbc85a | |||
| 7f1ebaa572 | |||
| c7195aae98 | |||
| ffb0a2f896 | |||
| b977a51253 | |||
| 046d0785c9 | |||
| 828d3f2b74 | |||
| a46dc0b856 | |||
| 3e0411a3f6 | |||
| c3107e6843 | |||
| e1bc7e3028 | |||
| 992418236e | |||
| 27a792790b | |||
| bd9821111e | |||
| dbba07e7ed | |||
| 753022d2ae | |||
| 546225f1d8 | |||
| 0532fd9de8 | |||
| 3fbf2c304f | |||
| 6cdf28270f | |||
| 7f1de77e8c | |||
| 1c482e8922 | |||
| 70ed9bf154 | |||
| 93ba8cd0df | |||
| c13bb07171 | |||
| 4929a7c16e | |||
| 212df8e6c3 | |||
| 8dc9bf49a1 | |||
| 4daa499912 | |||
| 939f346d83 | |||
| 08216ccf31 | |||
| 4deeaff8ad | |||
| b892efa25f | |||
| 4af9e245fb | |||
| 52bb3845f6 | |||
| a1e9b88495 | |||
| ed44e9acc3 | |||
| 86d8d8f07e | |||
| 62112b015f | |||
| ca2014bf8e | |||
| acc3f5479f | |||
| f48911d2c3 | |||
| 4c1f81dadb | |||
| 32cb1b435b | |||
| 8c9c7a5927 | |||
| 9e9157d878 | |||
| 1b5d372b5b | |||
| b11a2f17e9 | |||
| a91d1f7a3d | |||
| 0d57213d3f | |||
| 6ae252b892 | |||
| e7a7e3a050 | |||
| bed53b605f | |||
| 9b41d1bf08 | |||
| 0a41e6d80f | |||
| b636a4bfe3 | |||
| df90f2ca63 | |||
| 0bf3719a9a | |||
| bfa9e7c88c | |||
| 57003414be | |||
| a54f40baa7 | |||
| 56c779f9df | |||
| 26b859c4f9 | |||
| 0f0ace1a44 | |||
| f9f5e7422d | |||
| 2653e7a977 | |||
| 55b477ba07 | |||
| 6f666d63c8 | |||
| e8167a8b79 | |||
| 0702501552 | |||
| 89fe31310e | |||
| ca3dfc4580 | |||
| bba3107ae1 | |||
| 60010e82e2 | |||
| ad94eeb635 | |||
| 88be70a6af | |||
| 226833c4ab | |||
| 8e3b7a57d9 | |||
| dc8236ec05 | |||
| d94936f6c9 | |||
| bf4ab679e1 | |||
| 20b5755788 | |||
| f64ffdbab1 | |||
| eb1e22bd10 | |||
| 18fb7a5d62 | |||
| ee8b369a19 | |||
| f4f6feafb3 | |||
| 9ddfe696a4 | |||
| 762fff8a51 | |||
| 36bcd8317b | |||
| 10005db702 | |||
| 1b610403ba | |||
| 2fde8a76eb | |||
| a7c31a0d2b | |||
| 35d72660c8 | |||
| d197438e16 | |||
| 7fc0b7ef72 | |||
| f44b832bac | |||
| 5fe144bda8 | |||
| df2dab0169 | |||
| 7ab46b0850 | |||
| c3fe6eb3ac | |||
| a34c70de0e | |||
| 273bab5dd7 | |||
| f8fc9d0304 | |||
| b5431de2e8 | |||
| dc0cac9ba4 | |||
| 8628dc0fc4 | |||
| b57f56fb0b | |||
| 690169f6e5 | |||
| a543157f50 | |||
| 15a62ae0b9 | |||
| 28afbf164f | |||
| 4ce77373c0 | |||
| 8de1ede0f0 | |||
| 8054478cc3 | |||
| 167fcd0d78 | |||
| 206bf8cd0a | |||
| df687fe5d4 | |||
| de4f3ee4a2 | |||
| 202338bd2d | |||
| e2034b2438 | |||
| 0ec2399a2c | |||
| df5f158cef | |||
| 19ac4fbcaf | |||
| d07de3751e | |||
| 4977aca09c | |||
| 43ff2a540d | |||
| 35c59042bf | |||
| f8f84b42ac | |||
| 1dc9b40b90 | |||
| 345845ec16 | |||
| dbe8c93e20 | |||
| 4d481b0363 | |||
| 88d954cf97 | |||
| 71883f1b04 | |||
| bfc212f7ed | |||
| 43f80b1b0d | |||
| f83d64d092 | |||
| e67b9f0cc1 | |||
| 134c5f0e41 | |||
| e761aa1940 | |||
| 8c8380dcd4 | |||
| dd5eca2561 | |||
| 94bd1025a6 | |||
| 7be4a67673 | |||
| 3e6ac73cfe | |||
| aef6878ee0 | |||
| 81aa8e0231 | |||
| c28f625cd4 | |||
| 5b161b88f7 | |||
| cca8826a1f | |||
| 0327f30e3c | |||
| 0ce132eb8f | |||
| 00b76e0a64 | |||
| aec27be9b4 | |||
| 86dfcb819b | |||
| d613804b0a | |||
| 92e9d05432 | |||
| 8dfd93e915 | |||
| e5136332e3 | |||
| f10fd4b479 | |||
| 74368026a5 | |||
| fde1aa9d6a | |||
| f45cdba9ba | |||
| b270860b79 | |||
| e7ba930236 | |||
| aa679f7a73 | |||
| 7b379bf1f1 | |||
| e4e56b0f3f | |||
| 219d29f664 | |||
| 8fb138af06 | |||
| a39e6515da | |||
| 2c7fd286de | |||
| 8aa2712b4d | |||
| b5c2121cef | |||
| ca9b877646 | |||
| ff49b4fc71 | |||
| 222431b57a | |||
| ddb51529a7 | |||
| 7c048f331a | |||
| 8475183bbb | |||
| 10f35db3ed | |||
| b68af8ab63 | |||
| 266602a3d6 | |||
| 15d5142d9b | |||
| 0d88650511 | |||
| 8660370dc9 | |||
| 73339dde85 | |||
| 3f354f3c30 | |||
| c08eb5f892 | |||
| 27e41c4ad5 | |||
| 1da8a640da | |||
| e7ad031eb8 | |||
| 192f548c83 | |||
| d85b4197b4 | |||
| bb5f2e5525 | |||
| f00b696282 | |||
| e2d662608c | |||
| 9e9773c9c7 | |||
| 2f8e2487f7 | |||
| d6c1fbf841 | |||
| c3b404bab0 | |||
| cd283654eb | |||
| 5712cc9c62 | |||
| be5a10a4d4 | |||
| b53a2c67f1 | |||
| cc79692d68 | |||
| 796a598b37 | |||
| 73b4423682 | |||
| 198e942889 | |||
| 95703e4565 | |||
| 2729569f66 | |||
| 67146ecdc2 | |||
| bd049a51e6 | |||
| ffd89599a0 | |||
| 496293afa1 | |||
| 7d7f4046e8 | |||
| c800070477 | |||
| 877fe5fb0a | |||
| dad011cab4 | |||
| f939200b34 | |||
| 9c072a372c | |||
| fbb91c6cb7 | |||
| b2cf501952 | |||
| 1fce0f02b6 | |||
| 12061bd006 | |||
| 856c73b341 | |||
| 4de19805ba | |||
| c09b871a6b | |||
| e876cb917d | |||
| 8f5ef012ff | |||
| 287992cef3 | |||
| 344ce061ce | |||
| 5d14970a55 | |||
| 0615fe825f | |||
| 6a661fd08c | |||
| f6d9416a9e | |||
| a29676b86a | |||
| dc10f88c12 | |||
| e61edf6280 | |||
| 3410a0949b | |||
| 9216a47724 | |||
| cf37d64819 | |||
| f0c0f3d4c4 | |||
| c4e3f8c336 | |||
| 8875719619 | |||
| c19b349e96 | |||
| 14155c7e02 | |||
| 3d36398818 | |||
| 56a7897c91 | |||
| 5efb386224 | |||
| 4d2de923cd | |||
| f64b1468a5 | |||
| 56f281002b | |||
| 0d7250352f | |||
| ef43f01da4 | |||
| c9e5ae6a2b | |||
| 8a802d8f7a | |||
| 331ab3715c | |||
| 6b451a2197 | |||
| b4c0224746 | |||
| 500d793fc4 | |||
| 16af265e8b | |||
| b222cf8e13 | |||
| f549e20fa2 | |||
| 83e9c1fad2 | |||
| c220686c29 | |||
| 1b18730d54 | |||
| 5764519f5b | |||
| 957001f8ad | |||
| 0955671e45 | |||
| 57b060bac5 | |||
| 3136e1409e | |||
| ca791d7e8d | |||
| 05858b746c | |||
| 09d1450d7d | |||
| 1ca1fd411a | |||
| a2d4d62f1c | |||
| 8ab20f501c | |||
| 366fb228e5 | |||
| bba8c254d7 | |||
| ebd53794af | |||
| 2299244332 | |||
| cf61d98635 | |||
| 15d1967aaf | |||
| 410fec8ee4 | |||
| c831b6c03a | |||
| 290595f88e | |||
| ba64518ebd | |||
| b14d63b4a1 | |||
| af2db0a0d9 | |||
| fb255a7e5a | |||
| 47c1cb433e | |||
| 3fac7d79ab | |||
| 25c937fd0e | |||
| e5c1fbc591 | |||
| d781c9ae2d | |||
| 54951740bf | |||
| 0fb4734e67 | |||
| 346e034d0a | |||
| 116cc1e9ee | |||
| 762a5bc9e8 | |||
| d39b723511 | |||
| 5494e935f4 | |||
| e0863bb7eb | |||
| 89f86167ad | |||
| dfec1ad6b1 | |||
| 66d3e4a595 | |||
| 19a03babc4 | |||
| ee24b4f083 | |||
| 937ecc8d2f | |||
| 6078adb8bc | |||
| 39f18b0b70 | |||
| efb64b7dbb | |||
| f892b41299 | |||
| 1eb63635b5 | |||
| 688228377b | |||
| 506006f0fa | |||
| 22de001a80 | |||
| d90cc8bf2f | |||
| 5a8f19589b | |||
| d61df6363b | |||
| f062013a7b | |||
| fea93a5b7a | |||
| 30dc29e17f | |||
| 5387f73b5d | |||
| 90dd1f63c8 | |||
| 38cdb9cc2f | |||
| b3915b6020 | |||
| 08a2de1ef5 | |||
| 7a2dea6cc2 | |||
| 2a14feed93 | |||
| e8105d22ad | |||
| c76a5e608d | |||
| 359b8bc503 | |||
| fccab8ad27 | |||
| 46d09ec2bc | |||
| 7792dbc34d | |||
| 1d29f126f9 | |||
| bcddb98867 | |||
| f48dec1fb0 | |||
| ef9f65c78b | |||
| 99f8be5b3d | |||
| 9d2fdb84be | |||
| 8b004feb9a | |||
| 70736e24c0 | |||
| d44adafca5 | |||
| e57b20c1f8 | |||
| 94971aee77 | |||
| 0ed66d0664 | |||
| 7e27c97c6c | |||
| 669364985e | |||
| c7440274dd | |||
| 8ccbf53dfc | |||
| f908110fb6 | |||
| 9bb1904a38 | |||
| 06220ab8c8 | |||
| da66561d1e | |||
| 52861310b0 | |||
| 414b2ec5f8 | |||
| b875868a39 | |||
| 8148e0a0f8 | |||
| 25c82245b1 | |||
| 95a7e21f5f | |||
| 83ab47333f | |||
| 8d7d6ef358 | |||
| 494e0b5060 | |||
| 434997e676 | |||
| ec44b67e9f | |||
| e0c9234c0e | |||
| 980b772801 | |||
| a22da39e1c | |||
| 7253635cd3 | |||
| 0a030f3d8a | |||
| edee3b6765 | |||
| d62ae23a9c | |||
| c68226e9b0 | |||
| 041d68b1ce | |||
| 85a8f36565 | |||
| f34d58cfbd | |||
| e0a651bfc3 | |||
| cd35f503a0 | |||
| 8b3d223fc0 | |||
| bb25bd94c8 | |||
| 986e3d3c35 | |||
| 616db3442a | |||
| 5fe3c0bfe5 | |||
| 8849b85a7f | |||
| 893bbafa03 | |||
| 4c05623a3c | |||
| ec929b3d2f | |||
| 2299559397 | |||
| 6be53536d3 | |||
| 99d4b2b31a | |||
| ff8ad7d4ca | |||
| b692c045b5 | |||
| b5f53595ca | |||
| 88176dca53 | |||
| 020476614c | |||
| 0f42346976 | |||
| 692b27578c | |||
| 316b81f790 | |||
| 5e3262a62c | |||
| 053fa0e457 | |||
| 501a6bf3ea | |||
| 7345ce9192 | |||
| 6fa27373ed | |||
| e55c23164a | |||
| e25ab9f4ca | |||
| bea092f082 | |||
| d7ecb58f6c | |||
| 05d2b34e10 | |||
| 211fd9b9a3 | |||
| 19f0836e70 | |||
| 075d7d4fef | |||
| e5c215556e | |||
| 7c31edd5f8 | |||
| c4acf3c2cb | |||
| 53db26a614 | |||
| dde8f18b52 | |||
| 6071b74fb5 | |||
| ab041012b2 | |||
| 226d6ad22b | |||
| a08ff90976 | |||
| ae8404d760 | |||
| f00fd7a49e | |||
| ecbc42755c | |||
| 16db76bee2 | |||
| dca5305e8a | |||
| c0b0f63757 | |||
| ac5822f1ee | |||
| ae56becbbd | |||
| 5bb18e685e | |||
| 2dfaab439c | |||
| 62cedf64da | |||
| 9961ebb46e | |||
| fe31a71557 | |||
| c9a6b07c52 | |||
| f1e6eff303 | |||
| 2de03e5fd7 | |||
| 234c192faf | |||
| ea29920afb | |||
| 4d177a782e | |||
| b700d425a4 | |||
| 9a5b8ff50d | |||
| c787fd0483 | |||
| 1134ea5ff3 | |||
| fd16cd298f | |||
| 67e7aa223a | |||
| 9d0381306d | |||
| f8837bfcbd | |||
| 268651df18 | |||
| e17825474f | |||
| 0c75a0306b | |||
| d61d30ef66 | |||
| 71d4253927 | |||
| e515005083 | |||
| 4bdc18d64a | |||
| f840a5003e | |||
| 5ef1698632 | |||
| 1bb6967a38 | |||
| a056bdce7c | |||
| b08c636b44 | |||
| d03a02f2e4 | |||
| 08ca61c121 | |||
| 962588b649 | |||
| cf67d460b7 | |||
| 4e539361fa | |||
| 3ce2ab88f5 | |||
| 0a2363b49b | |||
| ec80ceb8c2 | |||
| ea2bb0275c | |||
| 51d4f737b8 | |||
| 3f81f685df | |||
| bb1ccf09e8 | |||
| c878237567 | |||
| ac8b6f944e | |||
| 0f2006ac50 | |||
| c697eed1be | |||
| b887d91f94 | |||
| 39470c1b11 | |||
| 6d553cb6fe | |||
| c612d86480 | |||
| 3f3d2f5027 | |||
| bc71640fd9 | |||
| b979db6d9d | |||
| 57b758db7e | |||
| b5f546b137 | |||
| d4b2c948dd | |||
| b9a5e9d610 | |||
| 57853b2849 | |||
| e5268dd0a6 | |||
| 4fd12430d0 | |||
| d134b469be | |||
| 9a685b2df5 | |||
| 6a666c9c7d | |||
| 53549b6b30 | |||
| 0a27a08184 | |||
| 77f0abb0ba | |||
| 29cccd0b4a | |||
| bf65dc71c7 | |||
| 5bc8ee39f7 | |||
| 8c1b477279 | |||
| f9cfbe96c0 | |||
| 2e379ca7d0 | |||
| 2659215d5e | |||
| 1063b954de | |||
| 4ff959ef50 | |||
| bc1c8223e6 | |||
| 27eb7db1c2 | |||
| 3ede494945 | |||
| f102aba4c1 | |||
| f2dd091ec0 | |||
| c1ad6d6c7c | |||
| 1e7cd8c484 | |||
| bf8ebf8ad2 | |||
| 1768958af7 | |||
| bf41d8d28e | |||
| 45700eaabe | |||
| 48dd30c4c2 | |||
| 8ff86ae08b | |||
| b2079669f7 | |||
| 2c1d1afd71 | |||
| 9a73828782 | |||
| c5d085cff8 | |||
| 9f4286fc4e | |||
| 77f5cc5023 | |||
| 8400aba81c | |||
| cc027d900e | |||
| bc0dc2e509 | |||
| 1fbc056dd4 | |||
| 98fc3dfbad | |||
| 4b0e27413e | |||
| 6b524e2c55 | |||
| 4cac7dcc48 | |||
| e907d24e3b | |||
| 39c352f210 | |||
| 67cd59d7bc | |||
| 3735b0e529 | |||
| 4362a885eb | |||
| 038b936ce9 | |||
| 6aaad7d63f | |||
| aaee01b1a7 | |||
| 71538adfdc | |||
| 2ea5b33955 | |||
| 3df87510a1 | |||
| 9d598836c3 | |||
| 05ac3ff274 | |||
| 76f6f80318 | |||
| cb96949551 | |||
| 87cc4f273b | |||
| f0980e9b30 | |||
| 977934f8fd | |||
| 9e116731b1 | |||
| 316424f0f7 | |||
| bd23dc1f18 | |||
| 501fc5c059 | |||
| 11e1e9120a | |||
| a81d86ed08 | |||
| 7206d5f964 | |||
| b5f5027549 | |||
| e8a86585da | |||
| 5374896ed0 | |||
| b6348ba59a | |||
| ca11c4a672 | |||
| 900b2d24f9 | |||
| 3324d140c0 | |||
| 3a378eb7d6 | |||
| 574bf5ac69 | |||
| 774c0d30a7 | |||
| 0f570d98e1 | |||
| 7f07e1a642 | |||
| 9b546a7242 | |||
| c4442a7533 | |||
| bea7c3f7e7 | |||
| cca9773269 | |||
| 600b20fb81 | |||
| 31a1ea6fae | |||
| bc7579f1d8 | |||
| 38aed6f0f6 | |||
| 7f8b35e295 | |||
| b09dd9a689 | |||
| a82ce4d1ae | |||
| 34234fc486 | |||
| 4a850ff8a0 | |||
| 350dd41732 | |||
| d8cca31738 | |||
| b864b3e350 | |||
| 41b746e435 | |||
| 906263291b | |||
| 46f0bda31e | |||
| d0e47206cc | |||
| ed6fe67880 | |||
| 1374a9e528 | |||
| a9c0e043d2 | |||
| 49a568e9c0 | |||
| dea2d7ab9f | |||
| ebb1a42d48 | |||
| 1096c8fb39 | |||
| 91b9c3f92e | |||
| 183c8bfb81 | |||
| 49a1130281 | |||
| 760233b858 | |||
| ede076e899 | |||
| fdcb013f24 | |||
| ce23ab36cb | |||
| 61f0619e97 | |||
| cfde3ec3d9 | |||
| 4a4c666528 | |||
| 8b35c264eb | |||
| 4999f6dfd4 | |||
| 31c4452fc7 | |||
| 2cafa289b6 | |||
| 0241197c6a | |||
| 0a15ae9b9d | |||
| d2b6d35220 | |||
| e1bc0f6288 | |||
| cd3d704aa5 | |||
| 9de69529ce | |||
| 623f0c0c90 | |||
| c766679687 | |||
| df8c0bb5e1 | |||
| d577c32563 | |||
| 365dfe9df5 | |||
| ae15793fc3 | |||
| 15fcd5ecef | |||
| 1750fd8d10 | |||
| 525ab094c8 | |||
| bb51b8203e | |||
| 75a75c56ad | |||
| 26cb6bf5b9 | |||
| 9cbbd71855 | |||
| f7655b71ea | |||
| 8e4d9e1702 | |||
| 20c95d94cd | |||
| 0a7d8afe67 | |||
| 7b7a859a03 | |||
| 8a8742f867 | |||
| 68b0238a5d | |||
| 1490aff38e | |||
| 3a22efbd7d | |||
| 5e65a46be3 | |||
| 18061723bb | |||
| d6bef84de0 | |||
| 82bc3cbf8d | |||
| cffb0a1eae | |||
| 134bc1c68a | |||
| 6c6c5359b1 | |||
| 8dbb5153f4 | |||
| 2674790694 | |||
| 9fb1d476a2 | |||
| 36386354d7 | |||
| bd70820b6b | |||
| 956029ae3d | |||
| e0c7f48d20 | |||
| d659e435c6 | |||
| 3d274ab6a2 |
+1
-3
@@ -1,3 +1 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
issuehunt: micro/development
|
||||
github: asim
|
||||
|
||||
@@ -1,24 +1,44 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: For reporting bugs in go-micro
|
||||
title: "[BUG]"
|
||||
labels: ''
|
||||
about: Create a report to help us improve
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
## Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
1. What are you trying to do?
|
||||
2. What did you expect to happen?
|
||||
3. What happens instead?
|
||||
## To Reproduce
|
||||
Steps to reproduce the behavior:
|
||||
1. Create service with '...'
|
||||
2. Configure plugin '...'
|
||||
3. Run command '...'
|
||||
4. See error
|
||||
|
||||
**How to reproduce the bug:**
|
||||
## Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
If possible, please include a minimal code snippet here.
|
||||
|
||||
**Environment:**
|
||||
Go Version: please paste `go version` output here
|
||||
## Code sample
|
||||
```go
|
||||
// Minimal reproducible code
|
||||
```
|
||||
please paste `go env` output here
|
||||
|
||||
## Environment
|
||||
- Go Micro version: [e.g. v5.3.0]
|
||||
- Go version: [e.g. 1.21.0]
|
||||
- OS: [e.g. Ubuntu 22.04]
|
||||
- Plugins used: [e.g. consul registry, nats broker]
|
||||
|
||||
## Logs
|
||||
```
|
||||
Paste relevant logs here
|
||||
```
|
||||
|
||||
## Additional context
|
||||
Add any other context about the problem here.
|
||||
|
||||
## Resources
|
||||
- [Documentation](https://github.com/micro/go-micro/tree/master/internal/website/docs)
|
||||
- [Examples](https://github.com/micro/go-micro/tree/master/internal/website/docs/examples)
|
||||
- [API Reference](https://pkg.go.dev/go-micro.dev/v5)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: Feature request / Enhancement
|
||||
about: If you have a need not served by go-micro
|
||||
title: "[FEATURE]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: '[FEATURE] '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Is your feature request related to a problem?
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
## Describe the solution you'd like
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
## Describe alternatives you've considered
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
## Use case
|
||||
Describe how this feature would be used in practice. What problem does it solve?
|
||||
|
||||
## Additional context
|
||||
Add any other context, code examples, or screenshots about the feature request here.
|
||||
|
||||
## Willing to contribute?
|
||||
- [ ] I'd be willing to submit a PR for this feature
|
||||
|
||||
## Resources
|
||||
- [Documentation](https://github.com/micro/go-micro/tree/master/internal/website/docs)
|
||||
- [Plugins](https://github.com/micro/go-micro/tree/master/internal/website/docs/plugins.md)
|
||||
- [Roadmap](https://github.com/micro/go-micro/blob/master/ROADMAP.md)
|
||||
@@ -1,14 +1,31 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask a question about go-micro
|
||||
title: ''
|
||||
labels: ''
|
||||
about: Ask a question about using Go Micro
|
||||
title: '[QUESTION] '
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before asking, please check if your question has already been answered:
|
||||
## Your question
|
||||
A clear and concise question about Go Micro usage.
|
||||
|
||||
1. Check the documentation - https://micro.mu/docs/
|
||||
2. Check the examples and plugins - https://github.com/micro/examples & https://github.com/micro/go-plugins
|
||||
3. Search existing issues
|
||||
## What have you tried?
|
||||
Describe what you've already attempted or researched.
|
||||
|
||||
## Code sample (if applicable)
|
||||
```go
|
||||
// Your code here
|
||||
```
|
||||
|
||||
## Context
|
||||
Provide any additional context that might help answer your question.
|
||||
|
||||
## Resources you've checked
|
||||
- [ ] [Getting Started Guide](https://github.com/micro/go-micro/tree/master/internal/website/docs/getting-started.md)
|
||||
- [ ] [Examples](https://github.com/micro/go-micro/tree/master/internal/website/docs/examples)
|
||||
- [ ] [API Documentation](https://pkg.go.dev/go-micro.dev/v5)
|
||||
- [ ] Searched existing issues
|
||||
|
||||
## Helpful links
|
||||
- [Documentation](https://github.com/micro/go-micro/tree/master/internal/website/docs)
|
||||
- [Plugins Guide](https://github.com/micro/go-micro/tree/master/internal/website/docs/plugins.md)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
find . -type f -name '*.pb.*.go' -o -name '*.pb.go' -a ! -name 'message.pb.go' -delete
|
||||
PROTOS=$(find . -type f -name '*.proto')
|
||||
|
||||
for PROTO in $PROTOS; do
|
||||
echo $PROTO
|
||||
protoc -I./ -I$(dirname $PROTO) --go_out=plugins=grpc,paths=source_relative:. --micro_out=paths=source_relative:. $PROTO
|
||||
done
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
name: Check out repository
|
||||
- uses: elgohr/Publish-Docker-Github-Action@2.12
|
||||
name: Build and Push Docker Image
|
||||
with:
|
||||
name: micro/go-micro
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -1,28 +0,0 @@
|
||||
name: PR Sanity Check
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
|
||||
prtest:
|
||||
name: PR sanity check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
|
||||
- name: Run tests
|
||||
id: tests
|
||||
env:
|
||||
IN_TRAVIS_CI: yes
|
||||
run: go test -v ./...
|
||||
@@ -0,0 +1,74 @@
|
||||
name: Run Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
branches:
|
||||
- "**"
|
||||
jobs:
|
||||
unittests:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.24
|
||||
check-latest: true
|
||||
cache: true
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go install github.com/kyoh86/richgo@latest
|
||||
go get -v -t -d ./...
|
||||
- name: Run tests
|
||||
id: tests
|
||||
run: richgo test -v -race -cover ./...
|
||||
env:
|
||||
IN_TRAVIS_CI: yes
|
||||
RICHGO_FORCE_COLOR: 1
|
||||
|
||||
etcd-integration:
|
||||
name: Etcd Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
services:
|
||||
etcd:
|
||||
image: quay.io/coreos/etcd:v3.5.2
|
||||
env:
|
||||
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
|
||||
ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379
|
||||
ports:
|
||||
- 2379:2379
|
||||
options: >-
|
||||
--health-cmd "etcdctl endpoint health"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.24
|
||||
check-latest: true
|
||||
cache: true
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go install github.com/kyoh86/richgo@latest
|
||||
go get -v -t -d ./...
|
||||
- name: Wait for etcd
|
||||
run: |
|
||||
timeout 30 bash -c 'until curl -s http://localhost:2379/health; do sleep 1; done'
|
||||
- name: Run etcd integration tests
|
||||
run: richgo test -v -race ./registry/etcd/...
|
||||
env:
|
||||
ETCD_ADDRESS: localhost:2379
|
||||
IN_TRAVIS_CI: yes
|
||||
RICHGO_FORCE_COLOR: 1
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
name: Run tests
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
name: Test repo
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
|
||||
- name: Run tests
|
||||
id: tests
|
||||
env:
|
||||
IN_TRAVIS_CI: yes
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Notify of test failure
|
||||
if: failure()
|
||||
uses: rtCamp/action-slack-notify@v2.0.0
|
||||
env:
|
||||
SLACK_CHANNEL: build
|
||||
SLACK_COLOR: '#BF280A'
|
||||
SLACK_ICON: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||
SLACK_TITLE: Tests Failed
|
||||
SLACK_USERNAME: GitHub Actions
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
- name: Notify of test success
|
||||
if: success()
|
||||
uses: rtCamp/action-slack-notify@v2.0.0
|
||||
env:
|
||||
SLACK_CHANNEL: build
|
||||
SLACK_COLOR: '#1FAD2B'
|
||||
SLACK_ICON: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||
SLACK_TITLE: Tests Passed
|
||||
SLACK_USERNAME: GitHub Actions
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
|
||||
name: Deploy Jekyll with GitHub Pages dependencies preinstalled
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["master"]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Build with Jekyll
|
||||
uses: actions/jekyll-build-pages@v1
|
||||
with:
|
||||
source: ./internal/website
|
||||
destination: ./_site
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -1,6 +1,7 @@
|
||||
# Develop tools
|
||||
/.vscode/
|
||||
/.idea/
|
||||
/.trunk
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
@@ -34,3 +35,7 @@ _cgo_export.*
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# go work files
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
# This file contains all available configuration options
|
||||
# with their default values.
|
||||
|
||||
# options for analysis running
|
||||
run:
|
||||
# go: '1.18'
|
||||
# default concurrency is a available CPU number
|
||||
# concurrency: 4
|
||||
|
||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||
deadline: 10m
|
||||
|
||||
# exit code when at least one issue was found, default is 1
|
||||
issues-exit-code: 1
|
||||
|
||||
# include test files or not, default is true
|
||||
tests: true
|
||||
|
||||
# which files to skip: they will be analyzed, but issues from them
|
||||
# won't be reported. Default value is empty list, but there is
|
||||
# no need to include all autogenerated files, we confidently recognize
|
||||
# autogenerated files. If it's not please let us know.
|
||||
skip-files:
|
||||
[]
|
||||
# - .*\\.pb\\.go$
|
||||
|
||||
allow-parallel-runners: true
|
||||
|
||||
# list of build tags, all linters use it. Default is empty list.
|
||||
build-tags: []
|
||||
|
||||
# output configuration options
|
||||
output:
|
||||
# Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
|
||||
#
|
||||
# Multiple can be specified by separating them by comma, output can be provided
|
||||
# for each of them by separating format name and path by colon symbol.
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Example: "checkstyle:report.json,colored-line-number"
|
||||
#
|
||||
# Default: colored-line-number
|
||||
format: colored-line-number
|
||||
# Print lines of code with issue.
|
||||
# Default: true
|
||||
print-issued-lines: true
|
||||
# Print linter name in the end of issue text.
|
||||
# Default: true
|
||||
print-linter-name: true
|
||||
# Make issues output unique by line.
|
||||
# Default: true
|
||||
uniq-by-line: true
|
||||
# Add a prefix to the output file references.
|
||||
# Default is no prefix.
|
||||
path-prefix: ""
|
||||
# Sort results by: filepath, line and column.
|
||||
sort-results: true
|
||||
|
||||
# all available settings of specific linters
|
||||
linters-settings:
|
||||
wsl:
|
||||
allow-cuddle-with-calls: ["Lock", "RLock", "defer"]
|
||||
funlen:
|
||||
lines: 80
|
||||
statements: 60
|
||||
varnamelen:
|
||||
# The longest distance, in source lines, that is being considered a "small scope".
|
||||
# Variables used in at most this many lines will be ignored.
|
||||
# Default: 5
|
||||
max-distance: 26
|
||||
ignore-names:
|
||||
- err
|
||||
- id
|
||||
- ch
|
||||
- wg
|
||||
- mu
|
||||
ignore-decls:
|
||||
- c echo.Context
|
||||
- t testing.T
|
||||
- f *foo.Bar
|
||||
- e error
|
||||
- i int
|
||||
- const C
|
||||
- T any
|
||||
- m map[string]int
|
||||
errcheck:
|
||||
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-type-assertions: true
|
||||
|
||||
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-blank: true
|
||||
govet:
|
||||
# report about shadowed variables
|
||||
check-shadowing: false
|
||||
gofmt:
|
||||
# simplify code: gofmt with `-s` option, true by default
|
||||
simplify: true
|
||||
gocyclo:
|
||||
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||
min-complexity: 15
|
||||
maligned:
|
||||
# print struct with more effective memory layout or not, false by default
|
||||
suggest-new: true
|
||||
dupl:
|
||||
# tokens count to trigger issue, 150 by default
|
||||
threshold: 100
|
||||
goconst:
|
||||
# minimal length of string constant, 3 by default
|
||||
min-len: 3
|
||||
# minimal occurrences count to trigger, 3 by default
|
||||
min-occurrences: 3
|
||||
depguard:
|
||||
list-type: blacklist
|
||||
# Packages listed here will reported as error if imported
|
||||
packages:
|
||||
- github.com/golang/protobuf/proto
|
||||
misspell:
|
||||
# Correct spellings using locale preferences for US or UK.
|
||||
# Default is to use a neutral variety of English.
|
||||
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
|
||||
locale: US
|
||||
lll:
|
||||
# max line length, lines longer will be reported. Default is 120.
|
||||
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
|
||||
line-length: 120
|
||||
# tab width in spaces. Default to 1.
|
||||
tab-width: 1
|
||||
unused:
|
||||
# treat code as a program (not a library) and report unused exported identifiers; default is false.
|
||||
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
|
||||
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
|
||||
# with golangci-lint call it on a directory with the changed file.
|
||||
check-exported: false
|
||||
unparam:
|
||||
# call graph construction algorithm (cha, rta). In general, use cha for libraries,
|
||||
# and rta for programs with main packages. Default is cha.
|
||||
algo: cha
|
||||
|
||||
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
|
||||
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
|
||||
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
|
||||
# with golangci-lint call it on a directory with the changed file.
|
||||
check-exported: false
|
||||
nakedret:
|
||||
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
|
||||
max-func-lines: 60
|
||||
nolintlint:
|
||||
allow-unused: false
|
||||
allow-leading-space: false
|
||||
allow-no-explanation: []
|
||||
require-explanation: false
|
||||
require-specific: true
|
||||
prealloc:
|
||||
# XXX: we don't recommend using this linter before doing performance profiling.
|
||||
# For most programs usage of prealloc will be a premature optimization.
|
||||
|
||||
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
|
||||
# True by default.
|
||||
simple: true
|
||||
range-loops: true # Report preallocation suggestions on range loops, true by default
|
||||
for-loops: false # Report preallocation suggestions on for loops, false by default
|
||||
cyclop:
|
||||
# the maximal code complexity to report
|
||||
max-complexity: 20
|
||||
gomoddirectives:
|
||||
replace-local: true
|
||||
retract-allow-no-explanation: false
|
||||
exclude-forbidden: true
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
disable-all: false
|
||||
fast: false
|
||||
disable:
|
||||
- golint
|
||||
- varcheck
|
||||
- ifshort
|
||||
- structcheck
|
||||
- deadcode
|
||||
# - nosnakecase
|
||||
- interfacer
|
||||
- maligned
|
||||
- scopelint
|
||||
- exhaustivestruct
|
||||
- testpackage
|
||||
- promlinter
|
||||
- nonamedreturns
|
||||
- makezero
|
||||
- gofumpt
|
||||
- nlreturn
|
||||
- thelper
|
||||
|
||||
# Can be considered to be enabled
|
||||
- gochecknoinits
|
||||
- gochecknoglobals # RIP
|
||||
- dogsled
|
||||
- wrapcheck
|
||||
- paralleltest
|
||||
- ireturn
|
||||
- gomnd
|
||||
- goerr113
|
||||
- exhaustruct
|
||||
- containedctx
|
||||
- godox
|
||||
- forcetypeassert
|
||||
- gci
|
||||
- lll
|
||||
|
||||
issues:
|
||||
# List of regexps of issue texts to exclude, empty list by default.
|
||||
# But independently from this option we use default exclude patterns,
|
||||
# it can be disabled by `exclude-use-default: false`. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`
|
||||
# exclude:
|
||||
# - package comment should be of the form "Package services ..." # revive
|
||||
# - ^ST1000 # ST1000: at least one file in a package should have a package comment (stylecheck)
|
||||
|
||||
# exclude-rules:
|
||||
# - path: internal/app/machined/pkg/system/services
|
||||
# linters:
|
||||
# - dupl
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- dupl
|
||||
- gosec
|
||||
- funlen
|
||||
- varnamelen
|
||||
- wsl
|
||||
|
||||
# Independently from option `exclude` we use default exclude patterns,
|
||||
# it can be disabled by this option. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`.
|
||||
# Default value for this option is true.
|
||||
exclude-use-default: false
|
||||
|
||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||
max-issues-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||
max-same-issues: 0
|
||||
|
||||
# Show only new issues: if there are unstaged changes or untracked files,
|
||||
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
|
||||
# It's a super-useful option for integration of golangci-lint into existing
|
||||
# large codebase. It's not practical to fix all existing issues at the moment
|
||||
# of integration: much better don't allow issues in new code.
|
||||
# Default is false.
|
||||
new: false
|
||||
@@ -1,26 +0,0 @@
|
||||
run:
|
||||
deadline: 10m
|
||||
linters:
|
||||
disable-all: false
|
||||
enable-all: false
|
||||
enable:
|
||||
- megacheck
|
||||
- staticcheck
|
||||
- deadcode
|
||||
- varcheck
|
||||
- gosimple
|
||||
- unused
|
||||
- prealloc
|
||||
- scopelint
|
||||
- gocritic
|
||||
- goimports
|
||||
- unconvert
|
||||
- govet
|
||||
- nakedret
|
||||
- structcheck
|
||||
- gosec
|
||||
disable:
|
||||
- maligned
|
||||
- interfacer
|
||||
- typecheck
|
||||
- dupl
|
||||
@@ -0,0 +1,29 @@
|
||||
labelType: long
|
||||
coverThreshold: 70
|
||||
buildStyle:
|
||||
bold: true
|
||||
foreground: yellow
|
||||
startStyle:
|
||||
foreground: lightBlack
|
||||
passStyle:
|
||||
foreground: green
|
||||
failStyle:
|
||||
bold: true
|
||||
foreground: "#821515"
|
||||
skipStyle:
|
||||
foreground: lightBlack
|
||||
passPackageStyle:
|
||||
foreground: green
|
||||
hide: false
|
||||
failPackageStyle:
|
||||
bold: true
|
||||
foreground: "#821515"
|
||||
coveredStyle:
|
||||
foreground: green
|
||||
uncoveredStyle:
|
||||
bold: true
|
||||
foreground: yellow
|
||||
fileStyle:
|
||||
foreground: cyan
|
||||
lineStyle:
|
||||
foreground: magenta
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
# Contributing to Go Micro
|
||||
|
||||
Thank you for your interest in contributing to Go Micro! This document provides guidelines and instructions for contributing.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Be respectful, inclusive, and collaborative. We're all here to build great software together.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Fork the repository
|
||||
2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/go-micro.git`
|
||||
3. Add upstream remote: `git remote add upstream https://github.com/micro/go-micro.git`
|
||||
4. Create a feature branch: `git checkout -b feature/my-feature`
|
||||
|
||||
## Development Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
go mod download
|
||||
|
||||
# Run tests
|
||||
go test ./...
|
||||
|
||||
# Run tests with coverage
|
||||
go test -race -coverprofile=coverage.out ./...
|
||||
|
||||
# Run linter (install golangci-lint first)
|
||||
golangci-lint run
|
||||
```
|
||||
|
||||
## Making Changes
|
||||
|
||||
### Code Guidelines
|
||||
|
||||
- Follow standard Go conventions (use `gofmt`, `golint`)
|
||||
- Write clear, descriptive commit messages
|
||||
- Add tests for new functionality
|
||||
- Update documentation for API changes
|
||||
- Keep PRs focused - one feature/fix per PR
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Use conventional commits format:
|
||||
|
||||
```
|
||||
type(scope): subject
|
||||
|
||||
body
|
||||
|
||||
footer
|
||||
```
|
||||
|
||||
Types:
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `docs`: Documentation changes
|
||||
- `test`: Test additions/changes
|
||||
- `refactor`: Code refactoring
|
||||
- `perf`: Performance improvements
|
||||
- `chore`: Maintenance tasks
|
||||
|
||||
Examples:
|
||||
```
|
||||
feat(registry): add kubernetes registry plugin
|
||||
fix(broker): resolve nats connection leak
|
||||
docs(examples): add streaming example
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
- Write unit tests for all new code
|
||||
- Ensure existing tests pass
|
||||
- Add integration tests for plugin implementations
|
||||
- Test with multiple Go versions (1.20+)
|
||||
|
||||
```bash
|
||||
# Run specific package tests
|
||||
go test ./registry/...
|
||||
|
||||
# Run with verbose output
|
||||
go test -v ./...
|
||||
|
||||
# Run specific test
|
||||
go test -run TestMyFunction ./pkg/...
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update relevant markdown files in `internal/website/docs/`
|
||||
- Add examples to `internal/website/docs/examples/` for new features
|
||||
- Update README.md for major features
|
||||
- Add godoc comments for exported functions/types
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. **Update your branch**
|
||||
```bash
|
||||
git fetch upstream
|
||||
git rebase upstream/master
|
||||
```
|
||||
|
||||
2. **Run tests and linting**
|
||||
```bash
|
||||
go test ./...
|
||||
golangci-lint run
|
||||
```
|
||||
|
||||
3. **Push to your fork**
|
||||
```bash
|
||||
git push origin feature/my-feature
|
||||
```
|
||||
|
||||
4. **Create Pull Request**
|
||||
- Use a descriptive title
|
||||
- Reference any related issues
|
||||
- Describe what changed and why
|
||||
- Add screenshots for UI changes
|
||||
- Mark as draft if work in progress
|
||||
|
||||
5. **PR Review**
|
||||
- Respond to feedback promptly
|
||||
- Make requested changes
|
||||
- Re-request review after updates
|
||||
|
||||
### PR Checklist
|
||||
|
||||
- [ ] Tests pass locally
|
||||
- [ ] Code follows Go conventions
|
||||
- [ ] Documentation updated
|
||||
- [ ] Commit messages are clear
|
||||
- [ ] Branch is up to date with master
|
||||
- [ ] No merge conflicts
|
||||
|
||||
## Adding Plugins
|
||||
|
||||
New plugins should:
|
||||
|
||||
1. Live in the appropriate interface directory (e.g., `registry/myplugin/`)
|
||||
2. Implement the interface completely
|
||||
3. Include comprehensive tests
|
||||
4. Provide usage examples
|
||||
5. Document configuration options (env vars, options)
|
||||
6. Add to plugin documentation
|
||||
|
||||
Example structure:
|
||||
```
|
||||
registry/myplugin/
|
||||
├── myplugin.go # Main implementation
|
||||
├── myplugin_test.go # Tests
|
||||
├── options.go # Plugin-specific options
|
||||
└── README.md # Usage and configuration
|
||||
```
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Before creating an issue:
|
||||
|
||||
1. Search existing issues
|
||||
2. Check documentation
|
||||
3. Try the latest version
|
||||
|
||||
When reporting bugs:
|
||||
- Use the bug report template
|
||||
- Include minimal reproduction code
|
||||
- Specify versions (Go, Go Micro, plugins)
|
||||
- Provide relevant logs
|
||||
|
||||
## Documentation Contributions
|
||||
|
||||
Documentation improvements are always welcome!
|
||||
|
||||
- Fix typos and grammar
|
||||
- Improve clarity
|
||||
- Add missing examples
|
||||
- Update outdated information
|
||||
|
||||
Documentation lives in `internal/website/docs/`. Preview locally with Jekyll:
|
||||
|
||||
```bash
|
||||
cd internal/website
|
||||
bundle install
|
||||
bundle exec jekyll serve --livereload
|
||||
```
|
||||
|
||||
## Community
|
||||
|
||||
- GitHub Issues: Bug reports and feature requests
|
||||
- GitHub Discussions: Questions, ideas, and community chat
|
||||
- Sponsorship: [GitHub Sponsors](https://github.com/sponsors/micro)
|
||||
|
||||
## Release Process
|
||||
|
||||
Maintainers handle releases:
|
||||
|
||||
1. Update CHANGELOG.md
|
||||
2. Tag release: `git tag -a v5.x.x -m "Release v5.x.x"`
|
||||
3. Push tag: `git push origin v5.x.x`
|
||||
4. GitHub Actions creates release
|
||||
|
||||
## Questions?
|
||||
|
||||
- Check [documentation](internal/website/docs/)
|
||||
- Browse [examples](internal/website/docs/examples/)
|
||||
- Open a [question issue](.github/ISSUE_TEMPLATE/question.md)
|
||||
|
||||
Thank you for contributing to Go Micro! 🎉
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
FROM golang:1.13-alpine
|
||||
|
||||
RUN mkdir /user && \
|
||||
echo 'nobody:x:65534:65534:nobody:/:' > /user/passwd && \
|
||||
echo 'nobody:x:65534:' > /user/group
|
||||
|
||||
ENV GO111MODULE=on
|
||||
RUN apk --no-cache add make git gcc libtool musl-dev ca-certificates dumb-init && \
|
||||
rm -rf /var/cache/apk/* /tmp/*
|
||||
|
||||
WORKDIR /
|
||||
COPY ./go.mod ./go.sum ./
|
||||
RUN go mod download && rm go.mod go.sum
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
@@ -176,7 +175,18 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
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.
|
||||
|
||||
@@ -1,48 +1,226 @@
|
||||
# Go Micro [](https://opensource.org/licenses/Apache-2.0) [](https://pkg.go.dev/github.com/micro/go-micro?tab=doc) [](https://travis-ci.org/micro/go-micro) [](https://goreportcard.com/report/github.com/micro/go-micro)
|
||||
# Go Micro [](https://pkg.go.dev/go-micro.dev/v5?tab=doc) [](https://goreportcard.com/report/github.com/go-micro/go-micro)
|
||||
|
||||
Go Micro is a framework for microservice development.
|
||||
Go Micro is a framework for distributed systems development.
|
||||
|
||||
**[📖 Documentation](https://go-micro.dev/docs/)** | [Sponsor the project](https://github.com/sponsors/micro)
|
||||
|
||||
## Overview
|
||||
|
||||
Go Micro provides the core requirements for distributed systems development including RPC and Event driven communication.
|
||||
The **micro** philosophy is sane defaults with a pluggable architecture. We provide defaults to get you started quickly
|
||||
but everything can be easily swapped out.
|
||||
|
||||
<img src="https://micro.mu/docs/images/go-micro.svg" />
|
||||
|
||||
Plugins are available at [github.com/micro/go-plugins](https://github.com/micro/go-plugins).
|
||||
|
||||
Follow us on [Twitter](https://twitter.com/microhq) or join the [Community](https://micro.mu/slack).
|
||||
Go Micro provides the core requirements for distributed systems development including RPC and Event driven communication.
|
||||
The Go Micro philosophy is sane defaults with a pluggable architecture. We provide defaults to get you started quickly
|
||||
but everything can be easily swapped out.
|
||||
|
||||
## Features
|
||||
|
||||
Go Micro abstracts away the details of distributed systems. Here are the main features.
|
||||
|
||||
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
|
||||
development. When service A needs to speak to service B it needs the location of that service. The default discovery mechanism is
|
||||
multicast DNS (mdns), a zeroconf system.
|
||||
- **Authentication** - Auth is built in as a first class citizen. Authentication and authorization enable secure
|
||||
zero trust networking by providing every service an identity and certificates. This additionally includes rule
|
||||
based access control.
|
||||
|
||||
- **Load Balancing** - Client side load balancing built on service discovery. Once we have the addresses of any number of instances
|
||||
of a service we now need a way to decide which node to route to. We use random hashed load balancing to provide even distribution
|
||||
across the services and retry a different node if there's a problem.
|
||||
- **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application
|
||||
level config from any source such as env vars, file, etcd. You can merge the sources and even define fallbacks.
|
||||
|
||||
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
|
||||
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
|
||||
and server handle this by default. This includes protobuf and json by default.
|
||||
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for many storage backends
|
||||
in the plugins repo. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
|
||||
|
||||
- **Request/Response** - RPC based request/response with support for bidirectional streaming. We provide an abstraction for synchronous
|
||||
communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed. The default
|
||||
transport is [gRPC](https://grpc.io/).
|
||||
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
|
||||
development. When service A needs to speak to service B it needs the location of that service. The default discovery mechanism is
|
||||
multicast DNS (mdns), a zeroconf system.
|
||||
|
||||
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
|
||||
Event notifications are a core pattern in micro service development. The default messaging system is an embedded [NATS](https://nats.io/)
|
||||
server.
|
||||
- **Load Balancing** - Client side load balancing built on service discovery. Once we have the addresses of any number of instances
|
||||
of a service we now need a way to decide which node to route to. We use random hashed load balancing to provide even distribution
|
||||
across the services and retry a different node if there's a problem.
|
||||
|
||||
- **Pluggable Interfaces** - Go Micro makes use of Go interfaces for each distributed system abstraction. Because of this these interfaces
|
||||
are pluggable and allows Go Micro to be runtime agnostic. You can plugin any underlying technology. Find plugins in
|
||||
[github.com/micro/go-plugins](https://github.com/micro/go-plugins).
|
||||
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
|
||||
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
|
||||
and server handle this by default. This includes protobuf and json by default.
|
||||
|
||||
- **RPC Client/Server** - RPC based request/response with support for bidirectional streaming. We provide an abstraction for synchronous
|
||||
communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed.
|
||||
|
||||
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
|
||||
Event notifications are a core pattern in micro service development. The default messaging system is a HTTP event message broker.
|
||||
|
||||
- **Pluggable Interfaces** - Go Micro makes use of Go interfaces for each distributed system abstraction. Because of this these interfaces
|
||||
are pluggable and allows Go Micro to be runtime agnostic. You can plugin any underlying technology.
|
||||
|
||||
## Getting Started
|
||||
|
||||
See the [docs](https://micro.mu/docs/framework.html) for detailed information on the architecture, installation and use of go-micro.
|
||||
To make use of Go Micro
|
||||
|
||||
```bash
|
||||
go get go-micro.dev/v5@latest
|
||||
```
|
||||
|
||||
Create a service and register a handler
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"go-micro.dev/v5"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type Say struct{}
|
||||
|
||||
func (h *Say) Hello(ctx context.Context, req *Request, rsp *Response) error {
|
||||
rsp.Message = "Hello " + req.Name
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// create the service
|
||||
service := micro.New("helloworld")
|
||||
|
||||
// register handler
|
||||
service.Handle(new(Say))
|
||||
|
||||
// run the service
|
||||
service.Run()
|
||||
}
|
||||
```
|
||||
|
||||
Set a fixed address
|
||||
|
||||
```go
|
||||
service := micro.NewService(
|
||||
micro.Name("helloworld"),
|
||||
micro.Address(":8080"),
|
||||
)
|
||||
```
|
||||
|
||||
Call it via curl
|
||||
|
||||
```bash
|
||||
curl -XPOST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Micro-Endpoint: Say.Hello' \
|
||||
-d '{"name": "alice"}' \
|
||||
http://localhost:8080
|
||||
```
|
||||
|
||||
## Experimental
|
||||
|
||||
There's a new `genai` package for generative AI capabilities.
|
||||
|
||||
## Protobuf
|
||||
|
||||
Install the code generator and see usage in the docs:
|
||||
|
||||
```bash
|
||||
go install go-micro.dev/v5/cmd/protoc-gen-micro@v5.13.0
|
||||
```
|
||||
|
||||
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
|
||||
|
||||
Docs: [`internal/website/docs/getting-started.md`](internal/website/docs/getting-started.md)
|
||||
|
||||
## Command Line
|
||||
|
||||
Install the CLI:
|
||||
|
||||
```
|
||||
go install go-micro.dev/v5/cmd/micro@v5.13.0
|
||||
```
|
||||
|
||||
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
micro new helloworld # Create a new service
|
||||
cd helloworld
|
||||
micro run # Run with API gateway
|
||||
```
|
||||
|
||||
Then open http://localhost:8080 to see your service and call it from the browser.
|
||||
|
||||
### micro run
|
||||
|
||||
`micro run` starts your services with:
|
||||
- **API Gateway** - HTTP to RPC proxy at `/api/{service}/{method}`
|
||||
- **Web Dashboard** - Browse and call services at `/`
|
||||
- **Health Checks** - Aggregated health at `/health`
|
||||
- **Hot Reload** - Auto-rebuild on file changes
|
||||
|
||||
```bash
|
||||
micro run # Gateway on :8080
|
||||
micro run --address :3000 # Custom gateway port
|
||||
micro run --no-gateway # Services only
|
||||
micro run --env production # Use production environment
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
For multi-service projects, create a `micro.mu` file:
|
||||
|
||||
```
|
||||
service users
|
||||
path ./users
|
||||
port 8081
|
||||
|
||||
service posts
|
||||
path ./posts
|
||||
port 8082
|
||||
depends users
|
||||
|
||||
env development
|
||||
DATABASE_URL sqlite://./dev.db
|
||||
```
|
||||
|
||||
The gateway runs on :8080 by default, so services should use other ports.
|
||||
|
||||
### Deployment
|
||||
|
||||
Deploy to any Linux server with systemd:
|
||||
|
||||
```bash
|
||||
# On your server (one-time setup)
|
||||
curl -fsSL https://go-micro.dev/install.sh | sh
|
||||
sudo micro init --server
|
||||
|
||||
# From your laptop
|
||||
micro deploy user@your-server
|
||||
```
|
||||
|
||||
The deploy command:
|
||||
1. Builds binaries for Linux
|
||||
2. Copies via SSH to the server
|
||||
3. Sets up systemd services
|
||||
4. Verifies services are healthy
|
||||
|
||||
Manage deployed services:
|
||||
```bash
|
||||
micro status --remote user@server # Check status
|
||||
micro logs --remote user@server # View logs
|
||||
micro logs myservice --remote user@server -f # Follow specific service
|
||||
```
|
||||
|
||||
No Docker required. No Kubernetes. Just systemd.
|
||||
|
||||
See [docs/deployment.md](docs/deployment.md) for full deployment guide.
|
||||
|
||||
See [cmd/micro/README.md](cmd/micro/README.md) for full CLI documentation.
|
||||
|
||||
Docs: [`internal/website/docs`](internal/website/docs)
|
||||
|
||||
Package reference: https://pkg.go.dev/go-micro.dev/v5
|
||||
|
||||
Selected topics:
|
||||
- Getting Started: [`internal/website/docs/getting-started.md`](internal/website/docs/getting-started.md)
|
||||
- Plugins overview: [`internal/website/docs/plugins.md`](internal/website/docs/plugins.md)
|
||||
- Learn by Example: [`internal/website/docs/examples/index.md`](internal/website/docs/examples/index.md)
|
||||
- Performance Considerations: [`docs/performance.md`](docs/performance.md)
|
||||
|
||||
## Adopters
|
||||
|
||||
- [Sourse](https://sourse.eu) - Work in the field of earth observation, including embedded Kubernetes running onboard aircraft, and we’ve built a mission management SaaS platform using Go Micro.
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# Go Micro [](https://opensource.org/licenses/Apache-2.0) [](https://pkg.go.dev/github.com/micro/go-micro?tab=doc) [](https://travis-ci.org/micro/go-micro) [](https://goreportcard.com/report/github.com/micro/go-micro)
|
||||
|
||||
Go Micro是基于Golang的微服务开发框架。
|
||||
|
||||
## 概览
|
||||
|
||||
Go Micro提供分布式系统开发的核心库,包含RPC与事件驱动的通信机制。
|
||||
|
||||
**micro**的设计哲学是可插拔的架构理念,她提供可快速构建系统的组件,并且可以根据自身的需求剥离默认实现并自行定制。
|
||||
|
||||
<img src="https://micro.mu/docs/images/go-micro.svg" />
|
||||
|
||||
所有插件可在仓库[github.com/micro/go-plugins](https://github.com/micro/go-plugins)中找到。
|
||||
|
||||
可以订阅我们的[Twitter](https://twitter.com/microhq)或者加入[Slack](http://slack.micro.mu/)论坛。
|
||||
|
||||
## 特性
|
||||
|
||||
Go Micro把分布式系统的各种细节抽象出来。下面是它的主要特性。
|
||||
|
||||
- **服务发现(Service Discovery)** - 自动服务注册与名称解析。服务发现是微服务开发中的核心。当服务A要与服务B协作时,它得知道B在哪里。默认的服务发现系统是Consul,而multicast DNS (mdns,组播)机制作为本地解决方案,或者零依赖的P2P网络中的SWIM协议(gossip)。
|
||||
|
||||
- **负载均衡(Load Balancing)** - 在服务发现之上构建了负载均衡机制。当我们得到一个服务的任意多个的实例节点时,我们要一个机制去决定要路由到哪一个节点。我们使用随机处理过的哈希负载均衡机制来保证对服务请求颁发的均匀分布,并且在发生问题时进行重试。
|
||||
|
||||
- **消息编码(Message Encoding)** - 支持基于内容类型(content-type)动态编码消息。客户端和服务端会一起使用content-type的格式来对Go进行无缝编/解码。各种各样的消息被编码会发送到不同的客户端,客户端服服务端默认会处理这些消息。content-type默认包含proto-rpc和json-rpc。
|
||||
|
||||
- **Request/Response** - RPC通信基于支持双向流的请求/响应方式,我们提供有抽象的同步通信机制。请求发送到服务时,会自动解析、负载均衡、拨号、转成字节流。默认的传输协议是http/1.1,而tls下使用http2协议。
|
||||
|
||||
- **异步消息(Async Messaging)** - 发布订阅(PubSub)头等功能内置在异步通信与事件驱动架构中。事件通知在微服务开发中处于核心位置。默认的消息传送使用点到点http/1.1,激活tls时则使用http2。
|
||||
|
||||
- **可插拔接口(Pluggable Interfaces)** - Go Micro为每个分布式系统抽象出接口。因此,Go Micro的接口都是可插拔的,允许其在运行时不可知的情况下仍可支持。所以只要实现接口,可以在内部使用任何的技术。更多插件请参考:[github.com/micro/go-plugins](https://github.com/micro/go-plugins)。
|
||||
|
||||
## 快速上手
|
||||
|
||||
更多关于架构、安装的资料可以查看[文档](https://micro.mu/docs/cn/)。
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
# Go Micro Roadmap
|
||||
|
||||
This roadmap outlines the planned features and improvements for Go Micro. Community feedback and contributions are welcome!
|
||||
|
||||
## Current Focus (Q1 2026)
|
||||
|
||||
### Documentation & Developer Experience
|
||||
- [x] Modernize documentation structure
|
||||
- [x] Add learn-by-example guides
|
||||
- [x] Update issue templates
|
||||
- [ ] Create video tutorials
|
||||
- [ ] Interactive documentation site
|
||||
- [ ] Plugin discovery dashboard
|
||||
|
||||
### Observability
|
||||
- [ ] OpenTelemetry native support
|
||||
- [ ] Auto-instrumentation for handlers
|
||||
- [ ] Metrics export standardization
|
||||
- [ ] Distributed tracing examples
|
||||
- [ ] Integration with popular observability platforms
|
||||
|
||||
### Developer Tools
|
||||
- [ ] `micro dev` with hot reload
|
||||
- [ ] Service templates (`micro new --template`)
|
||||
- [ ] Better error messages with suggestions
|
||||
- [ ] Debug tooling improvements
|
||||
- [ ] VS Code extension for Go Micro
|
||||
|
||||
## Q2 2026
|
||||
|
||||
### Production Readiness
|
||||
- [ ] Health check standardization
|
||||
- [ ] Graceful shutdown improvements
|
||||
- [ ] Resource cleanup best practices
|
||||
- [ ] Load testing framework integration
|
||||
- [ ] Performance benchmarking suite
|
||||
|
||||
### Cloud Native
|
||||
- [ ] Kubernetes operator
|
||||
- [ ] Helm charts for common setups
|
||||
- [ ] Service mesh integration guides (Istio, Linkerd)
|
||||
- [ ] Cloud provider quickstarts (AWS, GCP, Azure)
|
||||
- [ ] Multi-cluster patterns
|
||||
|
||||
### Security
|
||||
- [ ] mTLS by default option
|
||||
- [ ] Secret management integration (Vault, AWS Secrets Manager)
|
||||
- [ ] RBAC improvements
|
||||
- [ ] Security audit and hardening
|
||||
- [ ] CVE scanning and response process
|
||||
|
||||
## Q3 2026
|
||||
|
||||
### Plugin Ecosystem
|
||||
- [ ] Plugin marketplace/registry
|
||||
- [ ] Plugin quality standards
|
||||
- [ ] Community plugin contributions
|
||||
- [ ] Plugin compatibility matrix
|
||||
- [ ] Auto-discovery of available plugins
|
||||
|
||||
### Streaming & Async
|
||||
- [ ] Improved streaming support
|
||||
- [ ] Server-sent events (SSE) support
|
||||
- [ ] WebSocket plugin
|
||||
- [ ] Event sourcing patterns
|
||||
- [ ] CQRS examples
|
||||
|
||||
### Testing
|
||||
- [ ] Mock generation tooling
|
||||
- [ ] Integration test helpers
|
||||
- [ ] Contract testing support
|
||||
- [ ] Chaos engineering examples
|
||||
- [ ] E2E testing framework
|
||||
|
||||
## Q4 2026
|
||||
|
||||
### Performance
|
||||
- [ ] Connection pooling optimizations
|
||||
- [ ] Zero-allocation paths
|
||||
- [ ] gRPC performance improvements
|
||||
- [ ] Caching strategies guide
|
||||
- [ ] Performance profiling tools
|
||||
|
||||
### Developer Productivity
|
||||
- [ ] Code generation improvements
|
||||
- [ ] Better IDE support
|
||||
- [ ] Debugging tools
|
||||
- [ ] Migration automation tools
|
||||
- [ ] Upgrade helpers
|
||||
|
||||
### Community
|
||||
- [ ] Regular blog posts and case studies
|
||||
- [ ] Community spotlight program
|
||||
- [ ] Contribution rewards
|
||||
- [ ] Monthly community calls
|
||||
- [ ] Conference presence
|
||||
|
||||
## Long-term Vision
|
||||
|
||||
### Core Framework
|
||||
- Maintain backward compatibility (Go Micro v5+)
|
||||
- Progressive disclosure of complexity
|
||||
- Best-in-class developer experience
|
||||
- Production-grade reliability
|
||||
- Comprehensive plugin ecosystem
|
||||
|
||||
### Ecosystem Goals
|
||||
- 100+ production deployments documented
|
||||
- 50+ community plugins
|
||||
- Active contributor community
|
||||
- Regular releases (monthly patches, quarterly features)
|
||||
- Comprehensive benchmarks vs alternatives
|
||||
|
||||
### Differentiation
|
||||
- **Batteries included, fully swappable** - Start simple, scale complex
|
||||
- **Zero-config local development** - No infrastructure required to start
|
||||
- **Plugin ecosystem in-repo** - No version compatibility hell
|
||||
- **Progressive complexity** - Learn as you grow
|
||||
- **Cloud-native first** - Built for Kubernetes and containers
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions to any roadmap items! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
### High Priority Areas
|
||||
1. Documentation improvements
|
||||
2. Real-world examples
|
||||
3. Plugin development
|
||||
4. Performance optimizations
|
||||
5. Testing infrastructure
|
||||
|
||||
### How to Contribute
|
||||
- Pick an item from the roadmap
|
||||
- Open an issue to discuss approach
|
||||
- Submit a PR with implementation
|
||||
- Help review others' contributions
|
||||
|
||||
## Feedback
|
||||
|
||||
Have suggestions for the roadmap?
|
||||
|
||||
- Open a [feature request](.github/ISSUE_TEMPLATE/feature_request.md)
|
||||
- Start a discussion in GitHub Discussions
|
||||
- Comment on existing roadmap issues
|
||||
|
||||
## Version Compatibility
|
||||
|
||||
We follow semantic versioning:
|
||||
- Major versions (v5 → v6): Breaking changes
|
||||
- Minor versions (v5.3 → v5.4): New features, backward compatible
|
||||
- Patch versions (v5.3.0 → v5.3.1): Bug fixes, no API changes
|
||||
|
||||
## Support Timeline
|
||||
|
||||
- v5: Active development (current)
|
||||
- v4: Security fixes only (until v6 release)
|
||||
- v3: End of life
|
||||
|
||||
---
|
||||
|
||||
Last updated: November 2025
|
||||
|
||||
This roadmap is subject to change based on community needs and priorities. Star the repo to stay updated! ⭐
|
||||
@@ -1 +0,0 @@
|
||||
theme: jekyll-theme-architect
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package agent provides an interface for building robots
|
||||
package agent
|
||||
@@ -1,54 +0,0 @@
|
||||
// Package command is an interface for defining bot commands
|
||||
package command
|
||||
|
||||
var (
|
||||
// Commmands keyed by golang/regexp patterns
|
||||
// regexp.Match(key, input) is used to match
|
||||
Commands = map[string]Command{}
|
||||
)
|
||||
|
||||
// Command is the interface for specific named
|
||||
// commands executed via plugins or the bot.
|
||||
type Command interface {
|
||||
// Executes the command with args passed in
|
||||
Exec(args ...string) ([]byte, error)
|
||||
// Usage of the command
|
||||
Usage() string
|
||||
// Description of the command
|
||||
Description() string
|
||||
// Name of the command
|
||||
String() string
|
||||
}
|
||||
|
||||
type cmd struct {
|
||||
name string
|
||||
usage string
|
||||
description string
|
||||
exec func(args ...string) ([]byte, error)
|
||||
}
|
||||
|
||||
func (c *cmd) Description() string {
|
||||
return c.description
|
||||
}
|
||||
|
||||
func (c *cmd) Exec(args ...string) ([]byte, error) {
|
||||
return c.exec(args...)
|
||||
}
|
||||
|
||||
func (c *cmd) Usage() string {
|
||||
return c.usage
|
||||
}
|
||||
|
||||
func (c *cmd) String() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
// NewCommand helps quickly create a new command
|
||||
func NewCommand(name, usage, description string, exec func(args ...string) ([]byte, error)) Command {
|
||||
return &cmd{
|
||||
name: name,
|
||||
usage: usage,
|
||||
description: description,
|
||||
exec: exec,
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommand(t *testing.T) {
|
||||
c := &cmd{
|
||||
name: "test",
|
||||
usage: "test usage",
|
||||
description: "test description",
|
||||
exec: func(args ...string) ([]byte, error) {
|
||||
return []byte("test"), nil
|
||||
},
|
||||
}
|
||||
|
||||
if c.String() != c.name {
|
||||
t.Fatalf("expected name %s got %s", c.name, c.String())
|
||||
}
|
||||
|
||||
if c.Usage() != c.usage {
|
||||
t.Fatalf("expected usage %s got %s", c.usage, c.Usage())
|
||||
}
|
||||
|
||||
if c.Description() != c.description {
|
||||
t.Fatalf("expected description %s got %s", c.description, c.Description())
|
||||
}
|
||||
|
||||
if r, err := c.Exec(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if string(r) != "test" {
|
||||
t.Fatalf("expected exec result test got %s", string(r))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCommand(t *testing.T) {
|
||||
c := &cmd{
|
||||
name: "test",
|
||||
usage: "test usage",
|
||||
description: "test description",
|
||||
exec: func(args ...string) ([]byte, error) {
|
||||
return []byte("test"), nil
|
||||
},
|
||||
}
|
||||
|
||||
nc := NewCommand(c.name, c.usage, c.description, c.exec)
|
||||
|
||||
if nc.String() != c.name {
|
||||
t.Fatalf("expected name %s got %s", c.name, nc.String())
|
||||
}
|
||||
|
||||
if nc.Usage() != c.usage {
|
||||
t.Fatalf("expected usage %s got %s", c.usage, nc.Usage())
|
||||
}
|
||||
|
||||
if nc.Description() != c.description {
|
||||
t.Fatalf("expected description %s got %s", c.description, nc.Description())
|
||||
}
|
||||
|
||||
if r, err := nc.Exec(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if string(r) != "test" {
|
||||
t.Fatalf("expected exec result test got %s", string(r))
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
# Discord input for micro-bot
|
||||
[Discord](https://discordapp.com) support for micro bot based on [discordgo](github.com/bwmarrin/discordgo).
|
||||
|
||||
This was originally written by Aleksandr Tihomirov (@zet4) and can be found at https://github.com/zet4/micro-misc/.
|
||||
|
||||
## Options
|
||||
### discord_token
|
||||
|
||||
You have to supply an application token via `--discord_token`.
|
||||
|
||||
Head over to Discord's [developer introduction](https://discordapp.com/developers/docs/intro)
|
||||
to learn how to create applications and how the API works.
|
||||
|
||||
### discord_prefix
|
||||
|
||||
Set a command prefix with `--discord_prefix`. The default prefix is `Micro `.
|
||||
You can mention the bot or use the prefix to run a command.
|
||||
|
||||
### discord_whitelist
|
||||
|
||||
Pass a list of comma-separated user IDs with `--discord_whitelist`. Only allow
|
||||
these users to use the bot.
|
||||
@@ -1,96 +0,0 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/micro/go-micro/v2/agent/input"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
)
|
||||
|
||||
type discordConn struct {
|
||||
master *discordInput
|
||||
exit chan struct{}
|
||||
recv chan *discordgo.Message
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func newConn(master *discordInput) *discordConn {
|
||||
conn := &discordConn{
|
||||
master: master,
|
||||
exit: make(chan struct{}),
|
||||
recv: make(chan *discordgo.Message),
|
||||
}
|
||||
|
||||
conn.master.session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
if m.Author.ID == master.botID {
|
||||
return
|
||||
}
|
||||
|
||||
whitelisted := false
|
||||
for _, ID := range conn.master.whitelist {
|
||||
if m.Author.ID == ID {
|
||||
whitelisted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(master.whitelist) > 0 && !whitelisted {
|
||||
return
|
||||
}
|
||||
|
||||
var valid bool
|
||||
m.Message.Content, valid = conn.master.prefixfn(m.Message.Content)
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
conn.recv <- m.Message
|
||||
})
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func (dc *discordConn) Recv(event *input.Event) error {
|
||||
for {
|
||||
select {
|
||||
case <-dc.exit:
|
||||
return errors.New("connection closed")
|
||||
case msg := <-dc.recv:
|
||||
|
||||
event.From = msg.ChannelID + ":" + msg.Author.ID
|
||||
event.To = dc.master.botID
|
||||
event.Type = input.TextEvent
|
||||
event.Data = []byte(msg.Content)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *discordConn) Send(e *input.Event) error {
|
||||
fields := strings.Split(e.To, ":")
|
||||
_, err := dc.master.session.ChannelMessageSend(fields[0], string(e.Data))
|
||||
if err != nil {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Error("[bot][loop][send]", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *discordConn) Close() error {
|
||||
if err := dc.master.session.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-dc.exit:
|
||||
return nil
|
||||
default:
|
||||
close(dc.exit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/micro/cli/v2"
|
||||
"github.com/micro/go-micro/v2/agent/input"
|
||||
)
|
||||
|
||||
func init() {
|
||||
input.Inputs["discord"] = newInput()
|
||||
}
|
||||
|
||||
func newInput() *discordInput {
|
||||
return &discordInput{}
|
||||
}
|
||||
|
||||
type discordInput struct {
|
||||
token string
|
||||
whitelist []string
|
||||
prefix string
|
||||
prefixfn func(string) (string, bool)
|
||||
botID string
|
||||
|
||||
session *discordgo.Session
|
||||
|
||||
sync.Mutex
|
||||
running bool
|
||||
exit chan struct{}
|
||||
}
|
||||
|
||||
func (d *discordInput) Flags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "discord_token",
|
||||
EnvVars: []string{"MICRO_DISCORD_TOKEN"},
|
||||
Usage: "Discord token (prefix with Bot if it's for bot account)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "discord_whitelist",
|
||||
EnvVars: []string{"MICRO_DISCORD_WHITELIST"},
|
||||
Usage: "Discord Whitelist (seperated by ,)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "discord_prefix",
|
||||
Usage: "Discord Prefix",
|
||||
EnvVars: []string{"MICRO_DISCORD_PREFIX"},
|
||||
Value: "Micro ",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *discordInput) Init(ctx *cli.Context) error {
|
||||
token := ctx.String("discord_token")
|
||||
whitelist := ctx.String("discord_whitelist")
|
||||
prefix := ctx.String("discord_prefix")
|
||||
|
||||
if len(token) == 0 {
|
||||
return errors.New("require token")
|
||||
}
|
||||
|
||||
d.token = token
|
||||
d.prefix = prefix
|
||||
|
||||
if len(whitelist) > 0 {
|
||||
d.whitelist = strings.Split(whitelist, ",")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *discordInput) Start() error {
|
||||
if len(d.token) == 0 {
|
||||
return errors.New("missing discord configuration")
|
||||
}
|
||||
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
|
||||
if d.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
d.session, err = discordgo.New(d.token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u, err := d.session.User("@me")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.botID = u.ID
|
||||
d.prefixfn = CheckPrefixFactory(fmt.Sprintf("<@%s> ", d.botID), fmt.Sprintf("<@!%s> ", d.botID), d.prefix)
|
||||
|
||||
d.exit = make(chan struct{})
|
||||
d.running = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *discordInput) Stream() (input.Conn, error) {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
if !d.running {
|
||||
return nil, errors.New("not running")
|
||||
}
|
||||
|
||||
//Fire-n-forget close just in case...
|
||||
d.session.Close()
|
||||
|
||||
conn := newConn(d)
|
||||
if err := d.session.Open(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (d *discordInput) Stop() error {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
|
||||
if !d.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
close(d.exit)
|
||||
d.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *discordInput) String() string {
|
||||
return "discord"
|
||||
}
|
||||
|
||||
// CheckPrefixFactory Creates a prefix checking function and stuff.
|
||||
func CheckPrefixFactory(prefixes ...string) func(string) (string, bool) {
|
||||
return func(content string) (string, bool) {
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(content, prefix) {
|
||||
return strings.TrimPrefix(content, prefix), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// Package input is an interface for bot inputs
|
||||
package input
|
||||
|
||||
import (
|
||||
"github.com/micro/cli/v2"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
TextEvent EventType = "text"
|
||||
)
|
||||
|
||||
var (
|
||||
// Inputs keyed by name
|
||||
// Example slack or hipchat
|
||||
Inputs = map[string]Input{}
|
||||
)
|
||||
|
||||
// Event is the unit sent and received
|
||||
type Event struct {
|
||||
Type EventType
|
||||
From string
|
||||
To string
|
||||
Data []byte
|
||||
Meta map[string]interface{}
|
||||
}
|
||||
|
||||
// Input is an interface for sources which
|
||||
// provide a way to communicate with the bot.
|
||||
// Slack, HipChat, XMPP, etc.
|
||||
type Input interface {
|
||||
// Provide cli flags
|
||||
Flags() []cli.Flag
|
||||
// Initialise input using cli context
|
||||
Init(*cli.Context) error
|
||||
// Stream events from the input
|
||||
Stream() (Conn, error)
|
||||
// Start the input
|
||||
Start() error
|
||||
// Stop the input
|
||||
Stop() error
|
||||
// name of the input
|
||||
String() string
|
||||
}
|
||||
|
||||
// Conn interface provides a way to
|
||||
// send and receive events. Send and
|
||||
// Recv both block until succeeding
|
||||
// or failing.
|
||||
type Conn interface {
|
||||
Close() error
|
||||
Recv(*Event) error
|
||||
Send(*Event) error
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
package slack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/micro/go-micro/v2/agent/input"
|
||||
"github.com/nlopes/slack"
|
||||
)
|
||||
|
||||
// Satisfies the input.Conn interface
|
||||
type slackConn struct {
|
||||
auth *slack.AuthTestResponse
|
||||
rtm *slack.RTM
|
||||
exit chan bool
|
||||
|
||||
sync.Mutex
|
||||
names map[string]string
|
||||
}
|
||||
|
||||
func (s *slackConn) run() {
|
||||
// func retrieves user names and maps to IDs
|
||||
setNames := func() {
|
||||
names := make(map[string]string)
|
||||
users, err := s.rtm.Client.GetUsers()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
names[user.ID] = user.Name
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
s.names = names
|
||||
s.Unlock()
|
||||
}
|
||||
|
||||
setNames()
|
||||
|
||||
t := time.NewTicker(time.Minute)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.exit:
|
||||
return
|
||||
case <-t.C:
|
||||
setNames()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *slackConn) getName(id string) string {
|
||||
s.Lock()
|
||||
name := s.names[id]
|
||||
s.Unlock()
|
||||
return name
|
||||
}
|
||||
|
||||
func (s *slackConn) Close() error {
|
||||
select {
|
||||
case <-s.exit:
|
||||
return nil
|
||||
default:
|
||||
close(s.exit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *slackConn) Recv(event *input.Event) error {
|
||||
if event == nil {
|
||||
return errors.New("event cannot be nil")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.exit:
|
||||
return errors.New("connection closed")
|
||||
case e := <-s.rtm.IncomingEvents:
|
||||
switch ev := e.Data.(type) {
|
||||
case *slack.MessageEvent:
|
||||
// only accept type message
|
||||
if ev.Type != "message" {
|
||||
continue
|
||||
}
|
||||
|
||||
// only accept DMs or messages to me
|
||||
switch {
|
||||
case strings.HasPrefix(ev.Channel, "D"):
|
||||
case strings.HasPrefix(ev.Text, s.auth.User):
|
||||
case strings.HasPrefix(ev.Text, fmt.Sprintf("<@%s>", s.auth.UserID)):
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// Strip username from text
|
||||
switch {
|
||||
case strings.HasPrefix(ev.Text, s.auth.User):
|
||||
args := strings.Split(ev.Text, " ")[1:]
|
||||
ev.Text = strings.Join(args, " ")
|
||||
event.To = s.auth.User
|
||||
case strings.HasPrefix(ev.Text, fmt.Sprintf("<@%s>", s.auth.UserID)):
|
||||
args := strings.Split(ev.Text, " ")[1:]
|
||||
ev.Text = strings.Join(args, " ")
|
||||
event.To = s.auth.UserID
|
||||
}
|
||||
|
||||
if event.Meta == nil {
|
||||
event.Meta = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// fill in the blanks
|
||||
event.From = ev.Channel + ":" + ev.User
|
||||
event.Type = input.TextEvent
|
||||
event.Data = []byte(ev.Text)
|
||||
event.Meta["reply"] = ev
|
||||
return nil
|
||||
case *slack.InvalidAuthEvent:
|
||||
return errors.New("invalid credentials")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *slackConn) Send(event *input.Event) error {
|
||||
var channel, message, name string
|
||||
|
||||
if len(event.To) == 0 {
|
||||
return errors.New("require Event.To")
|
||||
}
|
||||
|
||||
parts := strings.Split(event.To, ":")
|
||||
|
||||
if len(parts) == 2 {
|
||||
channel = parts[0]
|
||||
name = s.getName(parts[1])
|
||||
// try using reply meta
|
||||
} else if ev, ok := event.Meta["reply"]; ok {
|
||||
channel = ev.(*slack.MessageEvent).Channel
|
||||
name = s.getName(ev.(*slack.MessageEvent).User)
|
||||
}
|
||||
|
||||
// don't know where to send the message
|
||||
if len(channel) == 0 {
|
||||
return errors.New("could not determine who message is to")
|
||||
}
|
||||
|
||||
if len(name) == 0 || strings.HasPrefix(channel, "D") {
|
||||
message = string(event.Data)
|
||||
} else {
|
||||
message = fmt.Sprintf("@%s: %s", name, string(event.Data))
|
||||
}
|
||||
|
||||
s.rtm.SendMessage(s.rtm.NewOutgoingMessage(message, channel))
|
||||
return nil
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package slack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/micro/cli/v2"
|
||||
"github.com/micro/go-micro/v2/agent/input"
|
||||
"github.com/nlopes/slack"
|
||||
)
|
||||
|
||||
type slackInput struct {
|
||||
debug bool
|
||||
token string
|
||||
|
||||
sync.Mutex
|
||||
running bool
|
||||
exit chan bool
|
||||
|
||||
api *slack.Client
|
||||
}
|
||||
|
||||
func init() {
|
||||
input.Inputs["slack"] = NewInput()
|
||||
}
|
||||
|
||||
func (p *slackInput) Flags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "slack_debug",
|
||||
Usage: "Slack debug output",
|
||||
EnvVars: []string{"MICRO_SLACK_DEBUG"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "slack_token",
|
||||
Usage: "Slack token",
|
||||
EnvVars: []string{"MICRO_SLACK_TOKEN"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *slackInput) Init(ctx *cli.Context) error {
|
||||
debug := ctx.Bool("slack_debug")
|
||||
token := ctx.String("slack_token")
|
||||
|
||||
if len(token) == 0 {
|
||||
return errors.New("missing slack token")
|
||||
}
|
||||
|
||||
p.debug = debug
|
||||
p.token = token
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *slackInput) Stream() (input.Conn, error) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
if !p.running {
|
||||
return nil, errors.New("not running")
|
||||
}
|
||||
|
||||
// test auth
|
||||
auth, err := p.api.AuthTest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtm := p.api.NewRTM()
|
||||
exit := make(chan bool)
|
||||
|
||||
go rtm.ManageConnection()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-p.exit:
|
||||
select {
|
||||
case <-exit:
|
||||
return
|
||||
default:
|
||||
close(exit)
|
||||
}
|
||||
case <-exit:
|
||||
}
|
||||
|
||||
rtm.Disconnect()
|
||||
}()
|
||||
|
||||
conn := &slackConn{
|
||||
auth: auth,
|
||||
rtm: rtm,
|
||||
exit: exit,
|
||||
names: make(map[string]string),
|
||||
}
|
||||
|
||||
go conn.run()
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (p *slackInput) Start() error {
|
||||
if len(p.token) == 0 {
|
||||
return errors.New("missing slack token")
|
||||
}
|
||||
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
if p.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
api := slack.New(p.token, slack.OptionDebug(p.debug))
|
||||
|
||||
// test auth
|
||||
_, err := api.AuthTest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.api = api
|
||||
p.exit = make(chan bool)
|
||||
p.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *slackInput) Stop() error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
if !p.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
close(p.exit)
|
||||
p.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *slackInput) String() string {
|
||||
return "slack"
|
||||
}
|
||||
|
||||
func NewInput() input.Input {
|
||||
return &slackInput{}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
# Telegram Messenger input for micro bot
|
||||
[Telegram](https://telegram.org) support for micro bot based on [telegram-bot-api](https://github.com/go-telegram-bot-api/telegram-bot-api).
|
||||
|
||||
## Options
|
||||
### --telegram_token (required)
|
||||
|
||||
Sets bot's token for interacting with API.
|
||||
|
||||
Head over to Telegram's [API documentation](https://core.telegram.org/bots/api)
|
||||
to learn how to create bots and how the API works.
|
||||
|
||||
### --telegram_debug
|
||||
|
||||
Sets the debug flag to make the bot's output verbose.
|
||||
|
||||
### --telegram_whitelist
|
||||
|
||||
Sets a list of comma-separated nicknames (without @ symbol in the beginning) for interacting with bot. Only these users can use the bot.
|
||||
@@ -1,115 +0,0 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/forestgiant/sliceutil"
|
||||
"github.com/micro/go-micro/v2/agent/input"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
tgbotapi "gopkg.in/telegram-bot-api.v4"
|
||||
)
|
||||
|
||||
type telegramConn struct {
|
||||
input *telegramInput
|
||||
|
||||
recv <-chan tgbotapi.Update
|
||||
exit chan bool
|
||||
|
||||
syncCond *sync.Cond
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func newConn(input *telegramInput) (*telegramConn, error) {
|
||||
conn := &telegramConn{
|
||||
input: input,
|
||||
}
|
||||
|
||||
conn.syncCond = sync.NewCond(&conn.mutex)
|
||||
|
||||
go conn.run()
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (tc *telegramConn) run() {
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
updates, err := tc.input.api.GetUpdatesChan(u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tc.recv = updates
|
||||
tc.syncCond.Signal()
|
||||
|
||||
select {
|
||||
case <-tc.exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *telegramConn) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *telegramConn) Recv(event *input.Event) error {
|
||||
if event == nil {
|
||||
return errors.New("event cannot be nil")
|
||||
}
|
||||
|
||||
for {
|
||||
if tc.recv == nil {
|
||||
tc.mutex.Lock()
|
||||
tc.syncCond.Wait()
|
||||
}
|
||||
|
||||
update := <-tc.recv
|
||||
|
||||
if update.Message == nil || (len(tc.input.whitelist) > 0 && !sliceutil.Contains(tc.input.whitelist, update.Message.From.UserName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if event.Meta == nil {
|
||||
event.Meta = make(map[string]interface{})
|
||||
}
|
||||
|
||||
event.Type = input.TextEvent
|
||||
event.From = update.Message.From.UserName
|
||||
event.To = tc.input.api.Self.UserName
|
||||
event.Data = []byte(update.Message.Text)
|
||||
event.Meta["chatId"] = update.Message.Chat.ID
|
||||
event.Meta["chatType"] = update.Message.Chat.Type
|
||||
event.Meta["messageId"] = update.Message.MessageID
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *telegramConn) Send(event *input.Event) error {
|
||||
messageText := strings.TrimSpace(string(event.Data))
|
||||
|
||||
chatId := event.Meta["chatId"].(int64)
|
||||
chatType := ChatType(event.Meta["chatType"].(string))
|
||||
|
||||
msgConfig := tgbotapi.NewMessage(chatId, messageText)
|
||||
msgConfig.ParseMode = tgbotapi.ModeHTML
|
||||
|
||||
if sliceutil.Contains([]ChatType{Group, Supergroup}, chatType) {
|
||||
msgConfig.ReplyToMessageID = event.Meta["messageId"].(int)
|
||||
}
|
||||
|
||||
_, err := tc.input.api.Send(msgConfig)
|
||||
|
||||
if err != nil {
|
||||
// probably it could be because of nested HTML tags -- telegram doesn't allow nested tags
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Error("[telegram][Send] error:", err)
|
||||
}
|
||||
msgConfig.Text = "This bot couldn't send the response (Internal error)"
|
||||
tc.input.api.Send(msgConfig)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/micro/cli/v2"
|
||||
"github.com/micro/go-micro/v2/agent/input"
|
||||
tgbotapi "gopkg.in/telegram-bot-api.v4"
|
||||
)
|
||||
|
||||
type telegramInput struct {
|
||||
sync.Mutex
|
||||
|
||||
debug bool
|
||||
token string
|
||||
whitelist []string
|
||||
|
||||
api *tgbotapi.BotAPI
|
||||
}
|
||||
|
||||
type ChatType string
|
||||
|
||||
const (
|
||||
Private ChatType = "private"
|
||||
Group ChatType = "group"
|
||||
Supergroup ChatType = "supergroup"
|
||||
)
|
||||
|
||||
func init() {
|
||||
input.Inputs["telegram"] = &telegramInput{}
|
||||
}
|
||||
|
||||
func (ti *telegramInput) Flags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "telegram_debug",
|
||||
EnvVars: []string{"MICRO_TELEGRAM_DEBUG"},
|
||||
Usage: "Telegram debug output",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "telegram_token",
|
||||
EnvVars: []string{"MICRO_TELEGRAM_TOKEN"},
|
||||
Usage: "Telegram token",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "telegram_whitelist",
|
||||
EnvVars: []string{"MICRO_TELEGRAM_WHITELIST"},
|
||||
Usage: "Telegram bot's users (comma-separated values)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (ti *telegramInput) Init(ctx *cli.Context) error {
|
||||
ti.debug = ctx.Bool("telegram_debug")
|
||||
ti.token = ctx.String("telegram_token")
|
||||
|
||||
whitelist := ctx.String("telegram_whitelist")
|
||||
|
||||
if whitelist != "" {
|
||||
ti.whitelist = strings.Split(whitelist, ",")
|
||||
}
|
||||
|
||||
if len(ti.token) == 0 {
|
||||
return errors.New("missing telegram token")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ti *telegramInput) Stream() (input.Conn, error) {
|
||||
ti.Lock()
|
||||
defer ti.Unlock()
|
||||
|
||||
return newConn(ti)
|
||||
}
|
||||
|
||||
func (ti *telegramInput) Start() error {
|
||||
ti.Lock()
|
||||
defer ti.Unlock()
|
||||
|
||||
api, err := tgbotapi.NewBotAPI(ti.token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ti.api = api
|
||||
|
||||
api.Debug = ti.debug
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ti *telegramInput) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *telegramInput) String() string {
|
||||
return "telegram"
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// source: agent/proto/bot.proto
|
||||
|
||||
package go_micro_bot
|
||||
|
||||
import (
|
||||
context "context"
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
math "math"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
|
||||
|
||||
type HelpRequest struct {
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *HelpRequest) Reset() { *m = HelpRequest{} }
|
||||
func (m *HelpRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*HelpRequest) ProtoMessage() {}
|
||||
func (*HelpRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_79b974b8c77805fa, []int{0}
|
||||
}
|
||||
|
||||
func (m *HelpRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_HelpRequest.Unmarshal(m, b)
|
||||
}
|
||||
func (m *HelpRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_HelpRequest.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *HelpRequest) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_HelpRequest.Merge(m, src)
|
||||
}
|
||||
func (m *HelpRequest) XXX_Size() int {
|
||||
return xxx_messageInfo_HelpRequest.Size(m)
|
||||
}
|
||||
func (m *HelpRequest) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_HelpRequest.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_HelpRequest proto.InternalMessageInfo
|
||||
|
||||
type HelpResponse struct {
|
||||
Usage string `protobuf:"bytes,1,opt,name=usage,proto3" json:"usage,omitempty"`
|
||||
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *HelpResponse) Reset() { *m = HelpResponse{} }
|
||||
func (m *HelpResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*HelpResponse) ProtoMessage() {}
|
||||
func (*HelpResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_79b974b8c77805fa, []int{1}
|
||||
}
|
||||
|
||||
func (m *HelpResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_HelpResponse.Unmarshal(m, b)
|
||||
}
|
||||
func (m *HelpResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_HelpResponse.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *HelpResponse) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_HelpResponse.Merge(m, src)
|
||||
}
|
||||
func (m *HelpResponse) XXX_Size() int {
|
||||
return xxx_messageInfo_HelpResponse.Size(m)
|
||||
}
|
||||
func (m *HelpResponse) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_HelpResponse.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_HelpResponse proto.InternalMessageInfo
|
||||
|
||||
func (m *HelpResponse) GetUsage() string {
|
||||
if m != nil {
|
||||
return m.Usage
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *HelpResponse) GetDescription() string {
|
||||
if m != nil {
|
||||
return m.Description
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ExecRequest struct {
|
||||
Args []string `protobuf:"bytes,1,rep,name=args,proto3" json:"args,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ExecRequest) Reset() { *m = ExecRequest{} }
|
||||
func (m *ExecRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*ExecRequest) ProtoMessage() {}
|
||||
func (*ExecRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_79b974b8c77805fa, []int{2}
|
||||
}
|
||||
|
||||
func (m *ExecRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ExecRequest.Unmarshal(m, b)
|
||||
}
|
||||
func (m *ExecRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_ExecRequest.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *ExecRequest) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_ExecRequest.Merge(m, src)
|
||||
}
|
||||
func (m *ExecRequest) XXX_Size() int {
|
||||
return xxx_messageInfo_ExecRequest.Size(m)
|
||||
}
|
||||
func (m *ExecRequest) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_ExecRequest.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_ExecRequest proto.InternalMessageInfo
|
||||
|
||||
func (m *ExecRequest) GetArgs() []string {
|
||||
if m != nil {
|
||||
return m.Args
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ExecResponse struct {
|
||||
Result []byte `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"`
|
||||
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ExecResponse) Reset() { *m = ExecResponse{} }
|
||||
func (m *ExecResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*ExecResponse) ProtoMessage() {}
|
||||
func (*ExecResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_79b974b8c77805fa, []int{3}
|
||||
}
|
||||
|
||||
func (m *ExecResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ExecResponse.Unmarshal(m, b)
|
||||
}
|
||||
func (m *ExecResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_ExecResponse.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *ExecResponse) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_ExecResponse.Merge(m, src)
|
||||
}
|
||||
func (m *ExecResponse) XXX_Size() int {
|
||||
return xxx_messageInfo_ExecResponse.Size(m)
|
||||
}
|
||||
func (m *ExecResponse) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_ExecResponse.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_ExecResponse proto.InternalMessageInfo
|
||||
|
||||
func (m *ExecResponse) GetResult() []byte {
|
||||
if m != nil {
|
||||
return m.Result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ExecResponse) GetError() string {
|
||||
if m != nil {
|
||||
return m.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*HelpRequest)(nil), "go.micro.bot.HelpRequest")
|
||||
proto.RegisterType((*HelpResponse)(nil), "go.micro.bot.HelpResponse")
|
||||
proto.RegisterType((*ExecRequest)(nil), "go.micro.bot.ExecRequest")
|
||||
proto.RegisterType((*ExecResponse)(nil), "go.micro.bot.ExecResponse")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("agent/proto/bot.proto", fileDescriptor_79b974b8c77805fa) }
|
||||
|
||||
var fileDescriptor_79b974b8c77805fa = []byte{
|
||||
// 234 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0x3f, 0x4f, 0xc3, 0x30,
|
||||
0x10, 0xc5, 0x1b, 0x28, 0x45, 0xbd, 0x84, 0xc5, 0x02, 0x14, 0x3a, 0x05, 0x4f, 0x9d, 0x5c, 0x09,
|
||||
0x56, 0x24, 0x06, 0x04, 0x62, 0xce, 0x37, 0x48, 0xd2, 0x53, 0x14, 0xa9, 0xf1, 0x99, 0xb3, 0x23,
|
||||
0xf1, 0x1d, 0xf8, 0xd2, 0xc8, 0x7f, 0x06, 0xab, 0xea, 0x76, 0xcf, 0x67, 0xbd, 0xf7, 0x7b, 0x07,
|
||||
0x0f, 0xdd, 0x88, 0xda, 0x1d, 0x0c, 0x93, 0xa3, 0x43, 0x4f, 0x4e, 0x85, 0x49, 0x54, 0x23, 0xa9,
|
||||
0x79, 0x1a, 0x98, 0x54, 0x4f, 0x4e, 0xde, 0x41, 0xf9, 0x8d, 0x27, 0xd3, 0xe2, 0xcf, 0x82, 0xd6,
|
||||
0xc9, 0x2f, 0xa8, 0xa2, 0xb4, 0x86, 0xb4, 0x45, 0x71, 0x0f, 0x37, 0x8b, 0xed, 0x46, 0xac, 0x8b,
|
||||
0xa6, 0xd8, 0x6f, 0xdb, 0x28, 0x44, 0x03, 0xe5, 0x11, 0xed, 0xc0, 0x93, 0x71, 0x13, 0xe9, 0xfa,
|
||||
0x2a, 0xec, 0xf2, 0x27, 0xf9, 0x0c, 0xe5, 0xe7, 0x2f, 0x0e, 0xc9, 0x56, 0x08, 0x58, 0x77, 0x3c,
|
||||
0xda, 0xba, 0x68, 0xae, 0xf7, 0xdb, 0x36, 0xcc, 0xf2, 0x0d, 0xaa, 0xf8, 0x25, 0x45, 0x3d, 0xc2,
|
||||
0x86, 0xd1, 0x2e, 0x27, 0x17, 0xb2, 0xaa, 0x36, 0x29, 0x8f, 0x80, 0xcc, 0xc4, 0x29, 0x26, 0x8a,
|
||||
0x97, 0xbf, 0x02, 0x6e, 0x3f, 0x68, 0x9e, 0x3b, 0x7d, 0x14, 0xef, 0xb0, 0xf6, 0xd0, 0xe2, 0x49,
|
||||
0xe5, 0xd5, 0x54, 0xd6, 0x6b, 0xb7, 0xbb, 0xb4, 0x8a, 0xc1, 0x72, 0xe5, 0x0d, 0x3c, 0xca, 0xb9,
|
||||
0x41, 0xd6, 0xe0, 0xdc, 0x20, 0x27, 0x97, 0xab, 0x7e, 0x13, 0x4e, 0xfb, 0xfa, 0x1f, 0x00, 0x00,
|
||||
0xff, 0xff, 0xe8, 0x08, 0x5e, 0xad, 0x73, 0x01, 0x00, 0x00,
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ context.Context
|
||||
var _ grpc.ClientConn
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
const _ = grpc.SupportPackageIsVersion4
|
||||
|
||||
// CommandClient is the client API for Command service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
|
||||
type CommandClient interface {
|
||||
Help(ctx context.Context, in *HelpRequest, opts ...grpc.CallOption) (*HelpResponse, error)
|
||||
Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error)
|
||||
}
|
||||
|
||||
type commandClient struct {
|
||||
cc *grpc.ClientConn
|
||||
}
|
||||
|
||||
func NewCommandClient(cc *grpc.ClientConn) CommandClient {
|
||||
return &commandClient{cc}
|
||||
}
|
||||
|
||||
func (c *commandClient) Help(ctx context.Context, in *HelpRequest, opts ...grpc.CallOption) (*HelpResponse, error) {
|
||||
out := new(HelpResponse)
|
||||
err := c.cc.Invoke(ctx, "/go.micro.bot.Command/Help", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *commandClient) Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error) {
|
||||
out := new(ExecResponse)
|
||||
err := c.cc.Invoke(ctx, "/go.micro.bot.Command/Exec", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CommandServer is the server API for Command service.
|
||||
type CommandServer interface {
|
||||
Help(context.Context, *HelpRequest) (*HelpResponse, error)
|
||||
Exec(context.Context, *ExecRequest) (*ExecResponse, error)
|
||||
}
|
||||
|
||||
// UnimplementedCommandServer can be embedded to have forward compatible implementations.
|
||||
type UnimplementedCommandServer struct {
|
||||
}
|
||||
|
||||
func (*UnimplementedCommandServer) Help(ctx context.Context, req *HelpRequest) (*HelpResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Help not implemented")
|
||||
}
|
||||
func (*UnimplementedCommandServer) Exec(ctx context.Context, req *ExecRequest) (*ExecResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Exec not implemented")
|
||||
}
|
||||
|
||||
func RegisterCommandServer(s *grpc.Server, srv CommandServer) {
|
||||
s.RegisterService(&_Command_serviceDesc, srv)
|
||||
}
|
||||
|
||||
func _Command_Help_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(HelpRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(CommandServer).Help(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/go.micro.bot.Command/Help",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(CommandServer).Help(ctx, req.(*HelpRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Command_Exec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ExecRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(CommandServer).Exec(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/go.micro.bot.Command/Exec",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(CommandServer).Exec(ctx, req.(*ExecRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
var _Command_serviceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "go.micro.bot.Command",
|
||||
HandlerType: (*CommandServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Help",
|
||||
Handler: _Command_Help_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Exec",
|
||||
Handler: _Command_Exec_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "agent/proto/bot.proto",
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
// Code generated by protoc-gen-micro. DO NOT EDIT.
|
||||
// source: agent/proto/bot.proto
|
||||
|
||||
package go_micro_bot
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
math "math"
|
||||
)
|
||||
|
||||
import (
|
||||
context "context"
|
||||
client "github.com/micro/go-micro/v2/client"
|
||||
server "github.com/micro/go-micro/v2/server"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ context.Context
|
||||
var _ client.Option
|
||||
var _ server.Option
|
||||
|
||||
// Client API for Command service
|
||||
|
||||
type CommandService interface {
|
||||
Help(ctx context.Context, in *HelpRequest, opts ...client.CallOption) (*HelpResponse, error)
|
||||
Exec(ctx context.Context, in *ExecRequest, opts ...client.CallOption) (*ExecResponse, error)
|
||||
}
|
||||
|
||||
type commandService struct {
|
||||
c client.Client
|
||||
name string
|
||||
}
|
||||
|
||||
func NewCommandService(name string, c client.Client) CommandService {
|
||||
return &commandService{
|
||||
c: c,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *commandService) Help(ctx context.Context, in *HelpRequest, opts ...client.CallOption) (*HelpResponse, error) {
|
||||
req := c.c.NewRequest(c.name, "Command.Help", in)
|
||||
out := new(HelpResponse)
|
||||
err := c.c.Call(ctx, req, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *commandService) Exec(ctx context.Context, in *ExecRequest, opts ...client.CallOption) (*ExecResponse, error) {
|
||||
req := c.c.NewRequest(c.name, "Command.Exec", in)
|
||||
out := new(ExecResponse)
|
||||
err := c.c.Call(ctx, req, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Server API for Command service
|
||||
|
||||
type CommandHandler interface {
|
||||
Help(context.Context, *HelpRequest, *HelpResponse) error
|
||||
Exec(context.Context, *ExecRequest, *ExecResponse) error
|
||||
}
|
||||
|
||||
func RegisterCommandHandler(s server.Server, hdlr CommandHandler, opts ...server.HandlerOption) error {
|
||||
type command interface {
|
||||
Help(ctx context.Context, in *HelpRequest, out *HelpResponse) error
|
||||
Exec(ctx context.Context, in *ExecRequest, out *ExecResponse) error
|
||||
}
|
||||
type Command struct {
|
||||
command
|
||||
}
|
||||
h := &commandHandler{hdlr}
|
||||
return s.Handle(s.NewHandler(&Command{h}, opts...))
|
||||
}
|
||||
|
||||
type commandHandler struct {
|
||||
CommandHandler
|
||||
}
|
||||
|
||||
func (h *commandHandler) Help(ctx context.Context, in *HelpRequest, out *HelpResponse) error {
|
||||
return h.CommandHandler.Help(ctx, in, out)
|
||||
}
|
||||
|
||||
func (h *commandHandler) Exec(ctx context.Context, in *ExecRequest, out *ExecResponse) error {
|
||||
return h.CommandHandler.Exec(ctx, in, out)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package go.micro.bot;
|
||||
|
||||
service Command {
|
||||
rpc Help(HelpRequest) returns (HelpResponse) {};
|
||||
rpc Exec(ExecRequest) returns (ExecResponse) {};
|
||||
}
|
||||
|
||||
message HelpRequest {
|
||||
}
|
||||
|
||||
message HelpResponse {
|
||||
string usage = 1;
|
||||
string description = 2;
|
||||
}
|
||||
|
||||
message ExecRequest {
|
||||
repeated string args = 1;
|
||||
}
|
||||
|
||||
message ExecResponse {
|
||||
bytes result = 1;
|
||||
string error = 2;
|
||||
}
|
||||
-171
@@ -1,171 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
"github.com/micro/go-micro/v2/server"
|
||||
)
|
||||
|
||||
type Api interface {
|
||||
// Register a http handler
|
||||
Register(*Endpoint) error
|
||||
// Register a route
|
||||
Deregister(*Endpoint) error
|
||||
// Init initialises the command line.
|
||||
// It also parses further options.
|
||||
//Init(...Option) error
|
||||
// Options
|
||||
//Options() Options
|
||||
// String
|
||||
String() string
|
||||
}
|
||||
|
||||
// Endpoint is a mapping between an RPC method and HTTP endpoint
|
||||
type Endpoint struct {
|
||||
// RPC Method e.g. Greeter.Hello
|
||||
Name string
|
||||
// Description e.g what's this endpoint for
|
||||
Description string
|
||||
// API Handler e.g rpc, proxy
|
||||
Handler string
|
||||
// HTTP Host e.g example.com
|
||||
Host []string
|
||||
// HTTP Methods e.g GET, POST
|
||||
Method []string
|
||||
// HTTP Path e.g /greeter. Expect POSIX regex
|
||||
Path []string
|
||||
// Stream flag
|
||||
Stream bool
|
||||
}
|
||||
|
||||
// Service represents an API service
|
||||
type Service struct {
|
||||
// Name of service
|
||||
Name string
|
||||
// The endpoint for this service
|
||||
Endpoint *Endpoint
|
||||
// Versions of this service
|
||||
Services []*registry.Service
|
||||
}
|
||||
|
||||
func strip(s string) string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func slice(s string) []string {
|
||||
var sl []string
|
||||
|
||||
for _, p := range strings.Split(s, ",") {
|
||||
if str := strip(p); len(str) > 0 {
|
||||
sl = append(sl, strip(p))
|
||||
}
|
||||
}
|
||||
|
||||
return sl
|
||||
}
|
||||
|
||||
// Encode encodes an endpoint to endpoint metadata
|
||||
func Encode(e *Endpoint) map[string]string {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// endpoint map
|
||||
ep := make(map[string]string)
|
||||
|
||||
// set vals only if they exist
|
||||
set := func(k, v string) {
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
ep[k] = v
|
||||
}
|
||||
|
||||
set("endpoint", e.Name)
|
||||
set("description", e.Description)
|
||||
set("handler", e.Handler)
|
||||
set("method", strings.Join(e.Method, ","))
|
||||
set("path", strings.Join(e.Path, ","))
|
||||
set("host", strings.Join(e.Host, ","))
|
||||
|
||||
return ep
|
||||
}
|
||||
|
||||
// Decode decodes endpoint metadata into an endpoint
|
||||
func Decode(e map[string]string) *Endpoint {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Endpoint{
|
||||
Name: e["endpoint"],
|
||||
Description: e["description"],
|
||||
Method: slice(e["method"]),
|
||||
Path: slice(e["path"]),
|
||||
Host: slice(e["host"]),
|
||||
Handler: e["handler"],
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates an endpoint to guarantee it won't blow up when being served
|
||||
func Validate(e *Endpoint) error {
|
||||
if e == nil {
|
||||
return errors.New("endpoint is nil")
|
||||
}
|
||||
|
||||
if len(e.Name) == 0 {
|
||||
return errors.New("name required")
|
||||
}
|
||||
|
||||
for _, p := range e.Path {
|
||||
_, err := regexp.CompilePOSIX(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(e.Handler) == 0 {
|
||||
return errors.New("invalid handler")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Design ideas
|
||||
|
||||
// Gateway is an api gateway interface
|
||||
type Gateway interface {
|
||||
// Register a http handler
|
||||
Handle(pattern string, http.Handler)
|
||||
// Register a route
|
||||
RegisterRoute(r Route)
|
||||
// Init initialises the command line.
|
||||
// It also parses further options.
|
||||
Init(...Option) error
|
||||
// Run the gateway
|
||||
Run() error
|
||||
}
|
||||
|
||||
// NewGateway returns a new api gateway
|
||||
func NewGateway() Gateway {
|
||||
return newGateway()
|
||||
}
|
||||
*/
|
||||
|
||||
// WithEndpoint returns a server.HandlerOption with endpoint metadata set
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// proto.RegisterHandler(service.Server(), new(Handler), api.WithEndpoint(
|
||||
// &api.Endpoint{
|
||||
// Name: "Greeter.Hello",
|
||||
// Path: []string{"/greeter"},
|
||||
// },
|
||||
// ))
|
||||
func WithEndpoint(e *Endpoint) server.HandlerOption {
|
||||
return server.EndpointMetadata(e.Name, Encode(e))
|
||||
}
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncoding(t *testing.T) {
|
||||
testData := []*Endpoint{
|
||||
nil,
|
||||
{
|
||||
Name: "Foo.Bar",
|
||||
Description: "A test endpoint",
|
||||
Handler: "meta",
|
||||
Host: []string{"foo.com"},
|
||||
Method: []string{"GET"},
|
||||
Path: []string{"/test"},
|
||||
},
|
||||
}
|
||||
|
||||
compare := func(expect, got []string) bool {
|
||||
// no data to compare, return true
|
||||
if len(expect) == 0 && len(got) == 0 {
|
||||
return true
|
||||
}
|
||||
// no data expected but got some return false
|
||||
if len(expect) == 0 && len(got) > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// compare expected with what we got
|
||||
for _, e := range expect {
|
||||
var seen bool
|
||||
for _, g := range got {
|
||||
if e == g {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// we're done, return true
|
||||
return true
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
// encode
|
||||
e := Encode(d)
|
||||
// decode
|
||||
de := Decode(e)
|
||||
|
||||
// nil endpoint returns nil
|
||||
if d == nil {
|
||||
if e != nil {
|
||||
t.Fatalf("expected nil got %v", e)
|
||||
}
|
||||
if de != nil {
|
||||
t.Fatalf("expected nil got %v", de)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// check encoded map
|
||||
name := e["endpoint"]
|
||||
desc := e["description"]
|
||||
method := strings.Split(e["method"], ",")
|
||||
path := strings.Split(e["path"], ",")
|
||||
host := strings.Split(e["host"], ",")
|
||||
handler := e["handler"]
|
||||
|
||||
if name != d.Name {
|
||||
t.Fatalf("expected %v got %v", d.Name, name)
|
||||
}
|
||||
if desc != d.Description {
|
||||
t.Fatalf("expected %v got %v", d.Description, desc)
|
||||
}
|
||||
if handler != d.Handler {
|
||||
t.Fatalf("expected %v got %v", d.Handler, handler)
|
||||
}
|
||||
if ok := compare(d.Method, method); !ok {
|
||||
t.Fatalf("expected %v got %v", d.Method, method)
|
||||
}
|
||||
if ok := compare(d.Path, path); !ok {
|
||||
t.Fatalf("expected %v got %v", d.Path, path)
|
||||
}
|
||||
if ok := compare(d.Host, host); !ok {
|
||||
t.Fatalf("expected %v got %v", d.Host, host)
|
||||
}
|
||||
|
||||
if de.Name != d.Name {
|
||||
t.Fatalf("expected %v got %v", d.Name, de.Name)
|
||||
}
|
||||
if de.Description != d.Description {
|
||||
t.Fatalf("expected %v got %v", d.Description, de.Description)
|
||||
}
|
||||
if de.Handler != d.Handler {
|
||||
t.Fatalf("expected %v got %v", d.Handler, de.Handler)
|
||||
}
|
||||
if ok := compare(d.Method, de.Method); !ok {
|
||||
t.Fatalf("expected %v got %v", d.Method, de.Method)
|
||||
}
|
||||
if ok := compare(d.Path, de.Path); !ok {
|
||||
t.Fatalf("expected %v got %v", d.Path, de.Path)
|
||||
}
|
||||
if ok := compare(d.Host, de.Host); !ok {
|
||||
t.Fatalf("expected %v got %v", d.Host, de.Host)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/micro/go-micro/v2"
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
ahandler "github.com/micro/go-micro/v2/api/handler"
|
||||
apirpc "github.com/micro/go-micro/v2/api/handler/rpc"
|
||||
"github.com/micro/go-micro/v2/api/router"
|
||||
rstatic "github.com/micro/go-micro/v2/api/router/static"
|
||||
bmemory "github.com/micro/go-micro/v2/broker/memory"
|
||||
"github.com/micro/go-micro/v2/client"
|
||||
gcli "github.com/micro/go-micro/v2/client/grpc"
|
||||
rmemory "github.com/micro/go-micro/v2/registry/memory"
|
||||
"github.com/micro/go-micro/v2/server"
|
||||
gsrv "github.com/micro/go-micro/v2/server/grpc"
|
||||
tgrpc "github.com/micro/go-micro/v2/transport/grpc"
|
||||
|
||||
pb "github.com/micro/go-micro/v2/server/grpc/proto"
|
||||
)
|
||||
|
||||
// server is used to implement helloworld.GreeterServer.
|
||||
type testServer struct {
|
||||
msgCount int
|
||||
}
|
||||
|
||||
// TestHello implements helloworld.GreeterServer
|
||||
func (s *testServer) Call(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
|
||||
rsp.Msg = "Hello " + req.Name
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestApiAndGRPC(t *testing.T) {
|
||||
r := rmemory.NewRegistry()
|
||||
b := bmemory.NewBroker()
|
||||
tr := tgrpc.NewTransport()
|
||||
s := gsrv.NewServer(
|
||||
server.Broker(b),
|
||||
server.Name("foo"),
|
||||
server.Registry(r),
|
||||
server.Transport(tr),
|
||||
)
|
||||
c := gcli.NewClient(
|
||||
client.Registry(r),
|
||||
client.Broker(b),
|
||||
client.Transport(tr),
|
||||
)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
svc := micro.NewService(
|
||||
micro.Server(s),
|
||||
micro.Client(c),
|
||||
micro.Broker(b),
|
||||
micro.Registry(r),
|
||||
micro.Transport(tr),
|
||||
micro.Context(ctx))
|
||||
h := &testServer{}
|
||||
pb.RegisterTestHandler(s, h)
|
||||
|
||||
go func() {
|
||||
if err := svc.Run(); err != nil {
|
||||
t.Fatalf("failed to start: %v", err)
|
||||
}
|
||||
}()
|
||||
time.Sleep(1 * time.Second)
|
||||
// check registration
|
||||
services, err := r.GetService("foo")
|
||||
if err != nil || len(services) == 0 {
|
||||
t.Fatalf("failed to get service: %v # %d", err, len(services))
|
||||
}
|
||||
|
||||
router := rstatic.NewRouter(
|
||||
router.WithHandler(apirpc.Handler),
|
||||
router.WithRegistry(svc.Server().Options().Registry),
|
||||
)
|
||||
err = router.Register(&api.Endpoint{
|
||||
Name: "foo.Test.Call",
|
||||
Method: []string{"GET"},
|
||||
Path: []string{"/api/v0/test/call/{name}"},
|
||||
Handler: "rpc",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hrpc := apirpc.NewHandler(
|
||||
ahandler.WithService(svc),
|
||||
ahandler.WithRouter(router),
|
||||
)
|
||||
|
||||
hsrv := &http.Server{
|
||||
Handler: hrpc,
|
||||
Addr: "127.0.0.1:6543",
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
IdleTimeout: 20 * time.Second,
|
||||
MaxHeaderBytes: 1024 * 1024 * 1, // 1Mb
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Println(hsrv.ListenAndServe())
|
||||
}()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
rsp, err := http.Get(fmt.Sprintf("http://%s/api/v0/test/call/TEST", hsrv.Addr))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to created http.Request: %v", err)
|
||||
}
|
||||
defer rsp.Body.Close()
|
||||
buf, err := ioutil.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
jsonMsg := `{"msg":"Hello TEST"}`
|
||||
if string(buf) != jsonMsg {
|
||||
t.Fatalf("invalid message received, parsing error %s != %s", buf, jsonMsg)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
// Package api provides an http-rpc handler which provides the entire http request over rpc
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
goapi "github.com/micro/go-micro/v2/api"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
api "github.com/micro/go-micro/v2/api/proto"
|
||||
"github.com/micro/go-micro/v2/client"
|
||||
"github.com/micro/go-micro/v2/client/selector"
|
||||
"github.com/micro/go-micro/v2/errors"
|
||||
"github.com/micro/go-micro/v2/util/ctx"
|
||||
)
|
||||
|
||||
type apiHandler struct {
|
||||
opts handler.Options
|
||||
s *goapi.Service
|
||||
}
|
||||
|
||||
const (
|
||||
Handler = "api"
|
||||
)
|
||||
|
||||
// API handler is the default handler which takes api.Request and returns api.Response
|
||||
func (a *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
bsize := handler.DefaultMaxRecvSize
|
||||
if a.opts.MaxRecvSize > 0 {
|
||||
bsize = a.opts.MaxRecvSize
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, bsize)
|
||||
request, err := requestToProto(r)
|
||||
if err != nil {
|
||||
er := errors.InternalServerError("go.micro.api", err.Error())
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(er.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var service *goapi.Service
|
||||
|
||||
if a.s != nil {
|
||||
// we were given the service
|
||||
service = a.s
|
||||
} else if a.opts.Router != nil {
|
||||
// try get service from router
|
||||
s, err := a.opts.Router.Route(r)
|
||||
if err != nil {
|
||||
er := errors.InternalServerError("go.micro.api", err.Error())
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(er.Error()))
|
||||
return
|
||||
}
|
||||
service = s
|
||||
} else {
|
||||
// we have no way of routing the request
|
||||
er := errors.InternalServerError("go.micro.api", "no route found")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(er.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// create request and response
|
||||
c := a.opts.Service.Client()
|
||||
req := c.NewRequest(service.Name, service.Endpoint.Name, request)
|
||||
rsp := &api.Response{}
|
||||
|
||||
// create the context from headers
|
||||
cx := ctx.FromRequest(r)
|
||||
// create strategy
|
||||
so := selector.WithStrategy(strategy(service.Services))
|
||||
|
||||
if err := c.Call(cx, req, rsp, client.WithSelectOption(so)); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
ce := errors.Parse(err.Error())
|
||||
switch ce.Code {
|
||||
case 0:
|
||||
w.WriteHeader(500)
|
||||
default:
|
||||
w.WriteHeader(int(ce.Code))
|
||||
}
|
||||
w.Write([]byte(ce.Error()))
|
||||
return
|
||||
} else if rsp.StatusCode == 0 {
|
||||
rsp.StatusCode = http.StatusOK
|
||||
}
|
||||
|
||||
for _, header := range rsp.GetHeader() {
|
||||
for _, val := range header.Values {
|
||||
w.Header().Add(header.Key, val)
|
||||
}
|
||||
}
|
||||
|
||||
if len(w.Header().Get("Content-Type")) == 0 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
w.WriteHeader(int(rsp.StatusCode))
|
||||
w.Write([]byte(rsp.Body))
|
||||
}
|
||||
|
||||
func (a *apiHandler) String() string {
|
||||
return "api"
|
||||
}
|
||||
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
return &apiHandler{
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func WithService(s *goapi.Service, opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
return &apiHandler{
|
||||
opts: options,
|
||||
s: s,
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
api "github.com/micro/go-micro/v2/api/proto"
|
||||
"github.com/micro/go-micro/v2/client/selector"
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
"github.com/oxtoacart/bpool"
|
||||
)
|
||||
|
||||
var (
|
||||
// need to calculate later to specify useful defaults
|
||||
bufferPool = bpool.NewSizedBufferPool(1024, 8)
|
||||
)
|
||||
|
||||
func requestToProto(r *http.Request) (*api.Request, error) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return nil, fmt.Errorf("Error parsing form: %v", err)
|
||||
}
|
||||
|
||||
req := &api.Request{
|
||||
Path: r.URL.Path,
|
||||
Method: r.Method,
|
||||
Header: make(map[string]*api.Pair),
|
||||
Get: make(map[string]*api.Pair),
|
||||
Post: make(map[string]*api.Pair),
|
||||
Url: r.URL.String(),
|
||||
}
|
||||
|
||||
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
ct = "text/plain; charset=UTF-8" //default CT is text/plain
|
||||
r.Header.Set("Content-Type", ct)
|
||||
}
|
||||
|
||||
//set the body:
|
||||
if r.Body != nil {
|
||||
switch ct {
|
||||
case "application/x-www-form-urlencoded":
|
||||
// expect form vals in Post data
|
||||
default:
|
||||
buf := bufferPool.Get()
|
||||
defer bufferPool.Put(buf)
|
||||
if _, err = buf.ReadFrom(r.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Body = buf.String()
|
||||
}
|
||||
}
|
||||
|
||||
// Set X-Forwarded-For if it does not exist
|
||||
if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
if prior, ok := r.Header["X-Forwarded-For"]; ok {
|
||||
ip = strings.Join(prior, ", ") + ", " + ip
|
||||
}
|
||||
|
||||
// Set the header
|
||||
req.Header["X-Forwarded-For"] = &api.Pair{
|
||||
Key: "X-Forwarded-For",
|
||||
Values: []string{ip},
|
||||
}
|
||||
}
|
||||
|
||||
// Host is stripped from net/http Headers so let's add it
|
||||
req.Header["Host"] = &api.Pair{
|
||||
Key: "Host",
|
||||
Values: []string{r.Host},
|
||||
}
|
||||
|
||||
// Get data
|
||||
for key, vals := range r.URL.Query() {
|
||||
header, ok := req.Get[key]
|
||||
if !ok {
|
||||
header = &api.Pair{
|
||||
Key: key,
|
||||
}
|
||||
req.Get[key] = header
|
||||
}
|
||||
header.Values = vals
|
||||
}
|
||||
|
||||
// Post data
|
||||
for key, vals := range r.PostForm {
|
||||
header, ok := req.Post[key]
|
||||
if !ok {
|
||||
header = &api.Pair{
|
||||
Key: key,
|
||||
}
|
||||
req.Post[key] = header
|
||||
}
|
||||
header.Values = vals
|
||||
}
|
||||
|
||||
for key, vals := range r.Header {
|
||||
header, ok := req.Header[key]
|
||||
if !ok {
|
||||
header = &api.Pair{
|
||||
Key: key,
|
||||
}
|
||||
req.Header[key] = header
|
||||
}
|
||||
header.Values = vals
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// strategy is a hack for selection
|
||||
func strategy(services []*registry.Service) selector.Strategy {
|
||||
return func(_ []*registry.Service) selector.Next {
|
||||
// ignore input to this function, use services above
|
||||
return selector.Random(services)
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRequestToProto(t *testing.T) {
|
||||
testData := []*http.Request{
|
||||
{
|
||||
Method: "GET",
|
||||
Header: http.Header{
|
||||
"Header": []string{"test"},
|
||||
},
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "localhost",
|
||||
Path: "/foo/bar",
|
||||
RawQuery: "param1=value1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
p, err := requestToProto(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Path != d.URL.Path {
|
||||
t.Fatalf("Expected path %s got %s", d.URL.Path, p.Path)
|
||||
}
|
||||
if p.Method != d.Method {
|
||||
t.Fatalf("Expected method %s got %s", d.Method, p.Method)
|
||||
}
|
||||
for k, v := range d.Header {
|
||||
if val, ok := p.Header[k]; !ok {
|
||||
t.Fatalf("Expected header %s", k)
|
||||
} else {
|
||||
if val.Values[0] != v[0] {
|
||||
t.Fatalf("Expected val %s, got %s", val.Values[0], v[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
// Package broker provides a go-micro/broker handler
|
||||
package broker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
"github.com/micro/go-micro/v2/broker"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
"github.com/oxtoacart/bpool"
|
||||
)
|
||||
|
||||
var (
|
||||
bufferPool = bpool.NewSizedBufferPool(1024, 8)
|
||||
)
|
||||
|
||||
const (
|
||||
Handler = "broker"
|
||||
|
||||
pingTime = (readDeadline * 9) / 10
|
||||
readLimit = 16384
|
||||
readDeadline = 60 * time.Second
|
||||
writeDeadline = 10 * time.Second
|
||||
)
|
||||
|
||||
type brokerHandler struct {
|
||||
once atomic.Value
|
||||
opts handler.Options
|
||||
u websocket.Upgrader
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
b broker.Broker
|
||||
cType string
|
||||
topic string
|
||||
queue string
|
||||
exit chan bool
|
||||
|
||||
sync.Mutex
|
||||
ws *websocket.Conn
|
||||
}
|
||||
|
||||
var (
|
||||
contentType = "text/plain"
|
||||
)
|
||||
|
||||
func checkOrigin(r *http.Request) bool {
|
||||
origin := r.Header["Origin"]
|
||||
if len(origin) == 0 {
|
||||
return true
|
||||
}
|
||||
u, err := url.Parse(origin[0])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return u.Host == r.Host
|
||||
}
|
||||
|
||||
func (c *conn) close() {
|
||||
select {
|
||||
case <-c.exit:
|
||||
return
|
||||
default:
|
||||
close(c.exit)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) readLoop() {
|
||||
defer func() {
|
||||
c.close()
|
||||
c.ws.Close()
|
||||
}()
|
||||
|
||||
// set read limit/deadline
|
||||
c.ws.SetReadLimit(readLimit)
|
||||
c.ws.SetReadDeadline(time.Now().Add(readDeadline))
|
||||
|
||||
// set close handler
|
||||
ch := c.ws.CloseHandler()
|
||||
c.ws.SetCloseHandler(func(code int, text string) error {
|
||||
err := ch(code, text)
|
||||
c.close()
|
||||
return err
|
||||
})
|
||||
|
||||
// set pong handler
|
||||
c.ws.SetPongHandler(func(string) error {
|
||||
c.ws.SetReadDeadline(time.Now().Add(readDeadline))
|
||||
return nil
|
||||
})
|
||||
|
||||
for {
|
||||
_, message, err := c.ws.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.b.Publish(c.topic, &broker.Message{
|
||||
Header: map[string]string{"Content-Type": c.cType},
|
||||
Body: message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) write(mType int, data []byte) error {
|
||||
c.Lock()
|
||||
c.ws.SetWriteDeadline(time.Now().Add(writeDeadline))
|
||||
err := c.ws.WriteMessage(mType, data)
|
||||
c.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *conn) writeLoop() {
|
||||
ticker := time.NewTicker(pingTime)
|
||||
|
||||
var opts []broker.SubscribeOption
|
||||
|
||||
if len(c.queue) > 0 {
|
||||
opts = append(opts, broker.Queue(c.queue))
|
||||
}
|
||||
|
||||
subscriber, err := c.b.Subscribe(c.topic, func(p broker.Event) error {
|
||||
b, err := json.Marshal(p.Message())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return c.write(websocket.TextMessage, b)
|
||||
}, opts...)
|
||||
|
||||
defer func() {
|
||||
subscriber.Unsubscribe()
|
||||
ticker.Stop()
|
||||
c.ws.Close()
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
case <-c.exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *brokerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
bsize := handler.DefaultMaxRecvSize
|
||||
if b.opts.MaxRecvSize > 0 {
|
||||
bsize = b.opts.MaxRecvSize
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, bsize)
|
||||
|
||||
br := b.opts.Service.Client().Options().Broker
|
||||
|
||||
// Setup the broker
|
||||
if !b.once.Load().(bool) {
|
||||
if err := br.Init(); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
if err := br.Connect(); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
b.once.Store(true)
|
||||
}
|
||||
|
||||
// Parse
|
||||
r.ParseForm()
|
||||
topic := r.Form.Get("topic")
|
||||
|
||||
// Can't do anything without a topic
|
||||
if len(topic) == 0 {
|
||||
http.Error(w, "Topic not specified", 400)
|
||||
return
|
||||
}
|
||||
|
||||
// Post assumed to be Publish
|
||||
if r.Method == "POST" {
|
||||
// Create a broker message
|
||||
msg := &broker.Message{
|
||||
Header: make(map[string]string),
|
||||
}
|
||||
|
||||
// Set header
|
||||
for k, v := range r.Header {
|
||||
msg.Header[k] = strings.Join(v, ", ")
|
||||
}
|
||||
|
||||
// Read body
|
||||
buf := bufferPool.Get()
|
||||
defer bufferPool.Put(buf)
|
||||
if _, err := buf.ReadFrom(r.Body); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
// Set body
|
||||
msg.Body = buf.Bytes()
|
||||
// Set body
|
||||
|
||||
// Publish
|
||||
br.Publish(topic, msg)
|
||||
return
|
||||
}
|
||||
|
||||
// now back to our regularly scheduled programming
|
||||
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
|
||||
queue := r.Form.Get("queue")
|
||||
|
||||
ws, err := b.u.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cType := r.Header.Get("Content-Type")
|
||||
if len(cType) == 0 {
|
||||
cType = contentType
|
||||
}
|
||||
|
||||
c := &conn{
|
||||
b: br,
|
||||
cType: cType,
|
||||
topic: topic,
|
||||
queue: queue,
|
||||
exit: make(chan bool),
|
||||
ws: ws,
|
||||
}
|
||||
|
||||
go c.writeLoop()
|
||||
c.readLoop()
|
||||
}
|
||||
|
||||
func (b *brokerHandler) String() string {
|
||||
return "broker"
|
||||
}
|
||||
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
h := &brokerHandler{
|
||||
u: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
},
|
||||
opts: handler.NewOptions(opts...),
|
||||
}
|
||||
h.once.Store(true)
|
||||
return h
|
||||
}
|
||||
|
||||
func WithCors(cors map[string]bool, opts ...handler.Option) handler.Handler {
|
||||
return &brokerHandler{
|
||||
u: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
if origin := r.Header.Get("Origin"); cors[origin] {
|
||||
return true
|
||||
} else if len(origin) > 0 && cors["*"] {
|
||||
return true
|
||||
} else if checkOrigin(r) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
},
|
||||
opts: handler.NewOptions(opts...),
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
// Package cloudevents provides a cloudevents handler publishing the event using the go-micro/client
|
||||
package cloudevents
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
"github.com/micro/go-micro/v2/util/ctx"
|
||||
)
|
||||
|
||||
type event struct {
|
||||
opts handler.Options
|
||||
}
|
||||
|
||||
var (
|
||||
Handler = "cloudevents"
|
||||
versionRe = regexp.MustCompilePOSIX("^v[0-9]+$")
|
||||
)
|
||||
|
||||
func eventName(parts []string) string {
|
||||
return strings.Join(parts, ".")
|
||||
}
|
||||
|
||||
func evRoute(ns, p string) (string, string) {
|
||||
p = path.Clean(p)
|
||||
p = strings.TrimPrefix(p, "/")
|
||||
|
||||
if len(p) == 0 {
|
||||
return ns, "event"
|
||||
}
|
||||
|
||||
parts := strings.Split(p, "/")
|
||||
|
||||
// no path
|
||||
if len(parts) == 0 {
|
||||
// topic: namespace
|
||||
// action: event
|
||||
return strings.Trim(ns, "."), "event"
|
||||
}
|
||||
|
||||
// Treat /v[0-9]+ as versioning
|
||||
// /v1/foo/bar => topic: v1.foo action: bar
|
||||
if len(parts) >= 2 && versionRe.Match([]byte(parts[0])) {
|
||||
topic := ns + "." + strings.Join(parts[:2], ".")
|
||||
action := eventName(parts[1:])
|
||||
return topic, action
|
||||
}
|
||||
|
||||
// /foo => topic: ns.foo action: foo
|
||||
// /foo/bar => topic: ns.foo action: bar
|
||||
topic := ns + "." + strings.Join(parts[:1], ".")
|
||||
action := eventName(parts[1:])
|
||||
|
||||
return topic, action
|
||||
}
|
||||
|
||||
func (e *event) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
bsize := handler.DefaultMaxRecvSize
|
||||
if e.opts.MaxRecvSize > 0 {
|
||||
bsize = e.opts.MaxRecvSize
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, bsize)
|
||||
|
||||
// request to topic:event
|
||||
// create event
|
||||
// publish to topic
|
||||
topic, _ := evRoute(e.opts.Namespace, r.URL.Path)
|
||||
|
||||
// create event
|
||||
ev, err := FromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// get client
|
||||
c := e.opts.Service.Client()
|
||||
|
||||
// create publication
|
||||
p := c.NewMessage(topic, ev)
|
||||
|
||||
// publish event
|
||||
if err := c.Publish(ctx.FromRequest(r), p); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (e *event) String() string {
|
||||
return "cloudevents"
|
||||
}
|
||||
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
return &event{
|
||||
opts: handler.NewOptions(opts...),
|
||||
}
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
/*
|
||||
* From: https://github.com/serverless/event-gateway/blob/master/event/event.go
|
||||
* Modified: Strip to handler requirements
|
||||
*
|
||||
* Copyright 2017 Serverless, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package cloudevents
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/oxtoacart/bpool"
|
||||
validator "gopkg.in/go-playground/validator.v9"
|
||||
)
|
||||
|
||||
var (
|
||||
bufferPool = bpool.NewSizedBufferPool(1024, 8)
|
||||
)
|
||||
|
||||
const (
|
||||
// TransformationVersion is indicative of the revision of how Event Gateway transforms a request into CloudEvents format.
|
||||
TransformationVersion = "0.1"
|
||||
|
||||
// CloudEventsVersion currently supported by Event Gateway
|
||||
CloudEventsVersion = "0.1"
|
||||
)
|
||||
|
||||
// Event is a default event structure. All data that passes through the Event Gateway
|
||||
// is formatted to a format defined CloudEvents v0.1 spec.
|
||||
type Event struct {
|
||||
EventType string `json:"eventType" validate:"required"`
|
||||
EventTypeVersion string `json:"eventTypeVersion,omitempty"`
|
||||
CloudEventsVersion string `json:"cloudEventsVersion" validate:"required"`
|
||||
Source string `json:"source" validate:"uri,required"`
|
||||
EventID string `json:"eventID" validate:"required"`
|
||||
EventTime *time.Time `json:"eventTime,omitempty"`
|
||||
SchemaURL string `json:"schemaURL,omitempty"`
|
||||
Extensions map[string]interface{} `json:"extensions,omitempty"`
|
||||
ContentType string `json:"contentType,omitempty"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// New return new instance of Event.
|
||||
func New(eventType string, mimeType string, payload interface{}) *Event {
|
||||
now := time.Now()
|
||||
|
||||
event := &Event{
|
||||
EventType: eventType,
|
||||
CloudEventsVersion: CloudEventsVersion,
|
||||
Source: "https://micro.mu",
|
||||
EventID: uuid.New().String(),
|
||||
EventTime: &now,
|
||||
ContentType: mimeType,
|
||||
Data: payload,
|
||||
Extensions: map[string]interface{}{
|
||||
"eventgateway": map[string]interface{}{
|
||||
"transformed": "true",
|
||||
"transformation-version": TransformationVersion,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
event.Data = normalizePayload(event.Data, event.ContentType)
|
||||
return event
|
||||
}
|
||||
|
||||
// FromRequest takes an HTTP request and returns an Event along with path. Most of the implementation
|
||||
// is based on https://github.com/cloudevents/spec/blob/master/http-transport-binding.md.
|
||||
// This function also supports legacy mode where event type is sent in Event header.
|
||||
func FromRequest(r *http.Request) (*Event, error) {
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
mimeType, _, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
if err.Error() != "mime: no media type" {
|
||||
return nil, err
|
||||
}
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
// Read request body
|
||||
body := []byte{}
|
||||
if r.Body != nil {
|
||||
buf := bufferPool.Get()
|
||||
defer bufferPool.Put(buf)
|
||||
if _, err := buf.ReadFrom(r.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = buf.Bytes()
|
||||
}
|
||||
|
||||
var event *Event
|
||||
if mimeType == mimeCloudEventsJSON { // CloudEvents Structured Content Mode
|
||||
return parseAsCloudEvent(mimeType, body)
|
||||
} else if isCloudEventsBinaryContentMode(r.Header) { // CloudEvents Binary Content Mode
|
||||
return parseAsCloudEventBinary(r.Header, body)
|
||||
} else if isLegacyMode(r.Header) {
|
||||
if mimeType == mimeJSON { // CloudEvent in Legacy Mode
|
||||
event, err = parseAsCloudEvent(mimeType, body)
|
||||
if err != nil {
|
||||
return New(string(r.Header.Get("event")), mimeType, body), nil
|
||||
}
|
||||
return event, err
|
||||
}
|
||||
|
||||
return New(string(r.Header.Get("event")), mimeType, body), nil
|
||||
}
|
||||
|
||||
return New("http.request", mimeJSON, newHTTPRequestData(r, body)), nil
|
||||
}
|
||||
|
||||
// Validate Event struct
|
||||
func (e *Event) Validate() error {
|
||||
validate := validator.New()
|
||||
err := validate.Struct(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CloudEvent not valid: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isLegacyMode(headers http.Header) bool {
|
||||
if headers.Get("Event") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isCloudEventsBinaryContentMode(headers http.Header) bool {
|
||||
if headers.Get("CE-EventType") != "" &&
|
||||
headers.Get("CE-CloudEventsVersion") != "" &&
|
||||
headers.Get("CE-Source") != "" &&
|
||||
headers.Get("CE-EventID") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func parseAsCloudEventBinary(headers http.Header, payload interface{}) (*Event, error) {
|
||||
event := &Event{
|
||||
EventType: headers.Get("CE-EventType"),
|
||||
EventTypeVersion: headers.Get("CE-EventTypeVersion"),
|
||||
CloudEventsVersion: headers.Get("CE-CloudEventsVersion"),
|
||||
Source: headers.Get("CE-Source"),
|
||||
EventID: headers.Get("CE-EventID"),
|
||||
ContentType: headers.Get("Content-Type"),
|
||||
Data: payload,
|
||||
}
|
||||
|
||||
err := event.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if headers.Get("CE-EventTime") != "" {
|
||||
val, err := time.Parse(time.RFC3339, headers.Get("CE-EventTime"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
event.EventTime = &val
|
||||
}
|
||||
|
||||
if val := headers.Get("CE-SchemaURL"); len(val) > 0 {
|
||||
event.SchemaURL = val
|
||||
}
|
||||
|
||||
event.Extensions = map[string]interface{}{}
|
||||
for key, val := range flatten(headers) {
|
||||
if strings.HasPrefix(key, "Ce-X-") {
|
||||
key = strings.TrimLeft(key, "Ce-X-")
|
||||
// Make first character lowercase
|
||||
runes := []rune(key)
|
||||
runes[0] = unicode.ToLower(runes[0])
|
||||
event.Extensions[string(runes)] = val
|
||||
}
|
||||
}
|
||||
|
||||
event.Data = normalizePayload(event.Data, event.ContentType)
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func flatten(h http.Header) map[string]string {
|
||||
headers := map[string]string{}
|
||||
for key, header := range h {
|
||||
headers[key] = header[0]
|
||||
if len(header) > 1 {
|
||||
headers[key] = strings.Join(header, ", ")
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func parseAsCloudEvent(mime string, payload interface{}) (*Event, error) {
|
||||
body, ok := payload.([]byte)
|
||||
if ok {
|
||||
event := &Event{}
|
||||
err := json.Unmarshal(body, event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = event.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event.Data = normalizePayload(event.Data, event.ContentType)
|
||||
return event, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("couldn't cast to []byte")
|
||||
}
|
||||
|
||||
const (
|
||||
mimeJSON = "application/json"
|
||||
mimeFormMultipart = "multipart/form-data"
|
||||
mimeFormURLEncoded = "application/x-www-form-urlencoded"
|
||||
mimeCloudEventsJSON = "application/cloudevents+json"
|
||||
)
|
||||
|
||||
// normalizePayload takes anything, checks if it's []byte array and depending on provided mime
|
||||
// type converts it to either string or map[string]interface to avoid having base64 string after
|
||||
// JSON marshaling.
|
||||
func normalizePayload(payload interface{}, mime string) interface{} {
|
||||
if bytePayload, ok := payload.([]byte); ok && len(bytePayload) > 0 {
|
||||
switch {
|
||||
case mime == mimeJSON || strings.HasSuffix(mime, "+json"):
|
||||
var result map[string]interface{}
|
||||
err := json.Unmarshal(bytePayload, &result)
|
||||
if err != nil {
|
||||
return payload
|
||||
}
|
||||
return result
|
||||
case strings.HasPrefix(mime, mimeFormMultipart), mime == mimeFormURLEncoded:
|
||||
return string(bytePayload)
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
// HTTPRequestData is a event schema used for sending events to HTTP subscriptions.
|
||||
type HTTPRequestData struct {
|
||||
Headers map[string]string `json:"headers"`
|
||||
Query map[string][]string `json:"query"`
|
||||
Body interface{} `json:"body"`
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
Params map[string]string `json:"params"`
|
||||
}
|
||||
|
||||
// NewHTTPRequestData returns a new instance of HTTPRequestData
|
||||
func newHTTPRequestData(r *http.Request, eventData interface{}) *HTTPRequestData {
|
||||
req := &HTTPRequestData{
|
||||
Headers: flatten(r.Header),
|
||||
Query: r.URL.Query(),
|
||||
Body: eventData,
|
||||
Host: r.Host,
|
||||
Path: r.URL.Path,
|
||||
Method: r.Method,
|
||||
}
|
||||
|
||||
req.Body = normalizePayload(req.Body, r.Header.Get("content-type"))
|
||||
return req
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
// Package event provides a handler which publishes an event
|
||||
package event
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
proto "github.com/micro/go-micro/v2/api/proto"
|
||||
"github.com/micro/go-micro/v2/util/ctx"
|
||||
"github.com/oxtoacart/bpool"
|
||||
)
|
||||
|
||||
var (
|
||||
bufferPool = bpool.NewSizedBufferPool(1024, 8)
|
||||
)
|
||||
|
||||
type event struct {
|
||||
opts handler.Options
|
||||
}
|
||||
|
||||
var (
|
||||
Handler = "event"
|
||||
versionRe = regexp.MustCompilePOSIX("^v[0-9]+$")
|
||||
)
|
||||
|
||||
func eventName(parts []string) string {
|
||||
return strings.Join(parts, ".")
|
||||
}
|
||||
|
||||
func evRoute(ns, p string) (string, string) {
|
||||
p = path.Clean(p)
|
||||
p = strings.TrimPrefix(p, "/")
|
||||
|
||||
if len(p) == 0 {
|
||||
return ns, "event"
|
||||
}
|
||||
|
||||
parts := strings.Split(p, "/")
|
||||
|
||||
// no path
|
||||
if len(parts) == 0 {
|
||||
// topic: namespace
|
||||
// action: event
|
||||
return strings.Trim(ns, "."), "event"
|
||||
}
|
||||
|
||||
// Treat /v[0-9]+ as versioning
|
||||
// /v1/foo/bar => topic: v1.foo action: bar
|
||||
if len(parts) >= 2 && versionRe.Match([]byte(parts[0])) {
|
||||
topic := ns + "." + strings.Join(parts[:2], ".")
|
||||
action := eventName(parts[1:])
|
||||
return topic, action
|
||||
}
|
||||
|
||||
// /foo => topic: ns.foo action: foo
|
||||
// /foo/bar => topic: ns.foo action: bar
|
||||
topic := ns + "." + strings.Join(parts[:1], ".")
|
||||
action := eventName(parts[1:])
|
||||
|
||||
return topic, action
|
||||
}
|
||||
|
||||
func (e *event) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
bsize := handler.DefaultMaxRecvSize
|
||||
if e.opts.MaxRecvSize > 0 {
|
||||
bsize = e.opts.MaxRecvSize
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, bsize)
|
||||
|
||||
// request to topic:event
|
||||
// create event
|
||||
// publish to topic
|
||||
|
||||
topic, action := evRoute(e.opts.Namespace, r.URL.Path)
|
||||
|
||||
// create event
|
||||
ev := &proto.Event{
|
||||
Name: action,
|
||||
// TODO: dedupe event
|
||||
Id: fmt.Sprintf("%s-%s-%s", topic, action, uuid.New().String()),
|
||||
Header: make(map[string]*proto.Pair),
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
// set headers
|
||||
for key, vals := range r.Header {
|
||||
header, ok := ev.Header[key]
|
||||
if !ok {
|
||||
header = &proto.Pair{
|
||||
Key: key,
|
||||
}
|
||||
ev.Header[key] = header
|
||||
}
|
||||
header.Values = vals
|
||||
}
|
||||
|
||||
// set body
|
||||
if r.Method == "GET" {
|
||||
bytes, _ := json.Marshal(r.URL.Query())
|
||||
ev.Data = string(bytes)
|
||||
} else {
|
||||
// Read body
|
||||
buf := bufferPool.Get()
|
||||
defer bufferPool.Put(buf)
|
||||
if _, err := buf.ReadFrom(r.Body); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
ev.Data = buf.String()
|
||||
}
|
||||
|
||||
// get client
|
||||
c := e.opts.Service.Client()
|
||||
|
||||
// create publication
|
||||
p := c.NewMessage(topic, ev)
|
||||
|
||||
// publish event
|
||||
if err := c.Publish(ctx.FromRequest(r), p); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (e *event) String() string {
|
||||
return "event"
|
||||
}
|
||||
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
return &event{
|
||||
opts: handler.NewOptions(opts...),
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Package file serves file relative to the current directory
|
||||
package file
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Handler struct{}
|
||||
|
||||
func (h *Handler) Serve(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "."+r.URL.Path)
|
||||
}
|
||||
|
||||
func (h *Handler) String() string {
|
||||
return "file"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// Package handler provides http handlers
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Handler represents a HTTP handler that manages a request
|
||||
type Handler interface {
|
||||
// standard http handler
|
||||
http.Handler
|
||||
// name of handler
|
||||
String() string
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// Package http is a http reverse proxy handler
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
"github.com/micro/go-micro/v2/client/selector"
|
||||
)
|
||||
|
||||
const (
|
||||
Handler = "http"
|
||||
)
|
||||
|
||||
type httpHandler struct {
|
||||
options handler.Options
|
||||
|
||||
// set with different initialiser
|
||||
s *api.Service
|
||||
}
|
||||
|
||||
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
service, err := h.getService(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
|
||||
if len(service) == 0 {
|
||||
w.WriteHeader(404)
|
||||
return
|
||||
}
|
||||
|
||||
rp, err := url.Parse(service)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
|
||||
httputil.NewSingleHostReverseProxy(rp).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// getService returns the service for this request from the selector
|
||||
func (h *httpHandler) getService(r *http.Request) (string, error) {
|
||||
var service *api.Service
|
||||
|
||||
if h.s != nil {
|
||||
// we were given the service
|
||||
service = h.s
|
||||
} else if h.options.Router != nil {
|
||||
// try get service from router
|
||||
s, err := h.options.Router.Route(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
service = s
|
||||
} else {
|
||||
// we have no way of routing the request
|
||||
return "", errors.New("no route found")
|
||||
}
|
||||
|
||||
// create a random selector
|
||||
next := selector.Random(service.Services)
|
||||
|
||||
// get the next node
|
||||
s, err := next()
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http://%s", s.Address), nil
|
||||
}
|
||||
|
||||
func (h *httpHandler) String() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
// NewHandler returns a http proxy handler
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
|
||||
return &httpHandler{
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// WithService creates a handler with a service
|
||||
func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
|
||||
return &httpHandler{
|
||||
options: options,
|
||||
s: s,
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
"github.com/micro/go-micro/v2/api/router"
|
||||
regRouter "github.com/micro/go-micro/v2/api/router/registry"
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
"github.com/micro/go-micro/v2/registry/memory"
|
||||
)
|
||||
|
||||
func testHttp(t *testing.T, path, service, ns string) {
|
||||
r := memory.NewRegistry()
|
||||
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
s := ®istry.Service{
|
||||
Name: service,
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: service + "-1",
|
||||
Address: l.Addr().String(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r.Register(s)
|
||||
defer r.Deregister(s)
|
||||
|
||||
// setup the test handler
|
||||
m := http.NewServeMux()
|
||||
m.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`you got served`))
|
||||
})
|
||||
|
||||
// start http test serve
|
||||
go http.Serve(l, m)
|
||||
|
||||
// create new request and writer
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("POST", path, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// initialise the handler
|
||||
rt := regRouter.NewRouter(
|
||||
router.WithHandler("http"),
|
||||
router.WithNamespace(ns),
|
||||
router.WithRegistry(r),
|
||||
)
|
||||
|
||||
p := NewHandler(handler.WithRouter(rt))
|
||||
|
||||
// execute the handler
|
||||
p.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("Expected 200 response got %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
if w.Body.String() != "you got served" {
|
||||
t.Fatalf("Expected body: you got served. Got: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHttpHandler(t *testing.T) {
|
||||
testData := []struct {
|
||||
path string
|
||||
service string
|
||||
namespace string
|
||||
}{
|
||||
{
|
||||
"/test/foo",
|
||||
"go.micro.api.test",
|
||||
"go.micro.api",
|
||||
},
|
||||
{
|
||||
"/test/foo/baz",
|
||||
"go.micro.api.test",
|
||||
"go.micro.api",
|
||||
},
|
||||
{
|
||||
"/v1/foo",
|
||||
"go.micro.api.v1.foo",
|
||||
"go.micro.api",
|
||||
},
|
||||
{
|
||||
"/v1/foo/bar",
|
||||
"go.micro.api.v1.foo",
|
||||
"go.micro.api",
|
||||
},
|
||||
{
|
||||
"/v2/baz",
|
||||
"go.micro.api.v2.baz",
|
||||
"go.micro.api",
|
||||
},
|
||||
{
|
||||
"/v2/baz/bar",
|
||||
"go.micro.api.v2.baz",
|
||||
"go.micro.api",
|
||||
},
|
||||
{
|
||||
"/v2/baz/bar",
|
||||
"v2.baz",
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
testHttp(t, d.path, d.service, d.namespace)
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/micro/go-micro/v2"
|
||||
"github.com/micro/go-micro/v2/api/router"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultMaxRecvSize int64 = 1024 * 1024 * 100 // 10Mb
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
MaxRecvSize int64
|
||||
Namespace string
|
||||
Router router.Router
|
||||
Service micro.Service
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
// NewOptions fills in the blanks
|
||||
func NewOptions(opts ...Option) Options {
|
||||
var options Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
// create service if its blank
|
||||
if options.Service == nil {
|
||||
WithService(micro.NewService())(&options)
|
||||
}
|
||||
|
||||
// set namespace if blank
|
||||
if len(options.Namespace) == 0 {
|
||||
WithNamespace("go.micro.api")(&options)
|
||||
}
|
||||
|
||||
if options.MaxRecvSize == 0 {
|
||||
options.MaxRecvSize = DefaultMaxRecvSize
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// WithNamespace specifies the namespace for the handler
|
||||
func WithNamespace(s string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithRouter specifies a router to be used by the handler
|
||||
func WithRouter(r router.Router) Option {
|
||||
return func(o *Options) {
|
||||
o.Router = r
|
||||
}
|
||||
}
|
||||
|
||||
// WithService specifies a micro.Service
|
||||
func WithService(s micro.Service) Option {
|
||||
return func(o *Options) {
|
||||
o.Service = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithmaxRecvSize specifies max body size
|
||||
func WithMaxRecvSize(size int64) Option {
|
||||
return func(o *Options) {
|
||||
o.MaxRecvSize = size
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
// Package registry is a go-micro/registry handler
|
||||
package registry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
"github.com/oxtoacart/bpool"
|
||||
)
|
||||
|
||||
var (
|
||||
bufferPool = bpool.NewSizedBufferPool(1024, 8)
|
||||
)
|
||||
|
||||
const (
|
||||
Handler = "registry"
|
||||
|
||||
pingTime = (readDeadline * 9) / 10
|
||||
readLimit = 16384
|
||||
readDeadline = 60 * time.Second
|
||||
writeDeadline = 10 * time.Second
|
||||
)
|
||||
|
||||
type registryHandler struct {
|
||||
opts handler.Options
|
||||
reg registry.Registry
|
||||
}
|
||||
|
||||
func (rh *registryHandler) add(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
defer r.Body.Close()
|
||||
|
||||
// Read body
|
||||
buf := bufferPool.Get()
|
||||
defer bufferPool.Put(buf)
|
||||
if _, err := buf.ReadFrom(r.Body); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
var opts []registry.RegisterOption
|
||||
|
||||
// parse ttl
|
||||
if ttl := r.Form.Get("ttl"); len(ttl) > 0 {
|
||||
d, err := time.ParseDuration(ttl)
|
||||
if err == nil {
|
||||
opts = append(opts, registry.RegisterTTL(d))
|
||||
}
|
||||
}
|
||||
|
||||
var service *registry.Service
|
||||
if err := json.NewDecoder(buf).Decode(&service); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
if err := rh.reg.Register(service, opts...); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *registryHandler) del(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
defer r.Body.Close()
|
||||
|
||||
// Read body
|
||||
buf := bufferPool.Get()
|
||||
defer bufferPool.Put(buf)
|
||||
if _, err := buf.ReadFrom(r.Body); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
var service *registry.Service
|
||||
if err := json.NewDecoder(buf).Decode(&service); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
if err := rh.reg.Deregister(service); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *registryHandler) get(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
service := r.Form.Get("service")
|
||||
|
||||
var s []*registry.Service
|
||||
var err error
|
||||
|
||||
if len(service) == 0 {
|
||||
//
|
||||
upgrade := r.Header.Get("Upgrade")
|
||||
connect := r.Header.Get("Connection")
|
||||
|
||||
// watch if websockets
|
||||
if upgrade == "websocket" && connect == "Upgrade" {
|
||||
rw, err := rh.reg.Watch()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
watch(rw, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise list services
|
||||
s, err = rh.reg.ListServices()
|
||||
} else {
|
||||
s, err = rh.reg.GetService(service)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
if s == nil || (len(service) > 0 && (len(s) == 0 || len(s[0].Name) == 0)) {
|
||||
http.Error(w, "Service not found", 404)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(b)))
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func ping(ws *websocket.Conn, exit chan bool) {
|
||||
ticker := time.NewTicker(pingTime)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ws.SetWriteDeadline(time.Now().Add(writeDeadline))
|
||||
err := ws.WriteMessage(websocket.PingMessage, []byte{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case <-exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func watch(rw registry.Watcher, w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// we need an exit chan
|
||||
exit := make(chan bool)
|
||||
|
||||
defer func() {
|
||||
close(exit)
|
||||
}()
|
||||
|
||||
// ping the socket
|
||||
go ping(ws, exit)
|
||||
|
||||
for {
|
||||
// get next result
|
||||
r, err := rw.Next()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// write to client
|
||||
ws.SetWriteDeadline(time.Now().Add(writeDeadline))
|
||||
if err := ws.WriteJSON(r); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *registryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
bsize := handler.DefaultMaxRecvSize
|
||||
if rh.opts.MaxRecvSize > 0 {
|
||||
bsize = rh.opts.MaxRecvSize
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, bsize)
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
rh.get(w, r)
|
||||
case "POST":
|
||||
rh.add(w, r)
|
||||
case "DELETE":
|
||||
rh.del(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *registryHandler) String() string {
|
||||
return "registry"
|
||||
}
|
||||
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
|
||||
return ®istryHandler{
|
||||
opts: options,
|
||||
reg: options.Service.Client().Options().Registry,
|
||||
}
|
||||
}
|
||||
@@ -1,444 +0,0 @@
|
||||
// Package rpc is a go-micro rpc handler.
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||
"github.com/joncalhoun/qson"
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
"github.com/micro/go-micro/v2/api/internal/proto"
|
||||
"github.com/micro/go-micro/v2/client"
|
||||
"github.com/micro/go-micro/v2/client/selector"
|
||||
"github.com/micro/go-micro/v2/codec"
|
||||
"github.com/micro/go-micro/v2/codec/jsonrpc"
|
||||
"github.com/micro/go-micro/v2/codec/protorpc"
|
||||
"github.com/micro/go-micro/v2/errors"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
"github.com/micro/go-micro/v2/metadata"
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
"github.com/micro/go-micro/v2/util/ctx"
|
||||
"github.com/oxtoacart/bpool"
|
||||
)
|
||||
|
||||
const (
|
||||
Handler = "rpc"
|
||||
)
|
||||
|
||||
var (
|
||||
// supported json codecs
|
||||
jsonCodecs = []string{
|
||||
"application/grpc+json",
|
||||
"application/json",
|
||||
"application/json-rpc",
|
||||
}
|
||||
|
||||
// support proto codecs
|
||||
protoCodecs = []string{
|
||||
"application/grpc",
|
||||
"application/grpc+proto",
|
||||
"application/proto",
|
||||
"application/protobuf",
|
||||
"application/proto-rpc",
|
||||
"application/octet-stream",
|
||||
}
|
||||
|
||||
bufferPool = bpool.NewSizedBufferPool(1024, 8)
|
||||
)
|
||||
|
||||
type rpcHandler struct {
|
||||
opts handler.Options
|
||||
s *api.Service
|
||||
}
|
||||
|
||||
type buffer struct {
|
||||
io.ReadCloser
|
||||
}
|
||||
|
||||
func (b *buffer) Write(_ []byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// strategy is a hack for selection
|
||||
func strategy(services []*registry.Service) selector.Strategy {
|
||||
return func(_ []*registry.Service) selector.Next {
|
||||
// ignore input to this function, use services above
|
||||
return selector.Random(services)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
bsize := handler.DefaultMaxRecvSize
|
||||
if h.opts.MaxRecvSize > 0 {
|
||||
bsize = h.opts.MaxRecvSize
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, bsize)
|
||||
|
||||
defer r.Body.Close()
|
||||
var service *api.Service
|
||||
|
||||
if h.s != nil {
|
||||
// we were given the service
|
||||
service = h.s
|
||||
} else if h.opts.Router != nil {
|
||||
// try get service from router
|
||||
s, err := h.opts.Router.Route(r)
|
||||
if err != nil {
|
||||
writeError(w, r, errors.InternalServerError("go.micro.api", err.Error()))
|
||||
return
|
||||
}
|
||||
service = s
|
||||
} else {
|
||||
// we have no way of routing the request
|
||||
writeError(w, r, errors.InternalServerError("go.micro.api", "no route found"))
|
||||
return
|
||||
}
|
||||
|
||||
// only allow post when we have the router
|
||||
if r.Method != "GET" && (h.opts.Router != nil && r.Method != "POST") {
|
||||
writeError(w, r, errors.MethodNotAllowed("go.micro.api", "method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
ct := r.Header.Get("Content-Type")
|
||||
|
||||
// Strip charset from Content-Type (like `application/json; charset=UTF-8`)
|
||||
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
|
||||
ct = ct[:idx]
|
||||
}
|
||||
|
||||
// micro client
|
||||
c := h.opts.Service.Client()
|
||||
|
||||
// create context
|
||||
cx := ctx.FromRequest(r)
|
||||
|
||||
// if stream we currently only support json
|
||||
if isStream(r, service) {
|
||||
serveWebsocket(cx, w, r, service, c)
|
||||
return
|
||||
}
|
||||
|
||||
// create strategy
|
||||
so := selector.WithStrategy(strategy(service.Services))
|
||||
|
||||
// walk the standard call path
|
||||
// get payload
|
||||
br, err := requestPayload(r)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var rsp []byte
|
||||
|
||||
switch {
|
||||
// proto codecs
|
||||
case hasCodec(ct, protoCodecs):
|
||||
request := &proto.Message{}
|
||||
// if the extracted payload isn't empty lets use it
|
||||
if len(br) > 0 {
|
||||
request = proto.NewMessage(br)
|
||||
}
|
||||
|
||||
// create request/response
|
||||
response := &proto.Message{}
|
||||
|
||||
req := c.NewRequest(
|
||||
service.Name,
|
||||
service.Endpoint.Name,
|
||||
request,
|
||||
client.WithContentType(ct),
|
||||
)
|
||||
|
||||
// make the call
|
||||
if err := c.Call(cx, req, response, client.WithSelectOption(so)); err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// marshall response
|
||||
rsp, err = response.Marshal()
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
// if json codec is not present set to json
|
||||
if !hasCodec(ct, jsonCodecs) {
|
||||
ct = "application/json"
|
||||
}
|
||||
|
||||
// default to trying json
|
||||
var request json.RawMessage
|
||||
// if the extracted payload isn't empty lets use it
|
||||
if len(br) > 0 {
|
||||
request = json.RawMessage(br)
|
||||
}
|
||||
|
||||
// create request/response
|
||||
var response json.RawMessage
|
||||
|
||||
req := c.NewRequest(
|
||||
service.Name,
|
||||
service.Endpoint.Name,
|
||||
&request,
|
||||
client.WithContentType(ct),
|
||||
)
|
||||
|
||||
// make the call
|
||||
if err := c.Call(cx, req, &response, client.WithSelectOption(so)); err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// marshall response
|
||||
rsp, err = response.MarshalJSON()
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// write the response
|
||||
writeResponse(w, r, rsp)
|
||||
}
|
||||
|
||||
func (rh *rpcHandler) String() string {
|
||||
return "rpc"
|
||||
}
|
||||
|
||||
func hasCodec(ct string, codecs []string) bool {
|
||||
for _, codec := range codecs {
|
||||
if ct == codec {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// requestPayload takes a *http.Request.
|
||||
// If the request is a GET the query string parameters are extracted and marshaled to JSON and the raw bytes are returned.
|
||||
// If the request method is a POST the request body is read and returned
|
||||
func requestPayload(r *http.Request) ([]byte, error) {
|
||||
var err error
|
||||
|
||||
// we have to decode json-rpc and proto-rpc because we suck
|
||||
// well actually because there's no proxy codec right now
|
||||
|
||||
ct := r.Header.Get("Content-Type")
|
||||
switch {
|
||||
case strings.Contains(ct, "application/json-rpc"):
|
||||
msg := codec.Message{
|
||||
Type: codec.Request,
|
||||
Header: make(map[string]string),
|
||||
}
|
||||
c := jsonrpc.NewCodec(&buffer{r.Body})
|
||||
if err = c.ReadHeader(&msg, codec.Request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var raw json.RawMessage
|
||||
if err = c.ReadBody(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ([]byte)(raw), nil
|
||||
case strings.Contains(ct, "application/proto-rpc"), strings.Contains(ct, "application/octet-stream"):
|
||||
msg := codec.Message{
|
||||
Type: codec.Request,
|
||||
Header: make(map[string]string),
|
||||
}
|
||||
c := protorpc.NewCodec(&buffer{r.Body})
|
||||
if err = c.ReadHeader(&msg, codec.Request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var raw proto.Message
|
||||
if err = c.ReadBody(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return raw.Marshal()
|
||||
case strings.Contains(ct, "application/www-x-form-urlencoded"):
|
||||
r.ParseForm()
|
||||
|
||||
// generate a new set of values from the form
|
||||
vals := make(map[string]string)
|
||||
for k, v := range r.Form {
|
||||
vals[k] = strings.Join(v, ",")
|
||||
}
|
||||
|
||||
// marshal
|
||||
return json.Marshal(vals)
|
||||
// TODO: application/grpc
|
||||
}
|
||||
|
||||
// otherwise as per usual
|
||||
ctx := r.Context()
|
||||
// dont user meadata.FromContext as it mangles names
|
||||
md, ok := ctx.Value(metadata.MetadataKey{}).(metadata.Metadata)
|
||||
if !ok {
|
||||
md = make(map[string]string)
|
||||
}
|
||||
// allocate maximum
|
||||
matches := make(map[string]string, len(md))
|
||||
for k, v := range md {
|
||||
if strings.HasPrefix(k, "x-api-field-") {
|
||||
matches[strings.TrimPrefix(k, "x-api-field-")] = v
|
||||
}
|
||||
delete(md, k)
|
||||
}
|
||||
|
||||
// restore context without fields
|
||||
ctx = metadata.NewContext(ctx, md)
|
||||
*r = *r.WithContext(ctx)
|
||||
req := make(map[string]interface{}, len(md))
|
||||
for k, v := range matches {
|
||||
ps := strings.Split(k, ".")
|
||||
if len(ps) == 1 {
|
||||
req[k] = v
|
||||
continue
|
||||
}
|
||||
|
||||
em := make(map[string]interface{})
|
||||
em[ps[len(ps)-1]] = v
|
||||
for i := len(ps) - 2; i > 0; i-- {
|
||||
nm := make(map[string]interface{})
|
||||
nm[ps[i]] = em
|
||||
em = nm
|
||||
}
|
||||
req[ps[0]] = em
|
||||
}
|
||||
pathbuf := []byte("{}")
|
||||
if len(req) > 0 {
|
||||
pathbuf, err = json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
urlbuf := []byte("{}")
|
||||
if len(r.URL.RawQuery) > 0 {
|
||||
urlbuf, err = qson.ToJSON(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
out, err := jsonpatch.MergeMergePatches(urlbuf, pathbuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
// empty response
|
||||
if strings.Contains(ct, "application/json") && string(out) == "{}" {
|
||||
return out, nil
|
||||
} else if string(out) == "{}" && !strings.Contains(ct, "application/json") {
|
||||
return []byte{}, nil
|
||||
}
|
||||
return out, nil
|
||||
case "PATCH", "POST", "PUT", "DELETE":
|
||||
bodybuf := []byte("{}")
|
||||
buf := bufferPool.Get()
|
||||
defer bufferPool.Put(buf)
|
||||
if _, err := buf.ReadFrom(r.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if b := buf.Bytes(); len(b) > 0 {
|
||||
bodybuf = b
|
||||
} else {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
if out, err = jsonpatch.MergeMergePatches(out, bodybuf); err == nil {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
//fallback to previous unknown behaviour
|
||||
return bodybuf, nil
|
||||
|
||||
}
|
||||
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
ce := errors.Parse(err.Error())
|
||||
|
||||
switch ce.Code {
|
||||
case 0:
|
||||
// assuming it's totally screwed
|
||||
ce.Code = 500
|
||||
ce.Id = "go.micro.api"
|
||||
ce.Status = http.StatusText(500)
|
||||
ce.Detail = "error during request: " + ce.Detail
|
||||
w.WriteHeader(500)
|
||||
default:
|
||||
w.WriteHeader(int(ce.Code))
|
||||
}
|
||||
|
||||
// response content type
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Set trailers
|
||||
if strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
|
||||
w.Header().Set("Trailer", "grpc-status")
|
||||
w.Header().Set("Trailer", "grpc-message")
|
||||
w.Header().Set("grpc-status", "13")
|
||||
w.Header().Set("grpc-message", ce.Detail)
|
||||
}
|
||||
|
||||
_, werr := w.Write([]byte(ce.Error()))
|
||||
if werr != nil {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Error(werr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeResponse(w http.ResponseWriter, r *http.Request, rsp []byte) {
|
||||
w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(rsp)))
|
||||
|
||||
// Set trailers
|
||||
if strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
|
||||
w.Header().Set("Trailer", "grpc-status")
|
||||
w.Header().Set("Trailer", "grpc-message")
|
||||
w.Header().Set("grpc-status", "0")
|
||||
w.Header().Set("grpc-message", "")
|
||||
}
|
||||
|
||||
// write 204 status if rsp is nil
|
||||
if len(rsp) == 0 {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// write response
|
||||
_, err := w.Write(rsp)
|
||||
if err != nil {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
return &rpcHandler{
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
return &rpcHandler{
|
||||
opts: options,
|
||||
s: s,
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
go_api "github.com/micro/go-micro/v2/api/proto"
|
||||
)
|
||||
|
||||
func TestRequestPayloadFromRequest(t *testing.T) {
|
||||
|
||||
// our test event so that we can validate serialising / deserializing of true protos works
|
||||
protoEvent := go_api.Event{
|
||||
Name: "Test",
|
||||
}
|
||||
|
||||
protoBytes, err := proto.Marshal(&protoEvent)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to marshal proto", err)
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(protoEvent)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to marshal proto to JSON ", err)
|
||||
}
|
||||
|
||||
jsonUrlBytes := []byte(`{"key1":"val1","key2":"val2","name":"Test"}`)
|
||||
|
||||
t.Run("extracting a json from a POST request with url params", func(t *testing.T) {
|
||||
r, err := http.NewRequest("POST", "http://localhost/my/path?key1=val1&key2=val2", bytes.NewReader(jsonBytes))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to created http.Request: %v", err)
|
||||
}
|
||||
|
||||
extByte, err := requestPayload(r)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to extract payload from request: %v", err)
|
||||
}
|
||||
if string(extByte) != string(jsonUrlBytes) {
|
||||
t.Fatalf("Expected %v and %v to match", string(extByte), jsonUrlBytes)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("extracting a proto from a POST request", func(t *testing.T) {
|
||||
r, err := http.NewRequest("POST", "http://localhost/my/path", bytes.NewReader(protoBytes))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to created http.Request: %v", err)
|
||||
}
|
||||
|
||||
extByte, err := requestPayload(r)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to extract payload from request: %v", err)
|
||||
}
|
||||
if string(extByte) != string(protoBytes) {
|
||||
t.Fatalf("Expected %v and %v to match", string(extByte), string(protoBytes))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("extracting JSON from a POST request", func(t *testing.T) {
|
||||
r, err := http.NewRequest("POST", "http://localhost/my/path", bytes.NewReader(jsonBytes))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to created http.Request: %v", err)
|
||||
}
|
||||
|
||||
extByte, err := requestPayload(r)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to extract payload from request: %v", err)
|
||||
}
|
||||
if string(extByte) != string(jsonBytes) {
|
||||
t.Fatalf("Expected %v and %v to match", string(extByte), string(jsonBytes))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("extracting params from a GET request", func(t *testing.T) {
|
||||
|
||||
r, err := http.NewRequest("GET", "http://localhost/my/path", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to created http.Request: %v", err)
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
q.Add("name", "Test")
|
||||
r.URL.RawQuery = q.Encode()
|
||||
|
||||
extByte, err := requestPayload(r)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to extract payload from request: %v", err)
|
||||
}
|
||||
if string(extByte) != string(jsonBytes) {
|
||||
t.Fatalf("Expected %v and %v to match", string(extByte), string(jsonBytes))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GET request with no params", func(t *testing.T) {
|
||||
|
||||
r, err := http.NewRequest("GET", "http://localhost/my/path", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to created http.Request: %v", err)
|
||||
}
|
||||
|
||||
extByte, err := requestPayload(r)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to extract payload from request: %v", err)
|
||||
}
|
||||
if string(extByte) != "" {
|
||||
t.Fatalf("Expected %v and %v to match", string(extByte), "")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
"github.com/micro/go-micro/v2/client"
|
||||
"github.com/micro/go-micro/v2/client/selector"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
// serveWebsocket will stream rpc back over websockets assuming json
|
||||
func serveWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, service *api.Service, c client.Client) {
|
||||
// upgrade the connection
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// close on exit
|
||||
defer conn.Close()
|
||||
|
||||
// wait for the first request so we know
|
||||
_, p, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// send to backend
|
||||
// default to trying json
|
||||
var request json.RawMessage
|
||||
// if the extracted payload isn't empty lets use it
|
||||
if len(p) > 0 {
|
||||
request = json.RawMessage(p)
|
||||
}
|
||||
|
||||
// create a request to the backend
|
||||
req := c.NewRequest(
|
||||
service.Name,
|
||||
service.Endpoint.Name,
|
||||
&request,
|
||||
client.WithContentType("application/json"),
|
||||
)
|
||||
|
||||
so := selector.WithStrategy(strategy(service.Services))
|
||||
|
||||
// create a new stream
|
||||
stream, err := c.Stream(ctx, req, client.WithSelectOption(so))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// send the first request for the client
|
||||
// since
|
||||
if err := stream.Send(request); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go writeLoop(conn, stream)
|
||||
|
||||
resp := stream.Response()
|
||||
|
||||
// receive from stream and send to client
|
||||
for {
|
||||
// read backend response body
|
||||
body, err := resp.Read()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// write the response
|
||||
if err := conn.WriteMessage(websocket.TextMessage, body); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeLoop
|
||||
func writeLoop(conn *websocket.Conn, stream client.Stream) {
|
||||
// close stream when done
|
||||
defer stream.Close()
|
||||
|
||||
for {
|
||||
_, p, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// send to backend
|
||||
// default to trying json
|
||||
var request json.RawMessage
|
||||
// if the extracted payload isn't empty lets use it
|
||||
if len(p) > 0 {
|
||||
request = json.RawMessage(p)
|
||||
}
|
||||
|
||||
if err := stream.Send(request); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isStream(r *http.Request, srv *api.Service) bool {
|
||||
// check if it's a web socket
|
||||
if !isWebSocket(r) {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if the endpoint supports streaming
|
||||
for _, service := range srv.Services {
|
||||
for _, ep := range service.Endpoints {
|
||||
// skip if it doesn't match the name
|
||||
if ep.Name != srv.Endpoint.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
// matched if the name
|
||||
if v := ep.Metadata["stream"]; v == "true" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isWebSocket(r *http.Request) bool {
|
||||
contains := func(key, val string) bool {
|
||||
vv := strings.Split(r.Header.Get(key), ",")
|
||||
for _, v := range vv {
|
||||
if val == strings.ToLower(strings.TrimSpace(v)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if contains("Connection", "upgrade") && contains("Upgrade", "websocket") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Package udp reads and write from a udp connection
|
||||
package udp
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Handler struct{}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := net.Dial("udp", r.Host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
go io.Copy(c, r.Body)
|
||||
// write response
|
||||
io.Copy(w, c)
|
||||
}
|
||||
|
||||
func (h *Handler) String() string {
|
||||
return "udp"
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Package unix reads from a unix socket expecting it to be in /tmp/path
|
||||
package unix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Handler struct{}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
sock := fmt.Sprintf("%s.sock", filepath.Clean(r.URL.Path))
|
||||
path := filepath.Join("/tmp", sock)
|
||||
|
||||
c, err := net.Dial("unix", path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
go io.Copy(c, r.Body)
|
||||
// write response
|
||||
io.Copy(w, c)
|
||||
}
|
||||
|
||||
func (h *Handler) String() string {
|
||||
return "unix"
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
// Package web contains the web handler including websocket support
|
||||
package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
"github.com/micro/go-micro/v2/api/handler"
|
||||
"github.com/micro/go-micro/v2/client/selector"
|
||||
)
|
||||
|
||||
const (
|
||||
Handler = "web"
|
||||
)
|
||||
|
||||
type webHandler struct {
|
||||
opts handler.Options
|
||||
s *api.Service
|
||||
}
|
||||
|
||||
func (wh *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
service, err := wh.getService(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
|
||||
if len(service) == 0 {
|
||||
w.WriteHeader(404)
|
||||
return
|
||||
}
|
||||
|
||||
rp, err := url.Parse(service)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
|
||||
if isWebSocket(r) {
|
||||
wh.serveWebSocket(rp.Host, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
httputil.NewSingleHostReverseProxy(rp).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// getService returns the service for this request from the selector
|
||||
func (wh *webHandler) getService(r *http.Request) (string, error) {
|
||||
var service *api.Service
|
||||
|
||||
if wh.s != nil {
|
||||
// we were given the service
|
||||
service = wh.s
|
||||
} else if wh.opts.Router != nil {
|
||||
// try get service from router
|
||||
s, err := wh.opts.Router.Route(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
service = s
|
||||
} else {
|
||||
// we have no way of routing the request
|
||||
return "", errors.New("no route found")
|
||||
}
|
||||
|
||||
// create a random selector
|
||||
next := selector.Random(service.Services)
|
||||
|
||||
// get the next node
|
||||
s, err := next()
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http://%s", s.Address), nil
|
||||
}
|
||||
|
||||
// serveWebSocket used to serve a web socket proxied connection
|
||||
func (wh *webHandler) serveWebSocket(host string, w http.ResponseWriter, r *http.Request) {
|
||||
req := new(http.Request)
|
||||
*req = *r
|
||||
|
||||
if len(host) == 0 {
|
||||
http.Error(w, "invalid host", 500)
|
||||
return
|
||||
}
|
||||
|
||||
// set x-forward-for
|
||||
if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
if ips, ok := req.Header["X-Forwarded-For"]; ok {
|
||||
clientIP = strings.Join(ips, ", ") + ", " + clientIP
|
||||
}
|
||||
req.Header.Set("X-Forwarded-For", clientIP)
|
||||
}
|
||||
|
||||
// connect to the backend host
|
||||
conn, err := net.Dial("tcp", host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// hijack the connection
|
||||
hj, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "failed to connect", 500)
|
||||
return
|
||||
}
|
||||
|
||||
nc, _, err := hj.Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer nc.Close()
|
||||
defer conn.Close()
|
||||
|
||||
if err = req.Write(conn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
errCh := make(chan error, 2)
|
||||
|
||||
cp := func(dst io.Writer, src io.Reader) {
|
||||
_, err := io.Copy(dst, src)
|
||||
errCh <- err
|
||||
}
|
||||
|
||||
go cp(conn, nc)
|
||||
go cp(nc, conn)
|
||||
|
||||
<-errCh
|
||||
}
|
||||
|
||||
func isWebSocket(r *http.Request) bool {
|
||||
contains := func(key, val string) bool {
|
||||
vv := strings.Split(r.Header.Get(key), ",")
|
||||
for _, v := range vv {
|
||||
if val == strings.ToLower(strings.TrimSpace(v)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if contains("Connection", "upgrade") && contains("Upgrade", "websocket") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (wh *webHandler) String() string {
|
||||
return "web"
|
||||
}
|
||||
|
||||
func NewHandler(opts ...handler.Option) handler.Handler {
|
||||
return &webHandler{
|
||||
opts: handler.NewOptions(opts...),
|
||||
}
|
||||
}
|
||||
|
||||
func WithService(s *api.Service, opts ...handler.Option) handler.Handler {
|
||||
options := handler.NewOptions(opts...)
|
||||
|
||||
return &webHandler{
|
||||
opts: options,
|
||||
s: s,
|
||||
}
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// source: api/proto/api.proto
|
||||
|
||||
package go_api
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
math "math"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
|
||||
|
||||
type Pair struct {
|
||||
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||
Values []string `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Pair) Reset() { *m = Pair{} }
|
||||
func (m *Pair) String() string { return proto.CompactTextString(m) }
|
||||
func (*Pair) ProtoMessage() {}
|
||||
func (*Pair) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_2df576b66d12087a, []int{0}
|
||||
}
|
||||
|
||||
func (m *Pair) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Pair.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Pair) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Pair.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Pair) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Pair.Merge(m, src)
|
||||
}
|
||||
func (m *Pair) XXX_Size() int {
|
||||
return xxx_messageInfo_Pair.Size(m)
|
||||
}
|
||||
func (m *Pair) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Pair.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Pair proto.InternalMessageInfo
|
||||
|
||||
func (m *Pair) GetKey() string {
|
||||
if m != nil {
|
||||
return m.Key
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Pair) GetValues() []string {
|
||||
if m != nil {
|
||||
return m.Values
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A HTTP request as RPC
|
||||
// Forward by the api handler
|
||||
type Request struct {
|
||||
Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"`
|
||||
Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"`
|
||||
Header map[string]*Pair `protobuf:"bytes,3,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
Get map[string]*Pair `protobuf:"bytes,4,rep,name=get,proto3" json:"get,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
Post map[string]*Pair `protobuf:"bytes,5,rep,name=post,proto3" json:"post,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
Body string `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
|
||||
Url string `protobuf:"bytes,7,opt,name=url,proto3" json:"url,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Request) Reset() { *m = Request{} }
|
||||
func (m *Request) String() string { return proto.CompactTextString(m) }
|
||||
func (*Request) ProtoMessage() {}
|
||||
func (*Request) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_2df576b66d12087a, []int{1}
|
||||
}
|
||||
|
||||
func (m *Request) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Request.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Request.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Request) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Request.Merge(m, src)
|
||||
}
|
||||
func (m *Request) XXX_Size() int {
|
||||
return xxx_messageInfo_Request.Size(m)
|
||||
}
|
||||
func (m *Request) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Request.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Request proto.InternalMessageInfo
|
||||
|
||||
func (m *Request) GetMethod() string {
|
||||
if m != nil {
|
||||
return m.Method
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Request) GetPath() string {
|
||||
if m != nil {
|
||||
return m.Path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Request) GetHeader() map[string]*Pair {
|
||||
if m != nil {
|
||||
return m.Header
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Request) GetGet() map[string]*Pair {
|
||||
if m != nil {
|
||||
return m.Get
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Request) GetPost() map[string]*Pair {
|
||||
if m != nil {
|
||||
return m.Post
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Request) GetBody() string {
|
||||
if m != nil {
|
||||
return m.Body
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Request) GetUrl() string {
|
||||
if m != nil {
|
||||
return m.Url
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// A HTTP response as RPC
|
||||
// Expected response for the api handler
|
||||
type Response struct {
|
||||
StatusCode int32 `protobuf:"varint,1,opt,name=statusCode,proto3" json:"statusCode,omitempty"`
|
||||
Header map[string]*Pair `protobuf:"bytes,2,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
Body string `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Response) Reset() { *m = Response{} }
|
||||
func (m *Response) String() string { return proto.CompactTextString(m) }
|
||||
func (*Response) ProtoMessage() {}
|
||||
func (*Response) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_2df576b66d12087a, []int{2}
|
||||
}
|
||||
|
||||
func (m *Response) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Response.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Response.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Response) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Response.Merge(m, src)
|
||||
}
|
||||
func (m *Response) XXX_Size() int {
|
||||
return xxx_messageInfo_Response.Size(m)
|
||||
}
|
||||
func (m *Response) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Response.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Response proto.InternalMessageInfo
|
||||
|
||||
func (m *Response) GetStatusCode() int32 {
|
||||
if m != nil {
|
||||
return m.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Response) GetHeader() map[string]*Pair {
|
||||
if m != nil {
|
||||
return m.Header
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Response) GetBody() string {
|
||||
if m != nil {
|
||||
return m.Body
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// A HTTP event as RPC
|
||||
// Forwarded by the event handler
|
||||
type Event struct {
|
||||
// e.g login
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
// uuid
|
||||
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
|
||||
// unix timestamp of event
|
||||
Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||
// event headers
|
||||
Header map[string]*Pair `protobuf:"bytes,4,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
// the event data
|
||||
Data string `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Event) Reset() { *m = Event{} }
|
||||
func (m *Event) String() string { return proto.CompactTextString(m) }
|
||||
func (*Event) ProtoMessage() {}
|
||||
func (*Event) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_2df576b66d12087a, []int{3}
|
||||
}
|
||||
|
||||
func (m *Event) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Event.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Event) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Event.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Event) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Event.Merge(m, src)
|
||||
}
|
||||
func (m *Event) XXX_Size() int {
|
||||
return xxx_messageInfo_Event.Size(m)
|
||||
}
|
||||
func (m *Event) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Event.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Event proto.InternalMessageInfo
|
||||
|
||||
func (m *Event) GetName() string {
|
||||
if m != nil {
|
||||
return m.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Event) GetId() string {
|
||||
if m != nil {
|
||||
return m.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Event) GetTimestamp() int64 {
|
||||
if m != nil {
|
||||
return m.Timestamp
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Event) GetHeader() map[string]*Pair {
|
||||
if m != nil {
|
||||
return m.Header
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Event) GetData() string {
|
||||
if m != nil {
|
||||
return m.Data
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*Pair)(nil), "go.api.Pair")
|
||||
proto.RegisterType((*Request)(nil), "go.api.Request")
|
||||
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Request.GetEntry")
|
||||
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Request.HeaderEntry")
|
||||
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Request.PostEntry")
|
||||
proto.RegisterType((*Response)(nil), "go.api.Response")
|
||||
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Response.HeaderEntry")
|
||||
proto.RegisterType((*Event)(nil), "go.api.Event")
|
||||
proto.RegisterMapType((map[string]*Pair)(nil), "go.api.Event.HeaderEntry")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("api/proto/api.proto", fileDescriptor_2df576b66d12087a) }
|
||||
|
||||
var fileDescriptor_2df576b66d12087a = []byte{
|
||||
// 393 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x53, 0xcd, 0xce, 0xd3, 0x30,
|
||||
0x10, 0x54, 0xe2, 0x24, 0x6d, 0xb6, 0x08, 0x21, 0x23, 0x21, 0x53, 0x2a, 0x54, 0xe5, 0x54, 0x21,
|
||||
0x91, 0x42, 0xcb, 0x01, 0x71, 0x85, 0xaa, 0x1c, 0x2b, 0xbf, 0x81, 0xab, 0x58, 0x6d, 0x44, 0x13,
|
||||
0x9b, 0xd8, 0xa9, 0xd4, 0x87, 0xe3, 0xc0, 0x63, 0xf0, 0x36, 0xc8, 0x1b, 0xf7, 0xe7, 0xab, 0xfa,
|
||||
0x5d, 0xbe, 0xaf, 0xb7, 0x89, 0x3d, 0x3b, 0x3b, 0x3b, 0xeb, 0xc0, 0x6b, 0xa1, 0xcb, 0xa9, 0x6e,
|
||||
0x94, 0x55, 0x53, 0xa1, 0xcb, 0x1c, 0x11, 0x4d, 0x36, 0x2a, 0x17, 0xba, 0xcc, 0x3e, 0x41, 0xb4,
|
||||
0x12, 0x65, 0x43, 0x5f, 0x01, 0xf9, 0x25, 0x0f, 0x2c, 0x18, 0x07, 0x93, 0x94, 0x3b, 0x48, 0xdf,
|
||||
0x40, 0xb2, 0x17, 0xbb, 0x56, 0x1a, 0x16, 0x8e, 0xc9, 0x24, 0xe5, 0xfe, 0x2b, 0xfb, 0x4b, 0xa0,
|
||||
0xc7, 0xe5, 0xef, 0x56, 0x1a, 0xeb, 0x38, 0x95, 0xb4, 0x5b, 0x55, 0xf8, 0x42, 0xff, 0x45, 0x29,
|
||||
0x44, 0x5a, 0xd8, 0x2d, 0x0b, 0xf1, 0x14, 0x31, 0x9d, 0x43, 0xb2, 0x95, 0xa2, 0x90, 0x0d, 0x23,
|
||||
0x63, 0x32, 0x19, 0xcc, 0xde, 0xe5, 0x9d, 0x85, 0xdc, 0x8b, 0xe5, 0x3f, 0xf1, 0x76, 0x51, 0xdb,
|
||||
0xe6, 0xc0, 0x3d, 0x95, 0x7e, 0x00, 0xb2, 0x91, 0x96, 0x45, 0x58, 0xc1, 0xae, 0x2b, 0x96, 0xd2,
|
||||
0x76, 0x74, 0x47, 0xa2, 0x1f, 0x21, 0xd2, 0xca, 0x58, 0x16, 0x23, 0xf9, 0xed, 0x35, 0x79, 0xa5,
|
||||
0x8c, 0x67, 0x23, 0xcd, 0x79, 0x5c, 0xab, 0xe2, 0xc0, 0x92, 0xce, 0xa3, 0xc3, 0x2e, 0x85, 0xb6,
|
||||
0xd9, 0xb1, 0x5e, 0x97, 0x42, 0xdb, 0xec, 0x86, 0x4b, 0x18, 0x5c, 0xf8, 0xba, 0x11, 0x53, 0x06,
|
||||
0x31, 0x06, 0x83, 0xb3, 0x0e, 0x66, 0x2f, 0x8e, 0x6d, 0x5d, 0xaa, 0xbc, 0xbb, 0xfa, 0x16, 0x7e,
|
||||
0x0d, 0x86, 0x3f, 0xa0, 0x7f, 0xb4, 0xfb, 0x0c, 0x95, 0x05, 0xa4, 0xa7, 0x39, 0x9e, 0x2e, 0x93,
|
||||
0xfd, 0x09, 0xa0, 0xcf, 0xa5, 0xd1, 0xaa, 0x36, 0x92, 0xbe, 0x07, 0x30, 0x56, 0xd8, 0xd6, 0x7c,
|
||||
0x57, 0x85, 0x44, 0xb5, 0x98, 0x5f, 0x9c, 0xd0, 0x2f, 0xa7, 0xc5, 0x85, 0x98, 0xec, 0xe8, 0x9c,
|
||||
0x6c, 0xa7, 0x70, 0x73, 0x73, 0xc7, 0x78, 0xc9, 0x39, 0xde, 0xbb, 0x85, 0x99, 0xfd, 0x0b, 0x20,
|
||||
0x5e, 0xec, 0x65, 0x8d, 0x5b, 0xac, 0x45, 0x25, 0xbd, 0x08, 0x62, 0xfa, 0x12, 0xc2, 0xb2, 0xf0,
|
||||
0x6f, 0x2f, 0x2c, 0x0b, 0x3a, 0x82, 0xd4, 0x96, 0x95, 0x34, 0x56, 0x54, 0x1a, 0xfd, 0x10, 0x7e,
|
||||
0x3e, 0xa0, 0x9f, 0x4f, 0xe3, 0x45, 0x0f, 0x1f, 0x0e, 0x36, 0x78, 0x6c, 0xb6, 0x42, 0x58, 0xc1,
|
||||
0xe2, 0xae, 0xa9, 0xc3, 0x77, 0x9b, 0x6d, 0x9d, 0xe0, 0x0f, 0x3a, 0xff, 0x1f, 0x00, 0x00, 0xff,
|
||||
0xff, 0xd4, 0x6d, 0x70, 0x51, 0xb7, 0x03, 0x00, 0x00,
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Code generated by protoc-gen-micro. DO NOT EDIT.
|
||||
// source: api/proto/api.proto
|
||||
|
||||
package go_api
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
math "math"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
|
||||
@@ -1,43 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package go.api;
|
||||
|
||||
message Pair {
|
||||
string key = 1;
|
||||
repeated string values = 2;
|
||||
}
|
||||
|
||||
// A HTTP request as RPC
|
||||
// Forward by the api handler
|
||||
message Request {
|
||||
string method = 1;
|
||||
string path = 2;
|
||||
map<string, Pair> header = 3;
|
||||
map<string, Pair> get = 4;
|
||||
map<string, Pair> post = 5;
|
||||
string body = 6; // raw request body; if not application/x-www-form-urlencoded
|
||||
string url = 7;
|
||||
}
|
||||
|
||||
// A HTTP response as RPC
|
||||
// Expected response for the api handler
|
||||
message Response {
|
||||
int32 statusCode = 1;
|
||||
map<string, Pair> header = 2;
|
||||
string body = 3;
|
||||
}
|
||||
|
||||
// A HTTP event as RPC
|
||||
// Forwarded by the event handler
|
||||
message Event {
|
||||
// e.g login
|
||||
string name = 1;
|
||||
// uuid
|
||||
string id = 2;
|
||||
// unix timestamp of event
|
||||
int64 timestamp = 3;
|
||||
// event headers
|
||||
map<string, Pair> header = 4;
|
||||
// the event data
|
||||
string data = 5;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Package grpc resolves a grpc service like /greeter.Say/Hello to greeter service
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
)
|
||||
|
||||
type Resolver struct{}
|
||||
|
||||
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||
// /foo.Bar/Service
|
||||
if req.URL.Path == "/" {
|
||||
return nil, errors.New("unknown name")
|
||||
}
|
||||
// [foo.Bar, Service]
|
||||
parts := strings.Split(req.URL.Path[1:], "/")
|
||||
// [foo, Bar]
|
||||
name := strings.Split(parts[0], ".")
|
||||
// foo
|
||||
return &resolver.Endpoint{
|
||||
Name: strings.Join(name[:len(name)-1], "."),
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) String() string {
|
||||
return "grpc"
|
||||
}
|
||||
|
||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
||||
return &Resolver{}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Package host resolves using http host
|
||||
package host
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
)
|
||||
|
||||
type Resolver struct{}
|
||||
|
||||
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||
return &resolver.Endpoint{
|
||||
Name: req.Host,
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) String() string {
|
||||
return "host"
|
||||
}
|
||||
|
||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
||||
return &Resolver{}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Package micro provides a micro rpc resolver which prefixes a namespace
|
||||
package micro
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
)
|
||||
|
||||
// default resolver for legacy purposes
|
||||
// it uses proxy routing to resolve names
|
||||
// /foo becomes namespace.foo
|
||||
// /v1/foo becomes namespace.v1.foo
|
||||
type Resolver struct {
|
||||
Options resolver.Options
|
||||
}
|
||||
|
||||
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||
var name, method string
|
||||
|
||||
switch r.Options.Handler {
|
||||
// internal handlers
|
||||
case "meta", "api", "rpc", "micro":
|
||||
name, method = apiRoute(req.URL.Path)
|
||||
default:
|
||||
method = req.Method
|
||||
name = proxyRoute(req.URL.Path)
|
||||
}
|
||||
|
||||
return &resolver.Endpoint{
|
||||
Name: name,
|
||||
Method: method,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) String() string {
|
||||
return "micro"
|
||||
}
|
||||
|
||||
// NewResolver creates a new micro resolver
|
||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
||||
return &Resolver{
|
||||
Options: resolver.NewOptions(opts...),
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package micro
|
||||
|
||||
import (
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
proxyRe = regexp.MustCompile("^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$")
|
||||
versionRe = regexp.MustCompilePOSIX("^v[0-9]+$")
|
||||
)
|
||||
|
||||
// Translates /foo/bar/zool into api service go.micro.api.foo method Bar.Zool
|
||||
// Translates /foo/bar into api service go.micro.api.foo method Foo.Bar
|
||||
func apiRoute(p string) (string, string) {
|
||||
p = path.Clean(p)
|
||||
p = strings.TrimPrefix(p, "/")
|
||||
parts := strings.Split(p, "/")
|
||||
|
||||
// if we have 1 part assume name Name.Call
|
||||
if len(parts) == 1 && len(parts[0]) > 0 {
|
||||
return parts[0], methodName(append(parts, "Call"))
|
||||
}
|
||||
|
||||
// If we've got two or less parts
|
||||
// Use first part as service
|
||||
// Use all parts as method
|
||||
if len(parts) <= 2 {
|
||||
name := parts[0]
|
||||
return name, methodName(parts)
|
||||
}
|
||||
|
||||
// Treat /v[0-9]+ as versioning where we have 3 parts
|
||||
// /v1/foo/bar => service: v1.foo method: Foo.bar
|
||||
if len(parts) == 3 && versionRe.Match([]byte(parts[0])) {
|
||||
name := strings.Join(parts[:len(parts)-1], ".")
|
||||
return name, methodName(parts[len(parts)-2:])
|
||||
}
|
||||
|
||||
// Service is everything minus last two parts
|
||||
// Method is the last two parts
|
||||
name := strings.Join(parts[:len(parts)-2], ".")
|
||||
return name, methodName(parts[len(parts)-2:])
|
||||
}
|
||||
|
||||
func proxyRoute(p string) string {
|
||||
parts := strings.Split(p, "/")
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var service string
|
||||
var alias string
|
||||
|
||||
// /[service]/methods
|
||||
if len(parts) > 2 {
|
||||
// /v1/[service]
|
||||
if versionRe.MatchString(parts[1]) {
|
||||
service = parts[1] + "." + parts[2]
|
||||
alias = parts[2]
|
||||
} else {
|
||||
service = parts[1]
|
||||
alias = parts[1]
|
||||
}
|
||||
// /[service]
|
||||
} else {
|
||||
service = parts[1]
|
||||
alias = parts[1]
|
||||
}
|
||||
|
||||
// check service name is valid
|
||||
if !proxyRe.MatchString(alias) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func methodName(parts []string) string {
|
||||
for i, part := range parts {
|
||||
parts[i] = toCamel(part)
|
||||
}
|
||||
|
||||
return strings.Join(parts, ".")
|
||||
}
|
||||
|
||||
func toCamel(s string) string {
|
||||
words := strings.Split(s, "-")
|
||||
var out string
|
||||
for _, word := range words {
|
||||
out += strings.Title(word)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package micro
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestApiRoute(t *testing.T) {
|
||||
testData := []struct {
|
||||
path string
|
||||
service string
|
||||
method string
|
||||
}{
|
||||
{
|
||||
"/foo/bar",
|
||||
"foo",
|
||||
"Foo.Bar",
|
||||
},
|
||||
{
|
||||
"/foo/foo/bar",
|
||||
"foo",
|
||||
"Foo.Bar",
|
||||
},
|
||||
{
|
||||
"/foo/bar/baz",
|
||||
"foo",
|
||||
"Bar.Baz",
|
||||
},
|
||||
{
|
||||
"/foo/bar/baz-xyz",
|
||||
"foo",
|
||||
"Bar.BazXyz",
|
||||
},
|
||||
{
|
||||
"/foo/bar/baz/cat",
|
||||
"foo.bar",
|
||||
"Baz.Cat",
|
||||
},
|
||||
{
|
||||
"/foo/bar/baz/cat/car",
|
||||
"foo.bar.baz",
|
||||
"Cat.Car",
|
||||
},
|
||||
{
|
||||
"/foo/fooBar/bazCat",
|
||||
"foo",
|
||||
"FooBar.BazCat",
|
||||
},
|
||||
{
|
||||
"/v1/foo/bar",
|
||||
"v1.foo",
|
||||
"Foo.Bar",
|
||||
},
|
||||
{
|
||||
"/v1/foo/bar/baz",
|
||||
"v1.foo",
|
||||
"Bar.Baz",
|
||||
},
|
||||
{
|
||||
"/v1/foo/bar/baz/cat",
|
||||
"v1.foo.bar",
|
||||
"Baz.Cat",
|
||||
},
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
s, m := apiRoute(d.path)
|
||||
if d.service != s {
|
||||
t.Fatalf("Expected service: %s for path: %s got: %s %s", d.service, d.path, s, m)
|
||||
}
|
||||
if d.method != m {
|
||||
t.Fatalf("Expected service: %s for path: %s got: %s", d.method, d.path, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyRoute(t *testing.T) {
|
||||
testData := []struct {
|
||||
path string
|
||||
service string
|
||||
}{
|
||||
// no namespace
|
||||
{
|
||||
"/f",
|
||||
"f",
|
||||
},
|
||||
{
|
||||
"/f",
|
||||
"f",
|
||||
},
|
||||
{
|
||||
"/f-b",
|
||||
"f-b",
|
||||
},
|
||||
{
|
||||
"/foo/bar",
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
"/foo-bar",
|
||||
"foo-bar",
|
||||
},
|
||||
{
|
||||
"/foo-bar-baz",
|
||||
"foo-bar-baz",
|
||||
},
|
||||
{
|
||||
"/foo/bar/bar",
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
"/v1/foo/bar",
|
||||
"v1.foo",
|
||||
},
|
||||
{
|
||||
"/v1/foo/bar/baz",
|
||||
"v1.foo",
|
||||
},
|
||||
{
|
||||
"/v1/foo/bar/baz/cat",
|
||||
"v1.foo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
s := proxyRoute(d.path)
|
||||
if d.service != s {
|
||||
t.Fatalf("Expected service: %s for path: %s got: %s", d.service, d.path, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package resolver
|
||||
|
||||
// NewOptions returns new initialised options
|
||||
func NewOptions(opts ...Option) Options {
|
||||
var options Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// WithHandler sets the handler being used
|
||||
func WithHandler(h string) Option {
|
||||
return func(o *Options) {
|
||||
o.Handler = h
|
||||
}
|
||||
}
|
||||
|
||||
// WithNamespace sets the namespace being used
|
||||
func WithNamespace(n string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = n
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Package path resolves using http path
|
||||
package path
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
)
|
||||
|
||||
type Resolver struct{}
|
||||
|
||||
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||
if req.URL.Path == "/" {
|
||||
return nil, errors.New("unknown name")
|
||||
}
|
||||
parts := strings.Split(req.URL.Path[1:], "/")
|
||||
return &resolver.Endpoint{
|
||||
Name: parts[0],
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) String() string {
|
||||
return "path"
|
||||
}
|
||||
|
||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
||||
return &Resolver{}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Package resolver resolves a http request to an endpoint
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Resolver resolves requests to endpoints
|
||||
type Resolver interface {
|
||||
Resolve(r *http.Request) (*Endpoint, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
// Endpoint is the endpoint for a http request
|
||||
type Endpoint struct {
|
||||
// e.g greeter
|
||||
Name string
|
||||
// HTTP Host e.g example.com
|
||||
Host string
|
||||
// HTTP Methods e.g GET, POST
|
||||
Method string
|
||||
// HTTP Path e.g /greeter.
|
||||
Path string
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Handler string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
@@ -1,59 +0,0 @@
|
||||
// Package vpath resolves using http path and recognised versioned urls
|
||||
package vpath
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
)
|
||||
|
||||
type Resolver struct{}
|
||||
|
||||
var (
|
||||
re = regexp.MustCompile("^v[0-9]+$")
|
||||
)
|
||||
|
||||
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
|
||||
if req.URL.Path == "/" {
|
||||
return nil, errors.New("unknown name")
|
||||
}
|
||||
|
||||
parts := strings.Split(req.URL.Path[1:], "/")
|
||||
|
||||
if len(parts) == 1 {
|
||||
return &resolver.Endpoint{
|
||||
Name: parts[0],
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// /v1/foo
|
||||
if re.MatchString(parts[0]) {
|
||||
return &resolver.Endpoint{
|
||||
Name: parts[1],
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &resolver.Endpoint{
|
||||
Name: parts[0],
|
||||
Host: req.Host,
|
||||
Method: req.Method,
|
||||
Path: req.URL.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) String() string {
|
||||
return "path"
|
||||
}
|
||||
|
||||
func NewResolver(opts ...resolver.Option) resolver.Resolver {
|
||||
return &Resolver{}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/micro/go-micro/v2/api/resolver"
|
||||
"github.com/micro/go-micro/v2/api/resolver/micro"
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Namespace string
|
||||
Handler string
|
||||
Registry registry.Registry
|
||||
Resolver resolver.Resolver
|
||||
}
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
func NewOptions(opts ...Option) Options {
|
||||
options := Options{
|
||||
Handler: "meta",
|
||||
Registry: registry.DefaultRegistry,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if options.Resolver == nil {
|
||||
options.Resolver = micro.NewResolver(
|
||||
resolver.WithHandler(options.Handler),
|
||||
resolver.WithNamespace(options.Namespace),
|
||||
)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func WithHandler(h string) Option {
|
||||
return func(o *Options) {
|
||||
o.Handler = h
|
||||
}
|
||||
}
|
||||
|
||||
func WithNamespace(ns string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = ns
|
||||
}
|
||||
}
|
||||
|
||||
func WithRegistry(r registry.Registry) Option {
|
||||
return func(o *Options) {
|
||||
o.Registry = r
|
||||
}
|
||||
}
|
||||
|
||||
func WithResolver(r resolver.Resolver) Option {
|
||||
return func(o *Options) {
|
||||
o.Resolver = r
|
||||
}
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
// Package registry provides a dynamic api service router
|
||||
package registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
"github.com/micro/go-micro/v2/api/router"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
"github.com/micro/go-micro/v2/registry"
|
||||
"github.com/micro/go-micro/v2/registry/cache"
|
||||
)
|
||||
|
||||
// router is the default router
|
||||
type registryRouter struct {
|
||||
exit chan bool
|
||||
opts router.Options
|
||||
|
||||
// registry cache
|
||||
rc cache.Cache
|
||||
|
||||
sync.RWMutex
|
||||
eps map[string]*api.Service
|
||||
}
|
||||
|
||||
func setNamespace(ns, name string) string {
|
||||
ns = strings.TrimSpace(ns)
|
||||
name = strings.TrimSpace(name)
|
||||
|
||||
// no namespace
|
||||
if len(ns) == 0 {
|
||||
return name
|
||||
}
|
||||
|
||||
switch {
|
||||
// has - suffix
|
||||
case strings.HasSuffix(ns, "-"):
|
||||
return strings.Replace(ns+name, ".", "-", -1)
|
||||
// has . suffix
|
||||
case strings.HasSuffix(ns, "."):
|
||||
return ns + name
|
||||
}
|
||||
|
||||
// default join .
|
||||
return strings.Join([]string{ns, name}, ".")
|
||||
}
|
||||
|
||||
func (r *registryRouter) isClosed() bool {
|
||||
select {
|
||||
case <-r.exit:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// refresh list of api services
|
||||
func (r *registryRouter) refresh() {
|
||||
var attempts int
|
||||
|
||||
for {
|
||||
services, err := r.opts.Registry.ListServices()
|
||||
if err != nil {
|
||||
attempts++
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Errorf("unable to list services: %v", err)
|
||||
}
|
||||
time.Sleep(time.Duration(attempts) * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
attempts = 0
|
||||
|
||||
// for each service, get service and store endpoints
|
||||
for _, s := range services {
|
||||
// only get services for this namespace
|
||||
if !strings.HasPrefix(s.Name, r.opts.Namespace) {
|
||||
continue
|
||||
}
|
||||
service, err := r.rc.GetService(s.Name)
|
||||
if err != nil {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Errorf("unable to get service: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
r.store(service)
|
||||
}
|
||||
|
||||
// refresh list in 10 minutes... cruft
|
||||
select {
|
||||
case <-time.After(time.Minute * 10):
|
||||
case <-r.exit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process watch event
|
||||
func (r *registryRouter) process(res *registry.Result) {
|
||||
// skip these things
|
||||
if res == nil || res.Service == nil || !strings.HasPrefix(res.Service.Name, r.opts.Namespace) {
|
||||
return
|
||||
}
|
||||
|
||||
// get entry from cache
|
||||
service, err := r.rc.GetService(res.Service.Name)
|
||||
if err != nil {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Errorf("unable to get service: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// update our local endpoints
|
||||
r.store(service)
|
||||
}
|
||||
|
||||
// store local endpoint cache
|
||||
func (r *registryRouter) store(services []*registry.Service) {
|
||||
// endpoints
|
||||
eps := map[string]*api.Service{}
|
||||
|
||||
// services
|
||||
names := map[string]bool{}
|
||||
|
||||
// create a new endpoint mapping
|
||||
for _, service := range services {
|
||||
// set names we need later
|
||||
names[service.Name] = true
|
||||
|
||||
// map per endpoint
|
||||
for _, endpoint := range service.Endpoints {
|
||||
// create a key service:endpoint_name
|
||||
key := fmt.Sprintf("%s:%s", service.Name, endpoint.Name)
|
||||
// decode endpoint
|
||||
end := api.Decode(endpoint.Metadata)
|
||||
|
||||
// if we got nothing skip
|
||||
if err := api.Validate(end); err != nil {
|
||||
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
|
||||
logger.Tracef("endpoint validation failed: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// try get endpoint
|
||||
ep, ok := eps[key]
|
||||
if !ok {
|
||||
ep = &api.Service{Name: service.Name}
|
||||
}
|
||||
|
||||
// overwrite the endpoint
|
||||
ep.Endpoint = end
|
||||
// append services
|
||||
ep.Services = append(ep.Services, service)
|
||||
// store it
|
||||
eps[key] = ep
|
||||
}
|
||||
}
|
||||
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
// delete any existing eps for services we know
|
||||
for key, service := range r.eps {
|
||||
// skip what we don't care about
|
||||
if !names[service.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
// ok we know this thing
|
||||
// delete delete delete
|
||||
delete(r.eps, key)
|
||||
}
|
||||
|
||||
// now set the eps we have
|
||||
for name, endpoint := range eps {
|
||||
r.eps[name] = endpoint
|
||||
}
|
||||
}
|
||||
|
||||
// watch for endpoint changes
|
||||
func (r *registryRouter) watch() {
|
||||
var attempts int
|
||||
|
||||
for {
|
||||
if r.isClosed() {
|
||||
return
|
||||
}
|
||||
|
||||
// watch for changes
|
||||
w, err := r.opts.Registry.Watch()
|
||||
if err != nil {
|
||||
attempts++
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Errorf("error watching endpoints: %v", err)
|
||||
}
|
||||
time.Sleep(time.Duration(attempts) * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
ch := make(chan bool)
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-ch:
|
||||
w.Stop()
|
||||
case <-r.exit:
|
||||
w.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
// reset if we get here
|
||||
attempts = 0
|
||||
|
||||
for {
|
||||
// process next event
|
||||
res, err := w.Next()
|
||||
if err != nil {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Errorf("error getting next endoint: %v", err)
|
||||
}
|
||||
close(ch)
|
||||
break
|
||||
}
|
||||
r.process(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registryRouter) Options() router.Options {
|
||||
return r.opts
|
||||
}
|
||||
|
||||
func (r *registryRouter) Close() error {
|
||||
select {
|
||||
case <-r.exit:
|
||||
return nil
|
||||
default:
|
||||
close(r.exit)
|
||||
r.rc.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registryRouter) Register(ep *api.Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registryRouter) Deregister(ep *api.Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registryRouter) Endpoint(req *http.Request) (*api.Service, error) {
|
||||
if r.isClosed() {
|
||||
return nil, errors.New("router closed")
|
||||
}
|
||||
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
|
||||
// use the first match
|
||||
// TODO: weighted matching
|
||||
for _, e := range r.eps {
|
||||
ep := e.Endpoint
|
||||
|
||||
// match
|
||||
var pathMatch, hostMatch, methodMatch bool
|
||||
|
||||
// 1. try method GET, POST, PUT, etc
|
||||
// 2. try host example.com, foobar.com, etc
|
||||
// 3. try path /foo/bar, /bar/baz, etc
|
||||
|
||||
// 1. try match method
|
||||
for _, m := range ep.Method {
|
||||
if req.Method == m {
|
||||
methodMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// no match on method pass
|
||||
if len(ep.Method) > 0 && !methodMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. try match host
|
||||
for _, h := range ep.Host {
|
||||
if req.Host == h {
|
||||
hostMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// no match on host pass
|
||||
if len(ep.Host) > 0 && !hostMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. try match paths
|
||||
for _, p := range ep.Path {
|
||||
re, err := regexp.CompilePOSIX(p)
|
||||
if err == nil && re.MatchString(req.URL.Path) {
|
||||
pathMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// no match pass
|
||||
if len(ep.Path) > 0 && !pathMatch {
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: Percentage traffic
|
||||
|
||||
// we got here, so its a match
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// no match
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
|
||||
func (r *registryRouter) Route(req *http.Request) (*api.Service, error) {
|
||||
if r.isClosed() {
|
||||
return nil, errors.New("router closed")
|
||||
}
|
||||
|
||||
// try get an endpoint
|
||||
ep, err := r.Endpoint(req)
|
||||
if err == nil {
|
||||
return ep, nil
|
||||
}
|
||||
|
||||
// error not nil
|
||||
// ignore that shit
|
||||
// TODO: don't ignore that shit
|
||||
|
||||
// get the service name
|
||||
rp, err := r.opts.Resolver.Resolve(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// service name
|
||||
name := setNamespace(r.opts.Namespace, rp.Name)
|
||||
|
||||
// get service
|
||||
services, err := r.rc.GetService(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// only use endpoint matching when the meta handler is set aka api.Default
|
||||
switch r.opts.Handler {
|
||||
// rpc handlers
|
||||
case "meta", "api", "rpc":
|
||||
handler := r.opts.Handler
|
||||
|
||||
// set default handler to api
|
||||
if r.opts.Handler == "meta" {
|
||||
handler = "rpc"
|
||||
}
|
||||
|
||||
// construct api service
|
||||
return &api.Service{
|
||||
Name: name,
|
||||
Endpoint: &api.Endpoint{
|
||||
Name: rp.Method,
|
||||
Handler: handler,
|
||||
},
|
||||
Services: services,
|
||||
}, nil
|
||||
// http handler
|
||||
case "http", "proxy", "web":
|
||||
// construct api service
|
||||
return &api.Service{
|
||||
Name: name,
|
||||
Endpoint: &api.Endpoint{
|
||||
Name: req.URL.String(),
|
||||
Handler: r.opts.Handler,
|
||||
Host: []string{req.Host},
|
||||
Method: []string{req.Method},
|
||||
Path: []string{req.URL.Path},
|
||||
},
|
||||
Services: services,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown handler")
|
||||
}
|
||||
|
||||
func newRouter(opts ...router.Option) *registryRouter {
|
||||
options := router.NewOptions(opts...)
|
||||
r := ®istryRouter{
|
||||
exit: make(chan bool),
|
||||
opts: options,
|
||||
rc: cache.New(options.Registry),
|
||||
eps: make(map[string]*api.Service),
|
||||
}
|
||||
go r.watch()
|
||||
go r.refresh()
|
||||
return r
|
||||
}
|
||||
|
||||
// NewRouter returns the default router
|
||||
func NewRouter(opts ...router.Option) router.Router {
|
||||
return newRouter(opts...)
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
)
|
||||
|
||||
func TestSetNamespace(t *testing.T) {
|
||||
testCases := []struct {
|
||||
namespace string
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
// default dotted path
|
||||
{
|
||||
"go.micro.api",
|
||||
"foo",
|
||||
"go.micro.api.foo",
|
||||
},
|
||||
// dotted end
|
||||
{
|
||||
"go.micro.api.",
|
||||
"foo",
|
||||
"go.micro.api.foo",
|
||||
},
|
||||
// dashed end
|
||||
{
|
||||
"go-micro-api-",
|
||||
"foo",
|
||||
"go-micro-api-foo",
|
||||
},
|
||||
// no namespace
|
||||
{
|
||||
"",
|
||||
"foo",
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
"go-micro-api-",
|
||||
"v2.foo",
|
||||
"go-micro-api-v2-foo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
name := setNamespace(test.namespace, test.name)
|
||||
if name != test.expected {
|
||||
t.Fatalf("expected name %s got %s", test.expected, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter(t *testing.T) {
|
||||
r := newRouter()
|
||||
|
||||
compare := func(expect, got []string) bool {
|
||||
// no data to compare, return true
|
||||
if len(expect) == 0 && len(got) == 0 {
|
||||
return true
|
||||
}
|
||||
// no data expected but got some return false
|
||||
if len(expect) == 0 && len(got) > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// compare expected with what we got
|
||||
for _, e := range expect {
|
||||
var seen bool
|
||||
for _, g := range got {
|
||||
if e == g {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// we're done, return true
|
||||
return true
|
||||
}
|
||||
|
||||
testData := []struct {
|
||||
e *api.Endpoint
|
||||
r *http.Request
|
||||
m bool
|
||||
}{
|
||||
{
|
||||
e: &api.Endpoint{
|
||||
Name: "Foo.Bar",
|
||||
Host: []string{"example.com"},
|
||||
Method: []string{"GET"},
|
||||
Path: []string{"/foo"},
|
||||
},
|
||||
r: &http.Request{
|
||||
Host: "example.com",
|
||||
Method: "GET",
|
||||
URL: &url.URL{
|
||||
Path: "/foo",
|
||||
},
|
||||
},
|
||||
m: true,
|
||||
},
|
||||
{
|
||||
e: &api.Endpoint{
|
||||
Name: "Bar.Baz",
|
||||
Host: []string{"example.com", "foo.com"},
|
||||
Method: []string{"GET", "POST"},
|
||||
Path: []string{"/foo/bar"},
|
||||
},
|
||||
r: &http.Request{
|
||||
Host: "foo.com",
|
||||
Method: "POST",
|
||||
URL: &url.URL{
|
||||
Path: "/foo/bar",
|
||||
},
|
||||
},
|
||||
m: true,
|
||||
},
|
||||
{
|
||||
e: &api.Endpoint{
|
||||
Name: "Test.Cruft",
|
||||
Host: []string{"example.com", "foo.com"},
|
||||
Method: []string{"GET", "POST"},
|
||||
Path: []string{"/xyz"},
|
||||
},
|
||||
r: &http.Request{
|
||||
Host: "fail.com",
|
||||
Method: "DELETE",
|
||||
URL: &url.URL{
|
||||
Path: "/test/fail",
|
||||
},
|
||||
},
|
||||
m: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
key := fmt.Sprintf("%s:%s", "test.service", d.e.Name)
|
||||
r.eps[key] = &api.Service{
|
||||
Endpoint: d.e,
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
e, err := r.Endpoint(d.r)
|
||||
if d.m && err != nil {
|
||||
t.Fatalf("expected match, got %v", err)
|
||||
}
|
||||
if !d.m && err == nil {
|
||||
t.Fatal("expected error got match")
|
||||
}
|
||||
// skip testing the non match
|
||||
if !d.m {
|
||||
continue
|
||||
}
|
||||
|
||||
ep := e.Endpoint
|
||||
|
||||
// test the match
|
||||
if d.e.Name != ep.Name {
|
||||
t.Fatalf("expected %v got %v", d.e.Name, ep.Name)
|
||||
}
|
||||
if ok := compare(d.e.Method, ep.Method); !ok {
|
||||
t.Fatalf("expected %v got %v", d.e.Method, ep.Method)
|
||||
}
|
||||
if ok := compare(d.e.Path, ep.Path); !ok {
|
||||
t.Fatalf("expected %v got %v", d.e.Path, ep.Path)
|
||||
}
|
||||
if ok := compare(d.e.Host, ep.Host); !ok {
|
||||
t.Fatalf("expected %v got %v", d.e.Host, ep.Host)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Package router provides api service routing
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
)
|
||||
|
||||
// Router is used to determine an endpoint for a request
|
||||
type Router interface {
|
||||
// Returns options
|
||||
Options() Options
|
||||
// Stop the router
|
||||
Close() error
|
||||
// Endpoint returns an api.Service endpoint or an error if it does not exist
|
||||
Endpoint(r *http.Request) (*api.Service, error)
|
||||
// Register endpoint in router
|
||||
Register(ep *api.Endpoint) error
|
||||
// Deregister endpoint from router
|
||||
Deregister(ep *api.Endpoint) error
|
||||
// Route returns an api.Service route
|
||||
Route(r *http.Request) (*api.Service, error)
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
package static
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway/httprule"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/runtime"
|
||||
"github.com/micro/go-micro/v2/api"
|
||||
"github.com/micro/go-micro/v2/api/router"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
"github.com/micro/go-micro/v2/metadata"
|
||||
)
|
||||
|
||||
type endpoint struct {
|
||||
apiep *api.Endpoint
|
||||
hostregs []*regexp.Regexp
|
||||
pathregs []runtime.Pattern
|
||||
}
|
||||
|
||||
// router is the default router
|
||||
type staticRouter struct {
|
||||
exit chan bool
|
||||
opts router.Options
|
||||
sync.RWMutex
|
||||
eps map[string]*endpoint
|
||||
}
|
||||
|
||||
func (r *staticRouter) isClosed() bool {
|
||||
select {
|
||||
case <-r.exit:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// watch for endpoint changes
|
||||
func (r *staticRouter) watch() {
|
||||
var attempts int
|
||||
|
||||
for {
|
||||
if r.isClosed() {
|
||||
return
|
||||
}
|
||||
|
||||
// watch for changes
|
||||
w, err := r.opts.Registry.Watch()
|
||||
if err != nil {
|
||||
attempts++
|
||||
log.Println("Error watching endpoints", err)
|
||||
time.Sleep(time.Duration(attempts) * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
ch := make(chan bool)
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-ch:
|
||||
w.Stop()
|
||||
case <-r.exit:
|
||||
w.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
// reset if we get here
|
||||
attempts = 0
|
||||
|
||||
for {
|
||||
// process next event
|
||||
res, err := w.Next()
|
||||
if err != nil {
|
||||
log.Println("Error getting next endpoint", err)
|
||||
close(ch)
|
||||
break
|
||||
}
|
||||
r.process(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func (r *staticRouter) Register(ep *api.Endpoint) error {
|
||||
if err := api.Validate(ep); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pathregs []runtime.Pattern
|
||||
var hostregs []*regexp.Regexp
|
||||
|
||||
for _, h := range ep.Host {
|
||||
if h == "" || h == "*" {
|
||||
continue
|
||||
}
|
||||
hostreg, err := regexp.CompilePOSIX(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostregs = append(hostregs, hostreg)
|
||||
}
|
||||
|
||||
for _, p := range ep.Path {
|
||||
rule, err := httprule.Parse(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tpl := rule.Compile()
|
||||
pathreg, err := runtime.NewPattern(tpl.Version, tpl.OpCodes, tpl.Pool, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pathregs = append(pathregs, pathreg)
|
||||
}
|
||||
|
||||
r.Lock()
|
||||
r.eps[ep.Name] = &endpoint{apiep: ep, pathregs: pathregs, hostregs: hostregs}
|
||||
r.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *staticRouter) Deregister(ep *api.Endpoint) error {
|
||||
if err := api.Validate(ep); err != nil {
|
||||
return err
|
||||
}
|
||||
r.Lock()
|
||||
delete(r.eps, ep.Name)
|
||||
r.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *staticRouter) Options() router.Options {
|
||||
return r.opts
|
||||
}
|
||||
|
||||
func (r *staticRouter) Close() error {
|
||||
select {
|
||||
case <-r.exit:
|
||||
return nil
|
||||
default:
|
||||
close(r.exit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *staticRouter) Endpoint(req *http.Request) (*api.Service, error) {
|
||||
ep, err := r.endpoint(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
epf := strings.Split(ep.apiep.Name, ".")
|
||||
services, err := r.opts.Registry.GetService(epf[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// hack for stream endpoint
|
||||
if ep.apiep.Stream {
|
||||
for _, svc := range services {
|
||||
for _, e := range svc.Endpoints {
|
||||
e.Name = strings.Join(epf[1:], ".")
|
||||
e.Metadata = make(map[string]string)
|
||||
e.Metadata["stream"] = "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svc := &api.Service{
|
||||
Name: epf[0],
|
||||
Endpoint: &api.Endpoint{
|
||||
Name: strings.Join(epf[1:], "."),
|
||||
Handler: "rpc",
|
||||
Host: ep.apiep.Host,
|
||||
Method: ep.apiep.Method,
|
||||
Path: ep.apiep.Path,
|
||||
},
|
||||
Services: services,
|
||||
}
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func (r *staticRouter) endpoint(req *http.Request) (*endpoint, error) {
|
||||
if r.isClosed() {
|
||||
return nil, errors.New("router closed")
|
||||
}
|
||||
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
|
||||
var idx int
|
||||
if len(req.URL.Path) > 0 && req.URL.Path != "/" {
|
||||
idx = 1
|
||||
}
|
||||
path := strings.Split(req.URL.Path[idx:], "/")
|
||||
// use the first match
|
||||
// TODO: weighted matching
|
||||
|
||||
for _, ep := range r.eps {
|
||||
var mMatch, hMatch, pMatch bool
|
||||
|
||||
// 1. try method
|
||||
methodLoop:
|
||||
for _, m := range ep.apiep.Method {
|
||||
if m == req.Method {
|
||||
mMatch = true
|
||||
break methodLoop
|
||||
}
|
||||
}
|
||||
if !mMatch {
|
||||
continue
|
||||
}
|
||||
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
|
||||
logger.Debugf("api method match %s", req.Method)
|
||||
}
|
||||
|
||||
// 2. try host
|
||||
if len(ep.apiep.Host) == 0 {
|
||||
hMatch = true
|
||||
} else {
|
||||
hostLoop:
|
||||
for idx, h := range ep.apiep.Host {
|
||||
if h == "" || h == "*" {
|
||||
hMatch = true
|
||||
break hostLoop
|
||||
} else {
|
||||
if ep.hostregs[idx].MatchString(req.URL.Host) {
|
||||
hMatch = true
|
||||
break hostLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hMatch {
|
||||
continue
|
||||
}
|
||||
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
|
||||
logger.Debugf("api host match %s", req.URL.Host)
|
||||
}
|
||||
|
||||
// 3. try path
|
||||
pathLoop:
|
||||
for _, pathreg := range ep.pathregs {
|
||||
matches, err := pathreg.Match(path, "")
|
||||
if err != nil {
|
||||
// TODO: log error
|
||||
continue
|
||||
}
|
||||
pMatch = true
|
||||
ctx := req.Context()
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
md = make(metadata.Metadata)
|
||||
}
|
||||
for k, v := range matches {
|
||||
md[fmt.Sprintf("x-api-field-%s", k)] = v
|
||||
}
|
||||
*req = *req.WithContext(context.WithValue(ctx, metadata.MetadataKey{}, md))
|
||||
break pathLoop
|
||||
}
|
||||
if !pMatch {
|
||||
continue
|
||||
}
|
||||
// TODO: Percentage traffic
|
||||
|
||||
// we got here, so its a match
|
||||
return ep, nil
|
||||
}
|
||||
// no match
|
||||
return nil, fmt.Errorf("endpoint not found for %v", req)
|
||||
}
|
||||
|
||||
func (r *staticRouter) Route(req *http.Request) (*api.Service, error) {
|
||||
if r.isClosed() {
|
||||
return nil, errors.New("router closed")
|
||||
}
|
||||
|
||||
// try get an endpoint
|
||||
ep, err := r.Endpoint(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ep, nil
|
||||
}
|
||||
|
||||
func NewRouter(opts ...router.Option) *staticRouter {
|
||||
options := router.NewOptions(opts...)
|
||||
r := &staticRouter{
|
||||
exit: make(chan bool),
|
||||
opts: options,
|
||||
eps: make(map[string]*endpoint),
|
||||
}
|
||||
//go r.watch()
|
||||
//go r.refresh()
|
||||
return r
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Package acme abstracts away various ACME libraries
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrProviderNotImplemented can be returned when attempting to
|
||||
// instantiate an unimplemented provider
|
||||
ErrProviderNotImplemented = errors.New("Provider not implemented")
|
||||
)
|
||||
|
||||
// Provider is a ACME provider interface
|
||||
type Provider interface {
|
||||
// Listen returns a new listener
|
||||
Listen(...string) (net.Listener, error)
|
||||
// TLSConfig returns a tls config
|
||||
TLSConfig(...string) (*tls.Config, error)
|
||||
}
|
||||
|
||||
// The Let's Encrypt ACME endpoints
|
||||
const (
|
||||
LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
)
|
||||
@@ -1,46 +0,0 @@
|
||||
// Package autocert is the ACME provider from golang.org/x/crypto/acme/autocert
|
||||
// This provider does not take any config.
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/server/acme"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
// autoCertACME is the ACME provider from golang.org/x/crypto/acme/autocert
|
||||
type autocertProvider struct{}
|
||||
|
||||
// Listen implements acme.Provider
|
||||
func (a *autocertProvider) Listen(hosts ...string) (net.Listener, error) {
|
||||
return autocert.NewListener(hosts...), nil
|
||||
}
|
||||
|
||||
// TLSConfig returns a new tls config
|
||||
func (a *autocertProvider) TLSConfig(hosts ...string) (*tls.Config, error) {
|
||||
// create a new manager
|
||||
m := &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
}
|
||||
if len(hosts) > 0 {
|
||||
m.HostPolicy = autocert.HostWhitelist(hosts...)
|
||||
}
|
||||
dir := cacheDir()
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
if logger.V(logger.InfoLevel, logger.DefaultLogger) {
|
||||
logger.Infof("warning: autocert not using a cache: %v", err)
|
||||
}
|
||||
} else {
|
||||
m.Cache = autocert.DirCache(dir)
|
||||
}
|
||||
return m.TLSConfig(), nil
|
||||
}
|
||||
|
||||
// New returns an autocert acme.Provider
|
||||
func NewProvider() acme.Provider {
|
||||
return &autocertProvider{}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAutocert(t *testing.T) {
|
||||
l := NewProvider()
|
||||
if _, ok := l.(*autocertProvider); !ok {
|
||||
t.Error("NewProvider() didn't return an autocertProvider")
|
||||
}
|
||||
// TODO: Travis CI doesn't let us bind :443
|
||||
// if _, err := l.NewListener(); err != nil {
|
||||
// t.Error(err.Error())
|
||||
// }
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func homeDir() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
||||
}
|
||||
if h := os.Getenv("HOME"); h != "" {
|
||||
return h
|
||||
}
|
||||
return "/"
|
||||
}
|
||||
|
||||
func cacheDir() string {
|
||||
const base = "golang-autocert"
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return filepath.Join(homeDir(), "Library", "Caches", base)
|
||||
case "windows":
|
||||
for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} {
|
||||
if v := os.Getenv(ev); v != "" {
|
||||
return filepath.Join(v, base)
|
||||
}
|
||||
}
|
||||
// Worst case:
|
||||
return filepath.Join(homeDir(), base)
|
||||
}
|
||||
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
|
||||
return filepath.Join(xdg, base)
|
||||
}
|
||||
return filepath.Join(homeDir(), ".cache", base)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// Package certmagic is the ACME provider from github.com/mholt/certmagic
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"math/rand"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/certmagic"
|
||||
"github.com/micro/go-micro/v2/api/server/acme"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
)
|
||||
|
||||
type certmagicProvider struct {
|
||||
opts acme.Options
|
||||
}
|
||||
|
||||
// TODO: set self-contained options
|
||||
func (c *certmagicProvider) setup() {
|
||||
certmagic.Default.CA = c.opts.CA
|
||||
if c.opts.ChallengeProvider != nil {
|
||||
// Enabling DNS Challenge disables the other challenges
|
||||
certmagic.Default.DNSProvider = c.opts.ChallengeProvider
|
||||
}
|
||||
if c.opts.OnDemand {
|
||||
certmagic.Default.OnDemand = new(certmagic.OnDemandConfig)
|
||||
}
|
||||
if c.opts.Cache != nil {
|
||||
// already validated by new()
|
||||
certmagic.Default.Storage = c.opts.Cache.(certmagic.Storage)
|
||||
}
|
||||
// If multiple instances of the provider are running, inject some
|
||||
// randomness so they don't collide
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
randomDuration := (7 * 24 * time.Hour) + (time.Duration(rand.Intn(504)) * time.Hour)
|
||||
certmagic.Default.RenewDurationBefore = randomDuration
|
||||
}
|
||||
|
||||
func (c *certmagicProvider) Listen(hosts ...string) (net.Listener, error) {
|
||||
c.setup()
|
||||
return certmagic.Listen(hosts)
|
||||
}
|
||||
|
||||
func (c *certmagicProvider) TLSConfig(hosts ...string) (*tls.Config, error) {
|
||||
c.setup()
|
||||
return certmagic.TLS(hosts)
|
||||
}
|
||||
|
||||
// NewProvider returns a certmagic provider
|
||||
func NewProvider(options ...acme.Option) acme.Provider {
|
||||
opts := acme.DefaultOptions()
|
||||
|
||||
for _, o := range options {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
if opts.Cache != nil {
|
||||
if _, ok := opts.Cache.(certmagic.Storage); !ok {
|
||||
logger.Fatal("ACME: cache provided doesn't implement certmagic's Storage interface")
|
||||
}
|
||||
}
|
||||
|
||||
return &certmagicProvider{
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v3/providers/dns/cloudflare"
|
||||
"github.com/mholt/certmagic"
|
||||
"github.com/micro/go-micro/v2/api/server/acme"
|
||||
cfstore "github.com/micro/go-micro/v2/store/cloudflare"
|
||||
"github.com/micro/go-micro/v2/sync/lock/memory"
|
||||
)
|
||||
|
||||
func TestCertMagic(t *testing.T) {
|
||||
if len(os.Getenv("IN_TRAVIS_CI")) != 0 {
|
||||
t.Skip("Travis doesn't let us bind :443")
|
||||
}
|
||||
l, err := NewProvider().Listen()
|
||||
if err != nil {
|
||||
if _, ok := err.(*net.OpError); ok {
|
||||
t.Skip("Run under non privileged user")
|
||||
}
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
l.Close()
|
||||
|
||||
c := cloudflare.NewDefaultConfig()
|
||||
c.AuthEmail = ""
|
||||
c.AuthKey = ""
|
||||
c.AuthToken = "test"
|
||||
c.ZoneToken = "test"
|
||||
|
||||
p, err := cloudflare.NewDNSProviderConfig(c)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
l, err = NewProvider(acme.AcceptToS(true),
|
||||
acme.CA(acme.LetsEncryptStagingCA),
|
||||
acme.ChallengeProvider(p),
|
||||
).Listen()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
l.Close()
|
||||
}
|
||||
|
||||
func TestStorageImplementation(t *testing.T) {
|
||||
apiToken, accountID := os.Getenv("CF_API_TOKEN"), os.Getenv("CF_ACCOUNT_ID")
|
||||
kvID := os.Getenv("KV_NAMESPACE_ID")
|
||||
if len(apiToken) == 0 || len(accountID) == 0 || len(kvID) == 0 {
|
||||
t.Skip("No Cloudflare API keys available, skipping test")
|
||||
}
|
||||
|
||||
var s certmagic.Storage
|
||||
st := cfstore.NewStore(
|
||||
cfstore.Token(apiToken),
|
||||
cfstore.Account(accountID),
|
||||
cfstore.Namespace(kvID),
|
||||
)
|
||||
s = &storage{
|
||||
lock: memory.NewLock(),
|
||||
store: st,
|
||||
}
|
||||
|
||||
// Test Lock
|
||||
if err := s.Lock("test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test Unlock
|
||||
if err := s.Unlock("test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test data
|
||||
testdata := []struct {
|
||||
key string
|
||||
value []byte
|
||||
}{
|
||||
{key: "/foo/a", value: []byte("lorem")},
|
||||
{key: "/foo/b", value: []byte("ipsum")},
|
||||
{key: "/foo/c", value: []byte("dolor")},
|
||||
{key: "/foo/d", value: []byte("sit")},
|
||||
{key: "/bar/a", value: []byte("amet")},
|
||||
{key: "/bar/b", value: []byte("consectetur")},
|
||||
{key: "/bar/c", value: []byte("adipiscing")},
|
||||
{key: "/bar/d", value: []byte("elit")},
|
||||
{key: "/foo/bar/a", value: []byte("sed")},
|
||||
{key: "/foo/bar/b", value: []byte("do")},
|
||||
{key: "/foo/bar/c", value: []byte("eiusmod")},
|
||||
{key: "/foo/bar/d", value: []byte("tempor")},
|
||||
{key: "/foo/bar/baz/a", value: []byte("incididunt")},
|
||||
{key: "/foo/bar/baz/b", value: []byte("ut")},
|
||||
{key: "/foo/bar/baz/c", value: []byte("labore")},
|
||||
{key: "/foo/bar/baz/d", value: []byte("et")},
|
||||
// a duplicate just in case there's any edge cases
|
||||
{key: "/foo/a", value: []byte("lorem")},
|
||||
}
|
||||
|
||||
// Test Store
|
||||
for _, d := range testdata {
|
||||
if err := s.Store(d.key, d.value); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Test Load
|
||||
for _, d := range testdata {
|
||||
if value, err := s.Load(d.key); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
} else {
|
||||
if !reflect.DeepEqual(value, d.value) {
|
||||
t.Fatalf("Load %s: expected %v, got %v", d.key, d.value, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test Exists
|
||||
for _, d := range testdata {
|
||||
if !s.Exists(d.key) {
|
||||
t.Fatalf("%s should exist, but doesn't\n", d.key)
|
||||
}
|
||||
}
|
||||
|
||||
// Test List
|
||||
if list, err := s.List("/", true); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
} else {
|
||||
var expected []string
|
||||
for i, d := range testdata {
|
||||
if i != len(testdata)-1 {
|
||||
// Don't store the intentionally duplicated key
|
||||
expected = append(expected, d.key)
|
||||
}
|
||||
}
|
||||
sort.Strings(expected)
|
||||
sort.Strings(list)
|
||||
if !reflect.DeepEqual(expected, list) {
|
||||
t.Fatalf("List: Expected %v, got %v\n", expected, list)
|
||||
}
|
||||
}
|
||||
if list, err := s.List("/foo", false); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
} else {
|
||||
sort.Strings(list)
|
||||
expected := []string{"/foo/a", "/foo/b", "/foo/bar", "/foo/c", "/foo/d"}
|
||||
if !reflect.DeepEqual(expected, list) {
|
||||
t.Fatalf("List: expected %s, got %s\n", expected, list)
|
||||
}
|
||||
}
|
||||
|
||||
// Test Stat
|
||||
for _, d := range testdata {
|
||||
info, err := s.Stat(d.key)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
} else {
|
||||
if info.Key != d.key {
|
||||
t.Fatalf("Stat().Key: expected %s, got %s\n", d.key, info.Key)
|
||||
}
|
||||
if info.Size != int64(len(d.value)) {
|
||||
t.Fatalf("Stat().Size: expected %d, got %d\n", len(d.value), info.Size)
|
||||
}
|
||||
if time.Since(info.Modified) > time.Minute {
|
||||
t.Fatalf("Stat().Modified: expected time since last modified to be < 1 minute, got %v\n", time.Since(info.Modified))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test Delete
|
||||
for _, d := range testdata {
|
||||
if err := s.Delete(d.key); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// New interface doesn't return an error, so call it in case any log.Fatal
|
||||
// happens
|
||||
NewProvider(acme.Cache(s))
|
||||
}
|
||||
|
||||
// Full test with a real zone, with against LE staging
|
||||
func TestE2e(t *testing.T) {
|
||||
apiToken, accountID := os.Getenv("CF_API_TOKEN"), os.Getenv("CF_ACCOUNT_ID")
|
||||
kvID := os.Getenv("KV_NAMESPACE_ID")
|
||||
if len(apiToken) == 0 || len(accountID) == 0 || len(kvID) == 0 {
|
||||
t.Skip("No Cloudflare API keys available, skipping test")
|
||||
}
|
||||
|
||||
testLock := memory.NewLock()
|
||||
testStore := cfstore.NewStore(
|
||||
cfstore.Token(apiToken),
|
||||
cfstore.Account(accountID),
|
||||
cfstore.Namespace(kvID),
|
||||
)
|
||||
testStorage := NewStorage(testLock, testStore)
|
||||
|
||||
conf := cloudflare.NewDefaultConfig()
|
||||
conf.AuthToken = apiToken
|
||||
conf.ZoneToken = apiToken
|
||||
testChallengeProvider, err := cloudflare.NewDNSProviderConfig(conf)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
testProvider := NewProvider(
|
||||
acme.AcceptToS(true),
|
||||
acme.Cache(testStorage),
|
||||
acme.CA(acme.LetsEncryptStagingCA),
|
||||
acme.ChallengeProvider(testChallengeProvider),
|
||||
acme.OnDemand(false),
|
||||
)
|
||||
|
||||
listener, err := testProvider.Listen("*.micro.mu", "micro.mu")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
go http.Serve(listener, http.NotFoundHandler())
|
||||
time.Sleep(10 * time.Minute)
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package certmagic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/certmagic"
|
||||
"github.com/micro/go-micro/v2/store"
|
||||
"github.com/micro/go-micro/v2/sync/lock"
|
||||
)
|
||||
|
||||
// File represents a "File" that will be stored in store.Store - the contents and last modified time
|
||||
type File struct {
|
||||
// last modified time
|
||||
LastModified time.Time
|
||||
// Contents
|
||||
Contents []byte
|
||||
}
|
||||
|
||||
// storage is an implementation of certmagic.Storage using micro's sync.Map and store.Store interfaces.
|
||||
// As certmagic storage expects a filesystem (with stat() abilities) we have to implement
|
||||
// the bare minimum of metadata.
|
||||
type storage struct {
|
||||
lock lock.Lock
|
||||
store store.Store
|
||||
}
|
||||
|
||||
func (s *storage) Lock(key string) error {
|
||||
return s.lock.Acquire(key, lock.TTL(10*time.Minute))
|
||||
}
|
||||
|
||||
func (s *storage) Unlock(key string) error {
|
||||
return s.lock.Release(key)
|
||||
}
|
||||
|
||||
func (s *storage) Store(key string, value []byte) error {
|
||||
f := File{
|
||||
LastModified: time.Now(),
|
||||
Contents: value,
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
e := gob.NewEncoder(buf)
|
||||
if err := e.Encode(f); err != nil {
|
||||
return err
|
||||
}
|
||||
r := &store.Record{
|
||||
Key: key,
|
||||
Value: buf.Bytes(),
|
||||
}
|
||||
return s.store.Write(r)
|
||||
}
|
||||
|
||||
func (s *storage) Load(key string) ([]byte, error) {
|
||||
if !s.Exists(key) {
|
||||
return nil, certmagic.ErrNotExist(errors.New(key + " doesn't exist"))
|
||||
}
|
||||
records, err := s.store.Read(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(records) != 1 {
|
||||
return nil, fmt.Errorf("ACME Storage: multiple records matched key %s", key)
|
||||
}
|
||||
b := bytes.NewBuffer(records[0].Value)
|
||||
d := gob.NewDecoder(b)
|
||||
var f File
|
||||
err = d.Decode(&f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.Contents, nil
|
||||
}
|
||||
|
||||
func (s *storage) Delete(key string) error {
|
||||
return s.store.Delete(key)
|
||||
}
|
||||
|
||||
func (s *storage) Exists(key string) bool {
|
||||
if _, err := s.store.Read(key); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *storage) List(prefix string, recursive bool) ([]string, error) {
|
||||
keys, err := s.store.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//nolint:prealloc
|
||||
var results []string
|
||||
for _, k := range keys {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
results = append(results, k)
|
||||
}
|
||||
}
|
||||
if recursive {
|
||||
return results, nil
|
||||
}
|
||||
keysMap := make(map[string]bool)
|
||||
for _, key := range results {
|
||||
dir := strings.Split(strings.TrimPrefix(key, prefix+"/"), "/")
|
||||
keysMap[dir[0]] = true
|
||||
}
|
||||
results = make([]string, 0)
|
||||
for k := range keysMap {
|
||||
results = append(results, path.Join(prefix, k))
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s *storage) Stat(key string) (certmagic.KeyInfo, error) {
|
||||
records, err := s.store.Read(key)
|
||||
if err != nil {
|
||||
return certmagic.KeyInfo{}, err
|
||||
}
|
||||
if len(records) != 1 {
|
||||
return certmagic.KeyInfo{}, fmt.Errorf("ACME Storage: multiple records matched key %s", key)
|
||||
}
|
||||
b := bytes.NewBuffer(records[0].Value)
|
||||
d := gob.NewDecoder(b)
|
||||
var f File
|
||||
err = d.Decode(&f)
|
||||
if err != nil {
|
||||
return certmagic.KeyInfo{}, err
|
||||
}
|
||||
return certmagic.KeyInfo{
|
||||
Key: key,
|
||||
Modified: f.LastModified,
|
||||
Size: int64(len(f.Contents)),
|
||||
IsTerminal: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewStorage returns a certmagic.Storage backed by a go-micro/lock and go-micro/store
|
||||
func NewStorage(lock lock.Lock, store store.Store) certmagic.Storage {
|
||||
return &storage{
|
||||
lock: lock,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package acme
|
||||
|
||||
import "github.com/go-acme/lego/v3/challenge"
|
||||
|
||||
// Option (or Options) are passed to New() to configure providers
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options represents various options you can present to ACME providers
|
||||
type Options struct {
|
||||
// AcceptTLS must be set to true to indicate that you have read your
|
||||
// provider's terms of service.
|
||||
AcceptToS bool
|
||||
// CA is the CA to use
|
||||
CA string
|
||||
// ChallengeProvider is a go-acme/lego challenge provider. Set this if you
|
||||
// want to use DNS Challenges. Otherwise, tls-alpn-01 will be used
|
||||
ChallengeProvider challenge.Provider
|
||||
// Issue certificates for domains on demand. Otherwise, certs will be
|
||||
// retrieved / issued on start-up.
|
||||
OnDemand bool
|
||||
// Cache is a storage interface. Most ACME libraries have an cache, but
|
||||
// there's no defined interface, so if you consume this option
|
||||
// sanity check it before using.
|
||||
Cache interface{}
|
||||
}
|
||||
|
||||
// AcceptToS indicates whether you accept your CA's terms of service
|
||||
func AcceptToS(b bool) Option {
|
||||
return func(o *Options) {
|
||||
o.AcceptToS = b
|
||||
}
|
||||
}
|
||||
|
||||
// CA sets the CA of an acme.Options
|
||||
func CA(CA string) Option {
|
||||
return func(o *Options) {
|
||||
o.CA = CA
|
||||
}
|
||||
}
|
||||
|
||||
// ChallengeProvider sets the Challenge provider of an acme.Options
|
||||
// if set, it enables the DNS challenge, otherwise tls-alpn-01 will be used.
|
||||
func ChallengeProvider(p challenge.Provider) Option {
|
||||
return func(o *Options) {
|
||||
o.ChallengeProvider = p
|
||||
}
|
||||
}
|
||||
|
||||
// OnDemand enables on-demand certificate issuance. Not recommended for use
|
||||
// with the DNS challenge, as the first connection may be very slow.
|
||||
func OnDemand(b bool) Option {
|
||||
return func(o *Options) {
|
||||
o.OnDemand = b
|
||||
}
|
||||
}
|
||||
|
||||
// Cache provides a cache / storage interface to the underlying ACME library
|
||||
// as there is no standard, this needs to be validated by the underlying
|
||||
// implentation.
|
||||
func Cache(c interface{}) Option {
|
||||
return func(o *Options) {
|
||||
o.Cache = c
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultOptions uses the Let's Encrypt Production CA, with DNS Challenge disabled.
|
||||
func DefaultOptions() Options {
|
||||
return Options{
|
||||
AcceptToS: true,
|
||||
CA: LetsEncryptProductionCA,
|
||||
OnDemand: true,
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/micro/go-micro/v2/auth"
|
||||
)
|
||||
|
||||
// CombinedAuthHandler wraps a server and authenticates requests
|
||||
func CombinedAuthHandler(h http.Handler) http.Handler {
|
||||
return authHandler{
|
||||
handler: h,
|
||||
auth: auth.DefaultAuth,
|
||||
}
|
||||
}
|
||||
|
||||
type authHandler struct {
|
||||
handler http.Handler
|
||||
auth auth.Auth
|
||||
}
|
||||
|
||||
func (h authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Extract the token from the request
|
||||
var token string
|
||||
if header := req.Header.Get("Authorization"); len(header) > 0 {
|
||||
// Extract the auth token from the request
|
||||
if strings.HasPrefix(header, auth.BearerScheme) {
|
||||
token = header[len(auth.BearerScheme):]
|
||||
}
|
||||
} else {
|
||||
// Get the token out the cookies if not provided in headers
|
||||
if c, err := req.Cookie("micro-token"); err == nil && c != nil {
|
||||
token = strings.TrimPrefix(c.Value, auth.TokenCookieName+"=")
|
||||
req.Header.Set("Authorization", auth.BearerScheme+token)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the account using the token, fallback to a blank account
|
||||
// since some endpoints can be unauthenticated, so the lack of an
|
||||
// account doesn't necesserially mean a forbidden request
|
||||
acc, err := h.auth.Inspect(token)
|
||||
if err != nil {
|
||||
acc = &auth.Account{}
|
||||
}
|
||||
err = h.auth.Verify(acc, &auth.Resource{
|
||||
Type: "service",
|
||||
Name: "go.micro.web",
|
||||
Endpoint: req.URL.Path,
|
||||
})
|
||||
|
||||
// The account has the necessary permissions to access the
|
||||
// resource
|
||||
if err == nil {
|
||||
h.handler.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// The account is set, but they don't have enough permissions,
|
||||
// hence we 403.
|
||||
if len(acc.ID) > 0 {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// If there is no auth login url set, 401
|
||||
loginURL := h.auth.Options().LoginURL
|
||||
if loginURL == "" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to the login path
|
||||
params := url.Values{"redirect_to": {req.URL.Path}}
|
||||
loginWithRedirect := fmt.Sprintf("%v?%v", loginURL, params.Encode())
|
||||
http.Redirect(w, req, loginWithRedirect, http.StatusTemporaryRedirect)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package cors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CombinedCORSHandler wraps a server and provides CORS headers
|
||||
func CombinedCORSHandler(h http.Handler) http.Handler {
|
||||
return corsHandler{h}
|
||||
}
|
||||
|
||||
type corsHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (c corsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
SetHeaders(w, r)
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
c.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// SetHeaders sets the CORS headers
|
||||
func SetHeaders(w http.ResponseWriter, r *http.Request) {
|
||||
set := func(w http.ResponseWriter, k, v string) {
|
||||
if v := w.Header().Get(k); len(v) > 0 {
|
||||
return
|
||||
}
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
if origin := r.Header.Get("Origin"); len(origin) > 0 {
|
||||
set(w, "Access-Control-Allow-Origin", origin)
|
||||
} else {
|
||||
set(w, "Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
|
||||
set(w, "Access-Control-Allow-Credentials", "true")
|
||||
set(w, "Access-Control-Allow-Methods", "POST, PATCH, GET, OPTIONS, PUT, DELETE")
|
||||
set(w, "Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// Package http provides a http server with features; acme, cors, etc
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/server/auth"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/micro/go-micro/v2/api/server"
|
||||
"github.com/micro/go-micro/v2/api/server/cors"
|
||||
"github.com/micro/go-micro/v2/logger"
|
||||
)
|
||||
|
||||
type httpServer struct {
|
||||
mux *http.ServeMux
|
||||
opts server.Options
|
||||
|
||||
mtx sync.RWMutex
|
||||
address string
|
||||
exit chan chan error
|
||||
}
|
||||
|
||||
func NewServer(address string) server.Server {
|
||||
return &httpServer{
|
||||
opts: server.Options{},
|
||||
mux: http.NewServeMux(),
|
||||
address: address,
|
||||
exit: make(chan chan error),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *httpServer) Address() string {
|
||||
s.mtx.RLock()
|
||||
defer s.mtx.RUnlock()
|
||||
return s.address
|
||||
}
|
||||
|
||||
func (s *httpServer) Init(opts ...server.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&s.opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *httpServer) Handle(path string, handler http.Handler) {
|
||||
h := handlers.CombinedLoggingHandler(os.Stdout, handler)
|
||||
h = auth.CombinedAuthHandler(handler)
|
||||
|
||||
if s.opts.EnableCORS {
|
||||
h = cors.CombinedCORSHandler(h)
|
||||
}
|
||||
|
||||
s.mux.Handle(path, h)
|
||||
}
|
||||
|
||||
func (s *httpServer) Start() error {
|
||||
var l net.Listener
|
||||
var err error
|
||||
|
||||
if s.opts.EnableACME && s.opts.ACMEProvider != nil {
|
||||
// should we check the address to make sure its using :443?
|
||||
l, err = s.opts.ACMEProvider.Listen(s.opts.ACMEHosts...)
|
||||
} else if s.opts.EnableTLS && s.opts.TLSConfig != nil {
|
||||
l, err = tls.Listen("tcp", s.address, s.opts.TLSConfig)
|
||||
} else {
|
||||
// otherwise plain listen
|
||||
l, err = net.Listen("tcp", s.address)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if logger.V(logger.InfoLevel, logger.DefaultLogger) {
|
||||
logger.Infof("HTTP API Listening on %s", l.Addr().String())
|
||||
}
|
||||
|
||||
s.mtx.Lock()
|
||||
s.address = l.Addr().String()
|
||||
s.mtx.Unlock()
|
||||
|
||||
go func() {
|
||||
if err := http.Serve(l, s.mux); err != nil {
|
||||
// temporary fix
|
||||
//logger.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ch := <-s.exit
|
||||
ch <- l.Close()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *httpServer) Stop() error {
|
||||
ch := make(chan error)
|
||||
s.exit <- ch
|
||||
return <-ch
|
||||
}
|
||||
|
||||
func (s *httpServer) String() string {
|
||||
return "http"
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPServer(t *testing.T) {
|
||||
testResponse := "hello world"
|
||||
|
||||
s := NewServer("localhost:0")
|
||||
|
||||
s.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, testResponse)
|
||||
}))
|
||||
|
||||
if err := s.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rsp, err := http.Get(fmt.Sprintf("http://%s/", s.Address()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rsp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(b) != testResponse {
|
||||
t.Fatalf("Unexpected response, got %s, expected %s", string(b), testResponse)
|
||||
}
|
||||
|
||||
if err := s.Stop(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/micro/go-micro/v2/api/server/acme"
|
||||
)
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
type Options struct {
|
||||
EnableACME bool
|
||||
EnableCORS bool
|
||||
ACMEProvider acme.Provider
|
||||
EnableTLS bool
|
||||
ACMEHosts []string
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
func EnableCORS(b bool) Option {
|
||||
return func(o *Options) {
|
||||
o.EnableCORS = b
|
||||
}
|
||||
}
|
||||
|
||||
func EnableACME(b bool) Option {
|
||||
return func(o *Options) {
|
||||
o.EnableACME = b
|
||||
}
|
||||
}
|
||||
|
||||
func ACMEHosts(hosts ...string) Option {
|
||||
return func(o *Options) {
|
||||
o.ACMEHosts = hosts
|
||||
}
|
||||
}
|
||||
|
||||
func ACMEProvider(p acme.Provider) Option {
|
||||
return func(o *Options) {
|
||||
o.ACMEProvider = p
|
||||
}
|
||||
}
|
||||
|
||||
func EnableTLS(b bool) Option {
|
||||
return func(o *Options) {
|
||||
o.EnableTLS = b
|
||||
}
|
||||
}
|
||||
|
||||
func TLSConfig(t *tls.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.TLSConfig = t
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Package server provides an API gateway server which handles inbound requests
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Server serves api requests
|
||||
type Server interface {
|
||||
Address() string
|
||||
Init(opts ...Option) error
|
||||
Handle(path string, handler http.Handler)
|
||||
Start() error
|
||||
Stop() error
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// source: api/service/proto/api.proto
|
||||
|
||||
package go_micro_api
|
||||
|
||||
import (
|
||||
context "context"
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
math "math"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
|
||||
|
||||
type Endpoint struct {
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Host []string `protobuf:"bytes,2,rep,name=host,proto3" json:"host,omitempty"`
|
||||
Path []string `protobuf:"bytes,3,rep,name=path,proto3" json:"path,omitempty"`
|
||||
Method []string `protobuf:"bytes,4,rep,name=method,proto3" json:"method,omitempty"`
|
||||
Stream bool `protobuf:"varint,5,opt,name=stream,proto3" json:"stream,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Endpoint) Reset() { *m = Endpoint{} }
|
||||
func (m *Endpoint) String() string { return proto.CompactTextString(m) }
|
||||
func (*Endpoint) ProtoMessage() {}
|
||||
func (*Endpoint) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_c4a48b6b680b5c31, []int{0}
|
||||
}
|
||||
|
||||
func (m *Endpoint) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_Endpoint.Unmarshal(m, b)
|
||||
}
|
||||
func (m *Endpoint) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_Endpoint.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *Endpoint) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_Endpoint.Merge(m, src)
|
||||
}
|
||||
func (m *Endpoint) XXX_Size() int {
|
||||
return xxx_messageInfo_Endpoint.Size(m)
|
||||
}
|
||||
func (m *Endpoint) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_Endpoint.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_Endpoint proto.InternalMessageInfo
|
||||
|
||||
func (m *Endpoint) GetName() string {
|
||||
if m != nil {
|
||||
return m.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Endpoint) GetHost() []string {
|
||||
if m != nil {
|
||||
return m.Host
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Endpoint) GetPath() []string {
|
||||
if m != nil {
|
||||
return m.Path
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Endpoint) GetMethod() []string {
|
||||
if m != nil {
|
||||
return m.Method
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Endpoint) GetStream() bool {
|
||||
if m != nil {
|
||||
return m.Stream
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type EmptyResponse struct {
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *EmptyResponse) Reset() { *m = EmptyResponse{} }
|
||||
func (m *EmptyResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*EmptyResponse) ProtoMessage() {}
|
||||
func (*EmptyResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_c4a48b6b680b5c31, []int{1}
|
||||
}
|
||||
|
||||
func (m *EmptyResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_EmptyResponse.Unmarshal(m, b)
|
||||
}
|
||||
func (m *EmptyResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_EmptyResponse.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *EmptyResponse) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_EmptyResponse.Merge(m, src)
|
||||
}
|
||||
func (m *EmptyResponse) XXX_Size() int {
|
||||
return xxx_messageInfo_EmptyResponse.Size(m)
|
||||
}
|
||||
func (m *EmptyResponse) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_EmptyResponse.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_EmptyResponse proto.InternalMessageInfo
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*Endpoint)(nil), "go.micro.api.Endpoint")
|
||||
proto.RegisterType((*EmptyResponse)(nil), "go.micro.api.EmptyResponse")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("api/service/proto/api.proto", fileDescriptor_c4a48b6b680b5c31) }
|
||||
|
||||
var fileDescriptor_c4a48b6b680b5c31 = []byte{
|
||||
// 212 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0xd0, 0xc1, 0x4a, 0x03, 0x31,
|
||||
0x10, 0x80, 0x61, 0xd7, 0xad, 0x65, 0x1d, 0x14, 0x21, 0x87, 0x12, 0xec, 0x65, 0xd9, 0x53, 0x4f,
|
||||
0x59, 0xd0, 0x27, 0x28, 0xda, 0x17, 0xd8, 0x37, 0x88, 0xed, 0xd0, 0x9d, 0x43, 0x32, 0x43, 0x32,
|
||||
0x14, 0x7c, 0x08, 0xdf, 0x59, 0x12, 0x2b, 0x2c, 0x5e, 0xbd, 0xfd, 0xf3, 0x1d, 0x86, 0x61, 0x60,
|
||||
0xeb, 0x85, 0xc6, 0x8c, 0xe9, 0x42, 0x47, 0x1c, 0x25, 0xb1, 0xf2, 0xe8, 0x85, 0x5c, 0x2d, 0xf3,
|
||||
0x70, 0x66, 0x17, 0xe8, 0x98, 0xd8, 0x79, 0xa1, 0xe1, 0x02, 0xdd, 0x21, 0x9e, 0x84, 0x29, 0xaa,
|
||||
0x31, 0xb0, 0x8a, 0x3e, 0xa0, 0x6d, 0xfa, 0x66, 0x77, 0x3f, 0xd5, 0x2e, 0x36, 0x73, 0x56, 0x7b,
|
||||
0xdb, 0xb7, 0xc5, 0x4a, 0x17, 0x13, 0xaf, 0xb3, 0x6d, 0x7f, 0xac, 0xb4, 0xd9, 0xc0, 0x3a, 0xa0,
|
||||
0xce, 0x7c, 0xb2, 0xab, 0xaa, 0xd7, 0xa9, 0x78, 0xd6, 0x84, 0x3e, 0xd8, 0xbb, 0xbe, 0xd9, 0x75,
|
||||
0xd3, 0x75, 0x1a, 0x9e, 0xe0, 0xf1, 0x10, 0x44, 0x3f, 0x27, 0xcc, 0xc2, 0x31, 0xe3, 0xcb, 0x57,
|
||||
0x03, 0xed, 0x5e, 0xc8, 0xec, 0xa1, 0x9b, 0xf0, 0x4c, 0x59, 0x31, 0x99, 0x8d, 0x5b, 0xde, 0xea,
|
||||
0x7e, 0x0f, 0x7d, 0xde, 0xfe, 0xf1, 0xe5, 0xa2, 0xe1, 0xc6, 0xbc, 0x01, 0xbc, 0x63, 0xfa, 0xdf,
|
||||
0x92, 0x8f, 0x75, 0xfd, 0xd6, 0xeb, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0x46, 0x62, 0x67, 0x30,
|
||||
0x4c, 0x01, 0x00, 0x00,
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ context.Context
|
||||
var _ grpc.ClientConn
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
const _ = grpc.SupportPackageIsVersion4
|
||||
|
||||
// ApiClient is the client API for Api service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
|
||||
type ApiClient interface {
|
||||
Register(ctx context.Context, in *Endpoint, opts ...grpc.CallOption) (*EmptyResponse, error)
|
||||
Deregister(ctx context.Context, in *Endpoint, opts ...grpc.CallOption) (*EmptyResponse, error)
|
||||
}
|
||||
|
||||
type apiClient struct {
|
||||
cc *grpc.ClientConn
|
||||
}
|
||||
|
||||
func NewApiClient(cc *grpc.ClientConn) ApiClient {
|
||||
return &apiClient{cc}
|
||||
}
|
||||
|
||||
func (c *apiClient) Register(ctx context.Context, in *Endpoint, opts ...grpc.CallOption) (*EmptyResponse, error) {
|
||||
out := new(EmptyResponse)
|
||||
err := c.cc.Invoke(ctx, "/go.micro.api.Api/Register", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) Deregister(ctx context.Context, in *Endpoint, opts ...grpc.CallOption) (*EmptyResponse, error) {
|
||||
out := new(EmptyResponse)
|
||||
err := c.cc.Invoke(ctx, "/go.micro.api.Api/Deregister", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ApiServer is the server API for Api service.
|
||||
type ApiServer interface {
|
||||
Register(context.Context, *Endpoint) (*EmptyResponse, error)
|
||||
Deregister(context.Context, *Endpoint) (*EmptyResponse, error)
|
||||
}
|
||||
|
||||
// UnimplementedApiServer can be embedded to have forward compatible implementations.
|
||||
type UnimplementedApiServer struct {
|
||||
}
|
||||
|
||||
func (*UnimplementedApiServer) Register(ctx context.Context, req *Endpoint) (*EmptyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Register not implemented")
|
||||
}
|
||||
func (*UnimplementedApiServer) Deregister(ctx context.Context, req *Endpoint) (*EmptyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Deregister not implemented")
|
||||
}
|
||||
|
||||
func RegisterApiServer(s *grpc.Server, srv ApiServer) {
|
||||
s.RegisterService(&_Api_serviceDesc, srv)
|
||||
}
|
||||
|
||||
func _Api_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Endpoint)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).Register(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/go.micro.api.Api/Register",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Register(ctx, req.(*Endpoint))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_Deregister_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Endpoint)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).Deregister(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/go.micro.api.Api/Deregister",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).Deregister(ctx, req.(*Endpoint))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
var _Api_serviceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "go.micro.api.Api",
|
||||
HandlerType: (*ApiServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Register",
|
||||
Handler: _Api_Register_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Deregister",
|
||||
Handler: _Api_Deregister_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "api/service/proto/api.proto",
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
// Code generated by protoc-gen-micro. DO NOT EDIT.
|
||||
// source: api/service/proto/api.proto
|
||||
|
||||
package go_micro_api
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
math "math"
|
||||
)
|
||||
|
||||
import (
|
||||
context "context"
|
||||
client "github.com/micro/go-micro/v2/client"
|
||||
server "github.com/micro/go-micro/v2/server"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ context.Context
|
||||
var _ client.Option
|
||||
var _ server.Option
|
||||
|
||||
// Client API for Api service
|
||||
|
||||
type ApiService interface {
|
||||
Register(ctx context.Context, in *Endpoint, opts ...client.CallOption) (*EmptyResponse, error)
|
||||
Deregister(ctx context.Context, in *Endpoint, opts ...client.CallOption) (*EmptyResponse, error)
|
||||
}
|
||||
|
||||
type apiService struct {
|
||||
c client.Client
|
||||
name string
|
||||
}
|
||||
|
||||
func NewApiService(name string, c client.Client) ApiService {
|
||||
return &apiService{
|
||||
c: c,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *apiService) Register(ctx context.Context, in *Endpoint, opts ...client.CallOption) (*EmptyResponse, error) {
|
||||
req := c.c.NewRequest(c.name, "Api.Register", in)
|
||||
out := new(EmptyResponse)
|
||||
err := c.c.Call(ctx, req, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiService) Deregister(ctx context.Context, in *Endpoint, opts ...client.CallOption) (*EmptyResponse, error) {
|
||||
req := c.c.NewRequest(c.name, "Api.Deregister", in)
|
||||
out := new(EmptyResponse)
|
||||
err := c.c.Call(ctx, req, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Server API for Api service
|
||||
|
||||
type ApiHandler interface {
|
||||
Register(context.Context, *Endpoint, *EmptyResponse) error
|
||||
Deregister(context.Context, *Endpoint, *EmptyResponse) error
|
||||
}
|
||||
|
||||
func RegisterApiHandler(s server.Server, hdlr ApiHandler, opts ...server.HandlerOption) error {
|
||||
type api interface {
|
||||
Register(ctx context.Context, in *Endpoint, out *EmptyResponse) error
|
||||
Deregister(ctx context.Context, in *Endpoint, out *EmptyResponse) error
|
||||
}
|
||||
type Api struct {
|
||||
api
|
||||
}
|
||||
h := &apiHandler{hdlr}
|
||||
return s.Handle(s.NewHandler(&Api{h}, opts...))
|
||||
}
|
||||
|
||||
type apiHandler struct {
|
||||
ApiHandler
|
||||
}
|
||||
|
||||
func (h *apiHandler) Register(ctx context.Context, in *Endpoint, out *EmptyResponse) error {
|
||||
return h.ApiHandler.Register(ctx, in, out)
|
||||
}
|
||||
|
||||
func (h *apiHandler) Deregister(ctx context.Context, in *Endpoint, out *EmptyResponse) error {
|
||||
return h.ApiHandler.Deregister(ctx, in, out)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package go.micro.api;
|
||||
|
||||
service Api {
|
||||
rpc Register(Endpoint) returns (EmptyResponse) {};
|
||||
rpc Deregister(Endpoint) returns (EmptyResponse) {};
|
||||
}
|
||||
|
||||
message Endpoint {
|
||||
string name = 1;
|
||||
repeated string host = 2;
|
||||
repeated string path = 3;
|
||||
repeated string method = 4;
|
||||
bool stream = 5;
|
||||
}
|
||||
|
||||
message EmptyResponse {}
|
||||
+86
-88
@@ -3,30 +3,27 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
"github.com/micro/go-micro/v2/metadata"
|
||||
const (
|
||||
// BearerScheme used for Authorization header.
|
||||
BearerScheme = "Bearer "
|
||||
// ScopePublic is the scope applied to a rule to allow access to the public.
|
||||
ScopePublic = ""
|
||||
// ScopeAccount is the scope applied to a rule to limit to users with any valid account.
|
||||
ScopeAccount = "*"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFound is returned when a resouce cannot be found
|
||||
ErrNotFound = errors.New("not found")
|
||||
// ErrEncodingToken is returned when the service encounters an error during encoding
|
||||
ErrEncodingToken = errors.New("error encoding the token")
|
||||
// ErrInvalidToken is returned when the token provided is not valid
|
||||
// ErrInvalidToken is when the token provided is not valid.
|
||||
ErrInvalidToken = errors.New("invalid token provided")
|
||||
// ErrInvalidRole is returned when the role provided was invalid
|
||||
ErrInvalidRole = errors.New("invalid role")
|
||||
// ErrForbidden is returned when a user does not have the necessary roles to access a resource
|
||||
// ErrForbidden is when a user does not have the necessary scope to access a resource.
|
||||
ErrForbidden = errors.New("resource forbidden")
|
||||
// BearerScheme used for Authorization header
|
||||
BearerScheme = "Bearer "
|
||||
)
|
||||
|
||||
// Auth providers authentication and authorization
|
||||
// Auth provides authentication and authorization.
|
||||
type Auth interface {
|
||||
// Init the auth
|
||||
Init(opts ...Option)
|
||||
@@ -34,106 +31,107 @@ type Auth interface {
|
||||
Options() Options
|
||||
// Generate a new account
|
||||
Generate(id string, opts ...GenerateOption) (*Account, error)
|
||||
// Grant access to a resource
|
||||
Grant(role string, res *Resource) error
|
||||
// Revoke access to a resource
|
||||
Revoke(role string, res *Resource) error
|
||||
// Verify an account has access to a resource
|
||||
Verify(acc *Account, res *Resource) error
|
||||
// Inspect a token
|
||||
Inspect(token string) (*Account, error)
|
||||
// Token generated using an account ID and secret
|
||||
Token(id, secret string, opts ...TokenOption) (*Token, error)
|
||||
// Token generated using refresh token or credentials
|
||||
Token(opts ...TokenOption) (*Token, error)
|
||||
// String returns the name of the implementation
|
||||
String() string
|
||||
}
|
||||
|
||||
// Resource is an entity such as a user or
|
||||
type Resource struct {
|
||||
// Name of the resource
|
||||
Name string
|
||||
// Type of resource, e.g.
|
||||
Type string
|
||||
// Endpoint resource e.g NotesService.Create
|
||||
Endpoint string
|
||||
// Rules manages access to resources.
|
||||
type Rules interface {
|
||||
// Verify an account has access to a resource using the rules
|
||||
Verify(acc *Account, res *Resource, opts ...VerifyOption) error
|
||||
// Grant access to a resource
|
||||
Grant(rule *Rule) error
|
||||
// Revoke access to a resource
|
||||
Revoke(rule *Rule) error
|
||||
// List returns all the rules used to verify requests
|
||||
List(...ListOption) ([]*Rule, error)
|
||||
}
|
||||
|
||||
// Account provided by an auth provider
|
||||
// Account provided by an auth provider.
|
||||
type Account struct {
|
||||
// ID of the account (UUIDV4, email or username)
|
||||
ID string `json:"id"`
|
||||
// Secret used to renew the account
|
||||
Secret string `json:"secret"`
|
||||
// Roles associated with the Account
|
||||
Roles []string `json:"roles"`
|
||||
// Any other associated metadata
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
// Namespace the account belongs to, default blank
|
||||
Namespace string `json:"namespace"`
|
||||
// ID of the account e.g. email
|
||||
ID string `json:"id"`
|
||||
// Type of the account, e.g. service
|
||||
Type string `json:"type"`
|
||||
// Issuer of the account
|
||||
Issuer string `json:"issuer"`
|
||||
// Secret for the account, e.g. the password
|
||||
Secret string `json:"secret"`
|
||||
// Scopes the account has access to
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
|
||||
// Token can be short or long lived
|
||||
// Token can be short or long lived.
|
||||
type Token struct {
|
||||
// The token itself
|
||||
Token string `json:"token"`
|
||||
// Type of token, e.g. JWT
|
||||
Type string `json:"type"`
|
||||
// Time of token creation
|
||||
Created time.Time `json:"created"`
|
||||
// Time of token expiry
|
||||
Expiry time.Time `json:"expiry"`
|
||||
// Subject of the token, e.g. the account ID
|
||||
Subject string `json:"subject"`
|
||||
// Roles granted to the token
|
||||
Roles []string `json:"roles"`
|
||||
// Metadata embedded in the token
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
// Namespace the token belongs to
|
||||
Namespace string `json:"namespace"`
|
||||
// The token to be used for accessing resources
|
||||
AccessToken string `json:"access_token"`
|
||||
// RefreshToken to be used to generate a new token
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// Expired returns a boolean indicating if the token needs to be refreshed.
|
||||
func (t *Token) Expired() bool {
|
||||
return t.Expiry.Unix() < time.Now().Unix()
|
||||
}
|
||||
|
||||
// Resource is an entity such as a user or.
|
||||
type Resource struct {
|
||||
// Name of the resource, e.g. go.micro.service.notes
|
||||
Name string `json:"name"`
|
||||
// Type of resource, e.g. service
|
||||
Type string `json:"type"`
|
||||
// Endpoint resource e.g NotesService.Create
|
||||
Endpoint string `json:"endpoint"`
|
||||
}
|
||||
|
||||
// Access defines the type of access a rule grants.
|
||||
type Access int
|
||||
|
||||
const (
|
||||
// MetadataKey is the key used when storing the account in metadata
|
||||
MetadataKey = "auth-account"
|
||||
// TokenCookieName is the name of the cookie which stores the auth token
|
||||
TokenCookieName = "micro-token"
|
||||
// SecretCookieName is the name of the cookie which stores the auth secret
|
||||
SecretCookieName = "micro-secret"
|
||||
// AccessGranted to a resource.
|
||||
AccessGranted Access = iota
|
||||
// AccessDenied to a resource.
|
||||
AccessDenied
|
||||
)
|
||||
|
||||
// Rule is used to verify access to a resource.
|
||||
type Rule struct {
|
||||
// Resource the rule applies to
|
||||
Resource *Resource
|
||||
// ID of the rule, e.g. "public"
|
||||
ID string
|
||||
// Scope the rule requires, a blank scope indicates open to the public and * indicates the rule
|
||||
// applies to any valid account
|
||||
Scope string
|
||||
// Access determines if the rule grants or denies access to the resource
|
||||
Access Access
|
||||
// Priority the rule should take when verifying a request, the higher the value the sooner the
|
||||
// rule will be applied
|
||||
Priority int32
|
||||
}
|
||||
|
||||
type accountKey struct{}
|
||||
|
||||
// AccountFromContext gets the account from the context, which
|
||||
// is set by the auth wrapper at the start of a call. If the account
|
||||
// is not set, a nil account will be returned. The error is only returned
|
||||
// when there was a problem retrieving an account
|
||||
func AccountFromContext(ctx context.Context) (*Account, error) {
|
||||
str, ok := metadata.Get(ctx, MetadataKey)
|
||||
// there was no account set
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var acc *Account
|
||||
// metadata is stored as a string, so unmarshal to an account
|
||||
if err := json.Unmarshal([]byte(str), &acc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return acc, nil
|
||||
// when there was a problem retrieving an account.
|
||||
func AccountFromContext(ctx context.Context) (*Account, bool) {
|
||||
acc, ok := ctx.Value(accountKey{}).(*Account)
|
||||
return acc, ok
|
||||
}
|
||||
|
||||
// ContextWithAccount sets the account in the context
|
||||
func ContextWithAccount(ctx context.Context, account *Account) (context.Context, error) {
|
||||
// metadata is stored as a string, so marshal to bytes
|
||||
bytes, err := json.Marshal(account)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// generate a new context with the MetadataKey set
|
||||
return metadata.Set(ctx, MetadataKey, string(bytes)), nil
|
||||
}
|
||||
|
||||
// ContextWithToken sets the auth token in the context
|
||||
func ContextWithToken(ctx context.Context, token string) context.Context {
|
||||
return metadata.Set(ctx, "Authorization", fmt.Sprintf("%v%v", BearerScheme, token))
|
||||
// ContextWithAccount sets the account in the context.
|
||||
func ContextWithAccount(ctx context.Context, account *Account) context.Context {
|
||||
return context.WithValue(ctx, accountKey{}, account)
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultAuth = NewAuth()
|
||||
)
|
||||
|
||||
func NewAuth(opts ...Option) Auth {
|
||||
return &noop{}
|
||||
}
|
||||
|
||||
type noop struct {
|
||||
opts Options
|
||||
}
|
||||
|
||||
// String returns the name of the implementation
|
||||
func (n *noop) String() string {
|
||||
return "noop"
|
||||
}
|
||||
|
||||
// Init the auth
|
||||
func (n *noop) Init(opts ...Option) {
|
||||
for _, o := range opts {
|
||||
o(&n.opts)
|
||||
}
|
||||
}
|
||||
|
||||
// Options set for auth
|
||||
func (n *noop) Options() Options {
|
||||
return n.opts
|
||||
}
|
||||
|
||||
// Generate a new account
|
||||
func (n *noop) Generate(id string, opts ...GenerateOption) (*Account, error) {
|
||||
options := NewGenerateOptions(opts...)
|
||||
|
||||
return &Account{
|
||||
ID: id,
|
||||
Roles: options.Roles,
|
||||
Metadata: options.Metadata,
|
||||
Secret: uuid.New().String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Grant access to a resource
|
||||
func (n *noop) Grant(role string, res *Resource) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Revoke access to a resource
|
||||
func (n *noop) Revoke(role string, res *Resource) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify an account has access to a resource
|
||||
func (n *noop) Verify(acc *Account, res *Resource) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Inspect a token
|
||||
func (n *noop) Inspect(token string) (*Account, error) {
|
||||
return &Account{
|
||||
ID: uuid.New().String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Token generation using an account id and secret
|
||||
func (n *noop) Token(id, secret string, opts ...TokenOption) (*Token, error) {
|
||||
return &Token{}, nil
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jwtToken "github.com/micro/plugins/v5/auth/jwt/token"
|
||||
"go-micro.dev/v5/auth"
|
||||
"go-micro.dev/v5/cmd"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmd.DefaultAuths["jwt"] = NewAuth
|
||||
}
|
||||
|
||||
// NewAuth returns a new instance of the Auth service.
|
||||
func NewAuth(opts ...auth.Option) auth.Auth {
|
||||
j := new(jwt)
|
||||
j.Init(opts...)
|
||||
return j
|
||||
}
|
||||
|
||||
func NewRules() auth.Rules {
|
||||
return new(jwtRules)
|
||||
}
|
||||
|
||||
type jwt struct {
|
||||
sync.Mutex
|
||||
options auth.Options
|
||||
jwt jwtToken.Provider
|
||||
}
|
||||
|
||||
type jwtRules struct {
|
||||
sync.Mutex
|
||||
rules []*auth.Rule
|
||||
}
|
||||
|
||||
func (j *jwt) String() string {
|
||||
return "jwt"
|
||||
}
|
||||
|
||||
func (j *jwt) Init(opts ...auth.Option) {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
|
||||
for _, o := range opts {
|
||||
o(&j.options)
|
||||
}
|
||||
|
||||
j.jwt = jwtToken.New(
|
||||
jwtToken.WithPrivateKey(j.options.PrivateKey),
|
||||
jwtToken.WithPublicKey(j.options.PublicKey),
|
||||
)
|
||||
}
|
||||
|
||||
func (j *jwt) Options() auth.Options {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
return j.options
|
||||
}
|
||||
|
||||
func (j *jwt) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) {
|
||||
options := auth.NewGenerateOptions(opts...)
|
||||
account := &auth.Account{
|
||||
ID: id,
|
||||
Type: options.Type,
|
||||
Scopes: options.Scopes,
|
||||
Metadata: options.Metadata,
|
||||
Issuer: j.Options().Namespace,
|
||||
}
|
||||
|
||||
// generate a JWT secret which can be provided to the Token() method
|
||||
// and exchanged for an access token
|
||||
secret, err := j.jwt.Generate(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account.Secret = secret.Token
|
||||
|
||||
// return the account
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (j *jwtRules) Grant(rule *auth.Rule) error {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
j.rules = append(j.rules, rule)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *jwtRules) Revoke(rule *auth.Rule) error {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
|
||||
rules := make([]*auth.Rule, 0, len(j.rules))
|
||||
for _, r := range j.rules {
|
||||
if r.ID != rule.ID {
|
||||
rules = append(rules, r)
|
||||
}
|
||||
}
|
||||
|
||||
j.rules = rules
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *jwtRules) Verify(acc *auth.Account, res *auth.Resource, opts ...auth.VerifyOption) error {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
|
||||
var options auth.VerifyOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return auth.Verify(j.rules, acc, res)
|
||||
}
|
||||
|
||||
func (j *jwtRules) List(opts ...auth.ListOption) ([]*auth.Rule, error) {
|
||||
j.Lock()
|
||||
defer j.Unlock()
|
||||
return j.rules, nil
|
||||
}
|
||||
|
||||
func (j *jwt) Inspect(token string) (*auth.Account, error) {
|
||||
return j.jwt.Inspect(token)
|
||||
}
|
||||
|
||||
func (j *jwt) Token(opts ...auth.TokenOption) (*auth.Token, error) {
|
||||
options := auth.NewTokenOptions(opts...)
|
||||
|
||||
secret := options.RefreshToken
|
||||
if len(options.Secret) > 0 {
|
||||
secret = options.Secret
|
||||
}
|
||||
|
||||
account, err := j.jwt.Inspect(secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
access, err := j.jwt.Generate(account, jwtToken.WithExpiry(options.Expiry))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refresh, err := j.jwt.Generate(account, jwtToken.WithExpiry(options.Expiry+time.Hour))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &auth.Token{
|
||||
Created: access.Created,
|
||||
Expiry: access.Expiry,
|
||||
AccessToken: access.Token,
|
||||
RefreshToken: refresh.Token,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"go-micro.dev/v5/auth"
|
||||
)
|
||||
|
||||
// authClaims to be encoded in the JWT.
|
||||
type authClaims struct {
|
||||
Type string `json:"type"`
|
||||
Scopes []string `json:"scopes"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
// JWT implementation of token provider.
|
||||
type JWT struct {
|
||||
opts Options
|
||||
}
|
||||
|
||||
// New returns an initialized basic provider.
|
||||
func New(opts ...Option) Provider {
|
||||
return &JWT{
|
||||
opts: NewOptions(opts...),
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new JWT.
|
||||
func (j *JWT) Generate(acc *auth.Account, opts ...GenerateOption) (*Token, error) {
|
||||
// decode the private key
|
||||
priv, err := base64.StdEncoding.DecodeString(j.opts.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse the private key
|
||||
key, err := jwt.ParseRSAPrivateKeyFromPEM(priv)
|
||||
if err != nil {
|
||||
return nil, ErrEncodingToken
|
||||
}
|
||||
|
||||
// parse the options
|
||||
options := NewGenerateOptions(opts...)
|
||||
|
||||
// generate the JWT
|
||||
expiry := time.Now().Add(options.Expiry)
|
||||
t := jwt.NewWithClaims(jwt.SigningMethodRS256, authClaims{
|
||||
acc.Type, acc.Scopes, acc.Metadata, jwt.StandardClaims{
|
||||
Subject: acc.ID,
|
||||
Issuer: acc.Issuer,
|
||||
ExpiresAt: expiry.Unix(),
|
||||
},
|
||||
})
|
||||
tok, err := t.SignedString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// return the token
|
||||
return &Token{
|
||||
Token: tok,
|
||||
Expiry: expiry,
|
||||
Created: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Inspect a JWT.
|
||||
func (j *JWT) Inspect(t string) (*auth.Account, error) {
|
||||
// decode the public key
|
||||
pub, err := base64.StdEncoding.DecodeString(j.opts.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse the public key
|
||||
res, err := jwt.ParseWithClaims(t, &authClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwt.ParseRSAPublicKeyFromPEM(pub)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
// validate the token
|
||||
if !res.Valid {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
claims, ok := res.Claims.(*authClaims)
|
||||
if !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
// return the token
|
||||
return &auth.Account{
|
||||
ID: claims.Subject,
|
||||
Issuer: claims.Issuer,
|
||||
Type: claims.Type,
|
||||
Scopes: claims.Scopes,
|
||||
Metadata: claims.Metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// String returns JWT.
|
||||
func (j *JWT) String() string {
|
||||
return "jwt"
|
||||
}
|
||||
@@ -1,55 +1,51 @@
|
||||
package jwt
|
||||
package token
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/micro/go-micro/v2/auth/token"
|
||||
"go-micro.dev/v5/auth"
|
||||
)
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
privKey, err := ioutil.ReadFile("test/sample_key")
|
||||
privKey, err := os.ReadFile("test/sample_key")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read private key: %v", err)
|
||||
}
|
||||
|
||||
j := NewTokenProvider(
|
||||
token.WithPrivateKey(string(privKey)),
|
||||
j := New(
|
||||
WithPrivateKey(string(privKey)),
|
||||
)
|
||||
|
||||
_, err = j.Generate("test")
|
||||
_, err = j.Generate(&auth.Account{ID: "test"})
|
||||
if err != nil {
|
||||
t.Fatalf("Generate returned %v error, expected nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspect(t *testing.T) {
|
||||
pubKey, err := ioutil.ReadFile("test/sample_key.pub")
|
||||
pubKey, err := os.ReadFile("test/sample_key.pub")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read public key: %v", err)
|
||||
}
|
||||
privKey, err := ioutil.ReadFile("test/sample_key")
|
||||
privKey, err := os.ReadFile("test/sample_key")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read private key: %v", err)
|
||||
}
|
||||
|
||||
j := NewTokenProvider(
|
||||
token.WithPublicKey(string(pubKey)),
|
||||
token.WithPrivateKey(string(privKey)),
|
||||
j := New(
|
||||
WithPublicKey(string(pubKey)),
|
||||
WithPrivateKey(string(privKey)),
|
||||
)
|
||||
|
||||
t.Run("Valid token", func(t *testing.T) {
|
||||
md := map[string]string{"foo": "bar"}
|
||||
roles := []string{"admin"}
|
||||
scopes := []string{"admin"}
|
||||
subject := "test"
|
||||
|
||||
opts := []token.GenerateOption{
|
||||
token.WithMetadata(md),
|
||||
token.WithRoles(roles...),
|
||||
}
|
||||
|
||||
tok, err := j.Generate(subject, opts...)
|
||||
acc := &auth.Account{ID: subject, Scopes: scopes, Metadata: md}
|
||||
tok, err := j.Generate(acc)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate returned %v error, expected nil", err)
|
||||
}
|
||||
@@ -58,11 +54,11 @@ func TestInspect(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Inspect returned %v error, expected nil", err)
|
||||
}
|
||||
if tok2.Subject != subject {
|
||||
t.Errorf("Inspect returned %v as the token subject, expected %v", tok2.Subject, subject)
|
||||
if acc.ID != subject {
|
||||
t.Errorf("Inspect returned %v as the token subject, expected %v", acc.ID, subject)
|
||||
}
|
||||
if len(tok2.Roles) != len(roles) {
|
||||
t.Errorf("Inspect returned %v roles, expected %v", len(tok2.Roles), len(roles))
|
||||
if len(tok2.Scopes) != len(scopes) {
|
||||
t.Errorf("Inspect returned %v scopes, expected %v", len(tok2.Scopes), len(scopes))
|
||||
}
|
||||
if len(tok2.Metadata) != len(md) {
|
||||
t.Errorf("Inspect returned %v as the token metadata, expected %v", tok2.Metadata, md)
|
||||
@@ -70,21 +66,20 @@ func TestInspect(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Expired token", func(t *testing.T) {
|
||||
tok, err := j.Generate("foo", token.WithExpiry(-10*time.Second))
|
||||
tok, err := j.Generate(&auth.Account{}, WithExpiry(-10*time.Second))
|
||||
if err != nil {
|
||||
t.Fatalf("Generate returned %v error, expected nil", err)
|
||||
}
|
||||
|
||||
if _, err = j.Inspect(tok.Token); err != token.ErrInvalidToken {
|
||||
t.Fatalf("Inspect returned %v error, expected %v", err, token.ErrInvalidToken)
|
||||
if _, err = j.Inspect(tok.Token); err != ErrInvalidToken {
|
||||
t.Fatalf("Inspect returned %v error, expected %v", err, ErrInvalidToken)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Invalid token", func(t *testing.T) {
|
||||
_, err := j.Inspect("Invalid token")
|
||||
if err != token.ErrInvalidToken {
|
||||
t.Fatalf("Inspect returned %v error, expected %v", err, token.ErrInvalidToken)
|
||||
if err != ErrInvalidToken {
|
||||
t.Fatalf("Inspect returned %v error, expected %v", err, ErrInvalidToken)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package token
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/micro/go-micro/v2/store"
|
||||
"go-micro.dev/v5/store"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
@@ -17,21 +17,21 @@ type Options struct {
|
||||
|
||||
type Option func(o *Options)
|
||||
|
||||
// WithStore sets the token providers store
|
||||
// WithStore sets the token providers store.
|
||||
func WithStore(s store.Store) Option {
|
||||
return func(o *Options) {
|
||||
o.Store = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithPublicKey sets the JWT public key
|
||||
// WithPublicKey sets the JWT public key.
|
||||
func WithPublicKey(key string) Option {
|
||||
return func(o *Options) {
|
||||
o.PublicKey = key
|
||||
}
|
||||
}
|
||||
|
||||
// WithPrivateKey sets the JWT private key
|
||||
// WithPrivateKey sets the JWT private key.
|
||||
func WithPrivateKey(key string) Option {
|
||||
return func(o *Options) {
|
||||
o.PrivateKey = key
|
||||
@@ -43,7 +43,7 @@ func NewOptions(opts ...Option) Options {
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
//set default store
|
||||
// set default store
|
||||
if options.Store == nil {
|
||||
options.Store = store.DefaultStore
|
||||
}
|
||||
@@ -53,51 +53,24 @@ func NewOptions(opts ...Option) Options {
|
||||
type GenerateOptions struct {
|
||||
// Expiry for the token
|
||||
Expiry time.Duration
|
||||
// Metadata associated with the account
|
||||
Metadata map[string]string
|
||||
// Roles/scopes associated with the account
|
||||
Roles []string
|
||||
// Namespace the account belongs too
|
||||
Namespace string
|
||||
}
|
||||
|
||||
type GenerateOption func(o *GenerateOptions)
|
||||
|
||||
// WithExpiry for the generated account's token expires
|
||||
// WithExpiry for the generated account's token expires.
|
||||
func WithExpiry(d time.Duration) GenerateOption {
|
||||
return func(o *GenerateOptions) {
|
||||
o.Expiry = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithMetadata for the token
|
||||
func WithMetadata(md map[string]string) func(o *GenerateOptions) {
|
||||
return func(o *GenerateOptions) {
|
||||
o.Metadata = md
|
||||
}
|
||||
}
|
||||
|
||||
// WithRoles for the token
|
||||
func WithRoles(rs ...string) func(o *GenerateOptions) {
|
||||
return func(o *GenerateOptions) {
|
||||
o.Roles = rs
|
||||
}
|
||||
}
|
||||
|
||||
// WithNamespace for the token
|
||||
func WithNamespace(n string) func(o *GenerateOptions) {
|
||||
return func(o *GenerateOptions) {
|
||||
o.Namespace = n
|
||||
}
|
||||
}
|
||||
|
||||
// NewGenerateOptions from a slice of options
|
||||
// NewGenerateOptions from a slice of options.
|
||||
func NewGenerateOptions(opts ...GenerateOption) GenerateOptions {
|
||||
var options GenerateOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
//set default Expiry of token
|
||||
// set default Expiry of token
|
||||
if options.Expiry == 0 {
|
||||
options.Expiry = time.Minute * 15
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user