mirror of https://github.com/schollz/croc.git
Compare commits
1733 Commits
Author | SHA1 | Date |
---|---|---|
Zack | 50e0f625bc | |
Zack | 94af2374c3 | |
Zack | a4322faa25 | |
Zack | 23dce2aa3e | |
Zack | 88002b322d | |
Zack | 9246408278 | |
Zack | fbf1eeedce | |
Zack | e4c9f2d9fb | |
Zack | c0c3370d9b | |
Zack | f6bd13fa06 | |
Zack Scholl | f83616e9bd | |
Zack Scholl | 23f385ab2f | |
Zack Scholl | 78feb393de | |
Zack | 69fc3cee47 | |
Zack | a5da77cf49 | |
Zack | f66e17dd46 | |
Zack | 63c9201938 | |
Zack | ca7a5979cc | |
Zack | fc457557e0 | |
Zack | f044f4dd86 | |
Zack | a2e71c7e1a | |
Zack | dff34fa7fc | |
Zack Scholl | 08103bb7bb | |
Zack Scholl | 4091ef0496 | |
Zack | 381f8369a3 | |
Zack | a95d67e31c | |
Zack | b0920bbe70 | |
Zack | ed55c746c2 | |
Zack | 8e10eac5c5 | |
Zack | 43f1c53538 | |
Zack | 3acac5d53b | |
Zack | 66f0d1264a | |
Zack | ee713c5146 | |
Zack | 6181903c83 | |
Zack | 7acd2def69 | |
Zack | eb0909033e | |
Zack | f6d862eac0 | |
Zack | 5a6005f1eb | |
Zack | f6633cbac9 | |
Zack | d8ef7cda20 | |
Zack | 7622e636e4 | |
Zack | bb018fd725 | |
Zack | 863dabb93a | |
Zack | 6f5f16aa1c | |
Zack | 0f1ca436cd | |
Zack | 4929635eb8 | |
Zack | 3f12f75fae | |
Zack | e255d472a6 | |
Zack | 2ffd4daeaf | |
Zack | accb310337 | |
Zack | 2b4c088100 | |
Zack | a591833dbf | |
Zack | b3668a6f5c | |
Zack | b05c3c8c42 | |
Zack | 13bc190f8b | |
Zack | 1b90484bb8 | |
Zack Scholl | 05359d6976 | |
Zack Scholl | cc4d74c490 | |
Zack | d81116382f | |
Zack Scholl | 94cc880568 | |
Zack | 24b907f4bb | |
Zack Scholl | 8166b2dbed | |
Zack | 14187f6f30 | |
dependabot[bot] | 90682d3ebd | |
Zack Scholl | f4057aa28b | |
Zack | 3c2548aa69 | |
dependabot[bot] | 7bab9c3cb5 | |
Zack Scholl | 355628f895 | |
Zack | eaffc6dcac | |
a1lu | 4baec420c8 | |
a1lu | cd89e8043f | |
Zack Scholl | 1324ff8897 | |
Zack | 8bc7a62b9e | |
Zack Scholl | 0c49ac3f02 | |
Zack | 8b4ab4c86c | |
Zack Scholl | f8f69e3157 | |
Zack | 4e75e131c4 | |
a1lu | e599e56415 | |
a1lu | 956598c427 | |
a1lu | 618ae1e5d0 | |
Zack Scholl | 7763a971f2 | |
Zack | d2b7c80369 | |
Rahul Garg | 241176d8a4 | |
Zack Scholl | 719f9b62c9 | |
Zack Scholl | 483c5255bb | |
Zack Scholl | 03e6dcd220 | |
Zack Scholl | 22ddbd83c2 | |
Zack Scholl | 6b930c365b | |
Zack | 1f6851f33b | |
Zack | 61224b4e6b | |
Zack Scholl | 6f2771e7b5 | |
Zack | c21beccb7a | |
dependabot[bot] | 40f5e9ca1e | |
bitraid | ed8c0475bf | |
dependabot[bot] | d7d7d3c8dc | |
Zack Scholl | 945ac74690 | |
Zack Scholl | fff6f48001 | |
Zack Scholl | 28ef8e99ac | |
Zack | 28bb36b321 | |
Zack Scholl | ab2cb477a8 | |
Zack | abc5029fce | |
Zack | e7f67ebea7 | |
Zack Scholl | c7f0228786 | |
Zack | 741714504a | |
D. Bohdan | be8a6689ff | |
Zack Scholl | de9c54e57a | |
Zack | 53fc9ebd99 | |
Zack | 4159ba7668 | |
Zack Scholl | 064f84ccd3 | |
Zack | 8ac1e3a501 | |
Zack | 11bc4eecc6 | |
Zack | d92cff92b9 | |
Zack | 00f12b5742 | |
Travis | 4f1f57b1ba | |
Zack | a240a4b982 | |
Zack | 508e0be335 | |
qk-santi | e1644401da | |
qk-santi | c83eb59963 | |
Zack | f874e30151 | |
Zack | b278f5a41d | |
Zack | ce91e3b420 | |
Zack | 30a6b14443 | |
qk-santi | 87152f8706 | |
qk-santi | 48eb2a2a7c | |
dependabot[bot] | 816ad09a50 | |
Zack | a2228e80c2 | |
Zack Scholl | 230de9184d | |
Zack | 3c781069ca | |
Zack | 7274a8bd4b | |
Zack Scholl | 3382306342 | |
dependabot[bot] | ad47739c8f | |
Zack | 2f33b14b3d | |
Zack | 4025efcd48 | |
Zack | f41a2afead | |
Zack | ba75f7badb | |
Zack | 55c954117a | |
Zack | 4d083f8d10 | |
Rehan Mahmood | 9ad2af18bd | |
Zack | 47fc96b98f | |
Rehan Mahmood | d4bba88fb1 | |
Zack Scholl | c1367671b6 | |
Zack Scholl | 1e572067ec | |
Zack Scholl | 8aa9f040f6 | |
Zack Scholl | 96bb34485e | |
Zack | 159f0f8d9b | |
Sandeep | c4d5c89e5f | |
Zack | 6ac67b68fc | |
PThorpe92 | 0af35d7149 | |
Zack | f91c7a9948 | |
Zack | cff8cddd13 | |
zx9597446 | d724f11297 | |
ferebee | 80aabea29b | |
sitiom | f8bb011eac | |
Zack Scholl | ef68dfa54c | |
Zack Scholl | 8ab65d06b5 | |
Zack Scholl | 9c82914e7c | |
Zack | 95717f16c9 | |
glitsj16 | 5a58ae294b | |
Zack Scholl | 0aa5c80393 | |
Zack | 23a8904193 | |
N0mansky | d5e63cd0bf | |
N0mansky | 1b1dc5cdfe | |
Zack | cd1162f85c | |
Alexander Seiler | ad7a22b218 | |
Zack | e3a18cd7a7 | |
Zack | 8611bfa44a | |
Zack Scholl | b3c0625659 | |
dependabot[bot] | 40e5893bb8 | |
Zack | e1b49e6d1c | |
BayLee4 | fae2e81b4a | |
BayLee4 | f5a02df17b | |
dependabot[bot] | d813aa2fa9 | |
Zack | 8d8d06557e | |
Zack Scholl | 3ae46c3c18 | |
Zack Scholl | 1fce28e72f | |
Zack Scholl | ac3caa5564 | |
Zack Scholl | 790ee9e6eb | |
Zack Scholl | 0656bb7851 | |
Zack Scholl | 5ed8204cb1 | |
Zack Scholl | 1220d54a65 | |
Zack Scholl | 642d20b48d | |
Zack | 58bb226ba5 | |
stefins | 080c21b66b | |
stefins | 4944355b75 | |
Zack Scholl | 3c1d60d6d2 | |
Zack Scholl | 5bdd4453cd | |
Zack Scholl | 5b333fc85c | |
Zack Scholl | 90e4e33dcd | |
Zack Scholl | 3135bd74e3 | |
Zack Scholl | 7c98ae3d5a | |
sitiom | 908f33cd61 | |
sitiom | 9eaba7be7c | |
Ryan Caezar Itang | b3db9d88c1 | |
Zack Scholl | 9006cde1b3 | |
Zack | 8f75bb8299 | |
Zack | faf64aadf1 | |
stefins | 65760b1189 | |
stefins | 24679d4e02 | |
Zack Scholl | 8b9977a7c4 | |
Zack | ae71b4b63b | |
Zack Scholl | c462b5948f | |
Zack | a2c1dc6a89 | |
chncaption | 98391bff63 | |
BayLee4 | adadaba75e | |
BayLee4 | 0024beaedc | |
Zack Scholl | 63feece074 | |
Zack Scholl | 9e280d7f48 | |
Zack Scholl | 7c7be95d6b | |
Zack | c20beb1be7 | |
Zack Scholl | e08c5e7f38 | |
Zack Scholl | 3b819346fa | |
Zack Scholl | 2467411bae | |
Zack Scholl | b436b31970 | |
Zack Scholl | e3ce565ca6 | |
Zack Scholl | e04981698c | |
Zack Scholl | 07a67ed54e | |
Zack | 9a8e584aca | |
Tai Groot | fa7ae114f5 | |
Zack | 8a4326bc0d | |
Tai Groot | ae20f2b5ac | |
Zack Scholl | cd6eb1ba53 | |
Zack Scholl | 8454008b45 | |
Zack | fce4629120 | |
Tai Groot | fd0db2ad55 | |
Zack | 7f9688c7a1 | |
IrishMaestro | f1e15a54ee | |
Zack Scholl | 0f7f449d7f | |
Zack Scholl | b27982a742 | |
Zack Scholl | 30f5a3e84e | |
Zack Scholl | 786fc8d34f | |
Zack | 8de4508876 | |
VRDighe | e640e4fce0 | |
VRDighe | 4f8b2aba9b | |
Zack | 1517767129 | |
Zack Scholl | 3dedd41557 | |
Zack | 69f939c0cd | |
Zack | cd5872bd2d | |
cui fliter | 1c6583f925 | |
chavacava | 40ac320261 | |
Zack Scholl | 1851327df7 | |
Zack Scholl | 0e93f1e285 | |
Zack Scholl | 7e0814a57e | |
Zack Scholl | c6bcb79928 | |
Zack | 134673691e | |
Stefin | d226ba536e | |
Stefin | b50fe88474 | |
Stefin | 37ae453ff7 | |
Stefin | ee772c4cec | |
Stefin | ed030375e5 | |
Stefin | ad36e21051 | |
Stefin | 4ea9a96d88 | |
Stefin | f0f9b80bdf | |
Zack | 7a0c0a8200 | |
Zack Scholl | 5270755c15 | |
Zack Scholl | b7e4a73c27 | |
Zack Scholl | da5d19ef28 | |
Zack | c68cfcea8a | |
DasSkelett | 38ed8ecc3c | |
Tadej Janež | a5d3e00f2b | |
Zack Scholl | 9de06a6bf9 | |
Zack Scholl | a6a3a57361 | |
Zack | 148c1a6cdd | |
Zack Scholl | 817905cf5a | |
Zack Scholl | 6158b42ad1 | |
Zack Scholl | 80539f27c3 | |
Zack | 35652e60a3 | |
RCL98 | 2ad8b1f1ce | |
RCL98 | b316c0159f | |
RCL98 | f78ee15605 | |
iulius98 | 15d0209a29 | |
Zack | 386c99d057 | |
Zack Scholl | fd99a405fd | |
Zack Scholl | 54f52e5427 | |
Zack Scholl | ec949888d8 | |
Zack Scholl | 0d52ead66e | |
Zack Scholl | b715993435 | |
Zack Scholl | 7e817e1cbf | |
Zack | 4abdbf1ad2 | |
Zack Scholl | 087c22d833 | |
Zack Scholl | 789b5bf607 | |
Davide Cavalca | e42f81a404 | |
Zack Scholl | 20bf7dd91d | |
Zack Scholl | 4a8c19b115 | |
Zack Scholl | a1a17ce6f7 | |
Zack Scholl | 00c4d248fd | |
Zack Scholl | 073196ccd2 | |
Zack Scholl | 635b362ca0 | |
Zack Scholl | 53f35c1da0 | |
Zack | 030ed2a03d | |
Zack Scholl | c28d731a66 | |
Zack Scholl | ef6683b550 | |
Zack | f6a75c8f53 | |
RCL98 | 7c1a59c102 | |
iulius98 | 32a188fa85 | |
RCL98 | ce8fb796b3 | |
Zack | 1bd705d2e1 | |
iulius98 | 1a635de69c | |
Zack | 353ae0db16 | |
Abhishek | 9f66842322 | |
Zack Scholl | 94c0f66a26 | |
Zack Scholl | b0e7d4d5df | |
Zack Scholl | 9b5252d54c | |
Zack Scholl | 1d3c822ef3 | |
Zack | df78f3333d | |
voocel | b45e625298 | |
Zack Scholl | 7390e7ed45 | |
Zack | f210ef8877 | |
Amey Shrivastava | 2964ede174 | |
Zack Scholl | cc837fa863 | |
Zack Scholl | 87bfdf11a8 | |
Zack | c30492609e | |
Zack | 2f733891db | |
Zack | 3eb66284e0 | |
Eng Zer Jun | 1645759742 | |
Eng Zer Jun | da8beb65bf | |
Hinux Chau | d30433658b | |
Marcel Battista | d372fa1fa4 | |
Marcel Battista | 8ff3fb9b89 | |
Zack | 696479024e | |
Zack | 9dfa0284fa | |
Zack Scholl | a12f374aba | |
Zack | 8ee1825f84 | |
Zack | f99c33d794 | |
Maxim Baz | 417b371454 | |
jolheiser | 2ffb20201c | |
Zack Scholl | 89efd38c10 | |
Zack Scholl | 49f50a53d5 | |
Zack Scholl | 5d9afe03ef | |
Zack Scholl | 1637765aad | |
Zack Scholl | ce628ea604 | |
Zack Scholl | ff4307c3c6 | |
Zack Scholl | e5f59fa0f9 | |
Zack Scholl | 04662df347 | |
Zack Scholl | 4ea66fbd18 | |
Zack Scholl | d96c98f4ce | |
Zack Scholl | 31db6a76e5 | |
Zack Scholl | a996f3c4f4 | |
Zack Scholl | 4334c870f5 | |
Zack Scholl | 2b1367a09b | |
Zack Scholl | add1650cbd | |
Zack Scholl | 04f2dfac97 | |
Zack Scholl | 060ce516b1 | |
Zack Scholl | 78b6f2d858 | |
Zack | d77c83ce09 | |
Zack | a34d29befc | |
Zack | 914d0c98a8 | |
Zack | 55cb35d12b | |
Brandon Ingalls | 860a75fee6 | |
jolheiser | e380c7b1f1 | |
jolheiser | 2381f26c61 | |
KallyDev | de454bbf5a | |
Zack Scholl | e19e06a652 | |
Zack | 6deafaadae | |
Austin Dworaczyk Wiltshire | ef3953a586 | |
Zack Scholl | 1d3fdffed6 | |
Zack Scholl | 94b2aff637 | |
Zack | 9ce2321d01 | |
Charlie Jonas | d75530b78b | |
Charlie Jonas | f64f68d5a9 | |
Charlie Jonas | be7705efc3 | |
Charlie Jonas | df2e29b74d | |
Zack Scholl | b73fdff702 | |
Zack Scholl | 664936feb2 | |
Zack Scholl | 4b1eb6519c | |
Zack Scholl | 86aeed93db | |
Zack Scholl | 7993e73ac2 | |
Zack Scholl | d922808fd8 | |
Zack Scholl | 8d22737fd3 | |
Zack Scholl | d442755814 | |
Zack Scholl | 0afdf71305 | |
Zack Scholl | ecf3eb872e | |
Zack Scholl | 646c879edc | |
Zack Scholl | cbe9cbe32a | |
Zack Scholl | 4796404b7e | |
Zack Scholl | 3e2c88c5bc | |
Zack Scholl | 0124356cd7 | |
Zack | f2ef6f19a5 | |
Niko Köser | 5e0d6522b0 | |
Niko Köser | c72aaf63cb | |
Zack Scholl | a9e77673f4 | |
Zack Scholl | aa93d3e30f | |
Zack Scholl | 15a19e1998 | |
Zack Scholl | e1ccb58d8f | |
Zack Scholl | d0ebb7ad38 | |
Zack Scholl | 2bfbacf972 | |
Zack Scholl | e29b4c47fd | |
Zack Scholl | b4deedf367 | |
Zack Scholl | 68e507dbba | |
Zack Scholl | 66f30c0565 | |
Zack Scholl | 340bafba3d | |
Zack Scholl | e47776bb10 | |
Zack | 422f099e5e | |
Zack Scholl | bd4886ae3c | |
Markus Wamser | d5fd2060dd | |
Zack Scholl | 7c28279a58 | |
Zack Scholl | 27b7cdbf12 | |
Zack Scholl | 24fb8746a4 | |
Zack | 45cb545a82 | |
Alvar Penning | 8f5c73837a | |
Zack | b90f9329ca | |
Craeckie | 968950f783 | |
Zack Scholl | 5b0883e1fe | |
Zack Scholl | 420030998f | |
Zack Scholl | de2bcf114c | |
Zack Scholl | ab5ae5cbb6 | |
Zack Scholl | 94b3dba034 | |
Zack | 1a47543be9 | |
Zack Scholl | 407e162ec2 | |
Zack | 35106d4dbe | |
Zack Scholl | d6af319ad8 | |
Zack Scholl | 85e7576311 | |
Zack Scholl | 7ac7be37af | |
Zack Scholl | 669aeb377a | |
Zack Scholl | b655afb533 | |
Zack Scholl | e9949ce3d7 | |
Zack Scholl | 91a3dd5866 | |
Zack Scholl | 026487a833 | |
Zack Scholl | f8ef096784 | |
Zack Scholl | 0c2089bd4c | |
Zack Scholl | 5ad68b9ced | |
Zack Scholl | 9286b3c965 | |
Zack Scholl | c1edf24338 | |
Zack Scholl | d38fc18390 | |
Zack Scholl | f4cedd1e91 | |
Zack Scholl | fc66c3b91f | |
Zack Scholl | 2ac7d4f31f | |
Zack Scholl | a96762b942 | |
Zack Scholl | 0a0f7a03d7 | |
Zack Scholl | 8148e191ae | |
Zack Scholl | 25a985464a | |
Zack | 333ece706e | |
Zack Scholl | daf10395a3 | |
Zack Scholl | 9231b1a661 | |
Zack Scholl | 3e56d4cdb9 | |
Zack Scholl | cec39ba2ce | |
Zack Scholl | 7a997156ed | |
Zack Scholl | c02b4f1256 | |
Zack Scholl | babfd5f35f | |
Zack Scholl | be5ceae8c7 | |
Zack Scholl | 6be4080f05 | |
Zack Scholl | b0693751c1 | |
Zack Scholl | 8250a39534 | |
Zack Scholl | 2131e99826 | |
Zack Scholl | 628043b228 | |
Zack Scholl | 362a30e5e7 | |
Zack | 828de41d6c | |
Tobias Dussa | 876ce5764b | |
Zack Scholl | 59b287df7f | |
Zack | 3fd125178f | |
Zack | 136faac448 | |
Zack | 85a0bfb898 | |
Missu | 4718f3897c | |
Sting Alleman | 068d5c1031 | |
Zack Scholl | 4dfa1a9236 | |
Zack Scholl | 6ba6ec9d32 | |
Zack Scholl | c373b38b59 | |
Zack Scholl | f2af5b7256 | |
Zack Scholl | 483f840401 | |
Zack Scholl | ff12c33e7f | |
Zack Scholl | e4cecf670b | |
Zack Scholl | 94e0e3ca67 | |
Zack Scholl | a9d582cc6a | |
k3b | ab9396937c | |
Matej Kafka | c1e546ede6 | |
Zack Scholl | 63ba28c9b0 | |
Zack | 6caf72df82 | |
Jona | 2273438373 | |
Zack Scholl | 50a0d86e3d | |
Zack Scholl | 50aa24d86d | |
Zack Scholl | 674bff7a13 | |
Zack Scholl | 757ed180fb | |
Zack Scholl | 76648a2926 | |
Zack Scholl | 0b76edc362 | |
Zack Scholl | c39061c7fc | |
Zack Scholl | fb2723d80b | |
Zack Scholl | f84341a197 | |
Zack | 6e9156d49c | |
Zack | cd6d9a5aa0 | |
Lars Lehtonen | 3e5b876a32 | |
Lars Lehtonen | a9487a332c | |
Zack Scholl | de924f6a4f | |
Zack Scholl | 03a27b6683 | |
Zack Scholl | 0b719b9b77 | |
Zack Scholl | fc34d21cb8 | |
Zack Scholl | bb05d48fe9 | |
Zack | 949480517f | |
Heiko Reese | a20ebd7120 | |
Zack Scholl | d49bb8632b | |
Zack Scholl | 10e57c3f97 | |
Zack Scholl | be95662e35 | |
Zack Scholl | 105fb9a3ce | |
Zack Scholl | 30783ce79c | |
Zack Scholl | a46f09ee1c | |
Zack Scholl | cdabf629b6 | |
Zack Scholl | 40cda1bed7 | |
Zack Scholl | 92ab62d03e | |
Zack Scholl | 31012b8f7d | |
Zack Scholl | 4b0d01d8f0 | |
Zack Scholl | 648eb6c71d | |
Zack Scholl | b19a028aea | |
Zack | 9588f2b4a4 | |
Zack Scholl | cd2802b8b5 | |
Zack Scholl | c5ff55a193 | |
Zack Scholl | 1cf309faf2 | |
Zack | 8dc5bd6e04 | |
leko | 37f09049a4 | |
Zack Scholl | eaa18cd502 | |
Zack Scholl | b9d461e5df | |
Zack Scholl | 89ea0166fa | |
Zack Scholl | 77fd78e408 | |
Zack Scholl | edeb7c8886 | |
Zack Scholl | 4481fe2336 | |
Zack | ef43873c3b | |
Zack Scholl | 0bec0b26ab | |
Zack | 681c824ef4 | |
Zack Scholl | 950b6f83d9 | |
Zack Scholl | 7f5d704a26 | |
Zack | a6a8ab71ce | |
Zack | de56d2f6dd | |
Zack Scholl | 2f96b77b73 | |
Zack | 0548c2e45f | |
Niko Köser | 0b60fef246 | |
Niko Köser | 3ccd4d07e9 | |
Benjamin Wen | b5f1b35213 | |
Spaceface16518 | 90eda5639f | |
Zack Scholl | 77d0c7ae2e | |
Zack | 493eb075f1 | |
Spaceface16518 | 247d2b18ec | |
Spaceface16518 | 6b8a14d831 | |
Amrit | 743b2c1bf4 | |
Peter | 17f3df7e36 | |
Zack Scholl | dab683c9dc | |
Zack | e829ca0ff4 | |
Zack Scholl | e27c32cf6e | |
Zack Scholl | a2b243ad94 | |
Zack Scholl | 05640cd49f | |
Zack Scholl | 037dbbd4e9 | |
Zack | 0994e4eda5 | |
Zack | 235f065a9c | |
vzlcn0 | 52bc6312e1 | |
Marcus | d735cca970 | |
Zack Scholl | af5ba6ead8 | |
Zack Scholl | 5978896936 | |
Zack Scholl | 2ffafd4607 | |
Zack Scholl | e7ed4fc05f | |
Zack Scholl | 1da2f58a53 | |
Zack Scholl | 284af7fc7a | |
Zack Scholl | dbc4a08ad0 | |
Zack Scholl | c55e9be0ef | |
Zack Scholl | b740e491e6 | |
Zack Scholl | 10ed62cfd5 | |
Zack Scholl | 8d430b6cb1 | |
Zack Scholl | cdb1981358 | |
Zack Scholl | f51ea8bfdc | |
Zack Scholl | a45e66733b | |
Zack Scholl | 23f97c61ac | |
Zack Scholl | cbb728b9a2 | |
Zack Scholl | 9bb2d2b7cc | |
Zack Scholl | 3f10a147dd | |
Zack Scholl | d112f43f63 | |
Zack Scholl | bf9ebfc4b4 | |
Zack Scholl | d3aa2be9ab | |
Zack Scholl | 034e012227 | |
Zack Scholl | 05c82809c6 | |
Zack Scholl | 79f85fda9b | |
Zack Scholl | 6ad8e08b0c | |
Zack Scholl | 3bcc83a8fc | |
Zack Scholl | 6c86188564 | |
Zack Scholl | 1d9393d39b | |
Zack Scholl | 4663df1059 | |
Zack | 817e0b78f6 | |
Khang. Nguyen Khac Nguyen | 4070f12cbf | |
Khang. Nguyen Khac Nguyen | 8e50b59671 | |
Zack Scholl | 61ffdc61f3 | |
Zack Scholl | ff45ccd346 | |
Zack Scholl | 5a791e98af | |
Zack Scholl | 0d767af80a | |
Zack Scholl | bb971a9937 | |
Zack Scholl | c88d3ad62a | |
Zack Scholl | 1a242711cc | |
Zack Scholl | 77732c49a0 | |
Zack Scholl | b9b22ad041 | |
Zack Scholl | 7a73e4912c | |
Zack Scholl | 3129b097af | |
Zack Scholl | 1967ef57be | |
Zack Scholl | b95c2c781b | |
Zack Scholl | ea0e334feb | |
Zack | de03a5167a | |
Khang. Nguyen Khac Nguyen | 12e09020d1 | |
Khang. Nguyen Khac Nguyen | e85ac9f8bf | |
Khang. Nguyen Khac Nguyen | 9f54f2d08f | |
Zack Scholl | f5624007c8 | |
Zack Scholl | 9b6c33c300 | |
Zack Scholl | b66cfb4cc1 | |
Zack Scholl | a5bcf382e0 | |
Zack Scholl | 539d66806a | |
Zack Scholl | 768f3e1318 | |
Zack Scholl | a968f494fa | |
Zack Scholl | 911bb22b2d | |
Zack Scholl | 58796824bb | |
Zack Scholl | f3697f6aad | |
Zack Scholl | 5374be1a0f | |
Zack Scholl | 1d874b5dbe | |
Zack Scholl | 586fc08768 | |
Zack Scholl | fa1359f1b8 | |
Zack Scholl | b12fc47951 | |
Zack | 5a2d51c060 | |
Zack | 3396743d73 | |
Zack Scholl | e75b75164f | |
Zack Scholl | 224c442567 | |
Zack | 669920d4b5 | |
Zack | 626490545a | |
fooofei | 5a0ec9431f | |
fooofei | bc8c5db898 | |
fooofei | c877497cc9 | |
Micheal Quinn | 4675976f08 | |
Zack | f484635321 | |
Mateusz Piotrowski | d0f3bad818 | |
Zack | 4e6efc2678 | |
ABDUL NIYAS P M | 4ce170e73d | |
Zack | 04e92a4ccd | |
Zack Scholl | 43d2ff0a09 | |
Zack Scholl | 78e4d5e179 | |
tuxuser | 3d0b5b9d34 | |
Zack | dab52b4af7 | |
Sebastian Falbesoner | a313a96a8e | |
Zack | a507b98cbe | |
Sebastian Falbesoner | 2de73cb138 | |
Zack | 20a8603ffe | |
TheChiefMeat | ebe713e881 | |
Zack | 28c38a4b9e | |
Zack | 1faebcf2e2 | |
Zack | db388f47a6 | |
sa3dany | a0cc9e943b | |
Zack Scholl | 9ac64f5c75 | |
Zack Scholl | 7b15dcd2dc | |
Zack Scholl | fda0bf7fd4 | |
Zack Scholl | 7d155adcf1 | |
Zack Scholl | ebc2cb2d03 | |
Zack | b144fa28e7 | |
masterZSH | f9f32a4b0e | |
Zack Scholl | b916b89f7e | |
Zack Scholl | 32bb98fd4f | |
Zack Scholl | ad6e7dff43 | |
Zack Scholl | a48bf7bbe0 | |
Zack | de8dfb10aa | |
masterZSH | 833668e8f2 | |
Zack Scholl | 8a0800ca90 | |
Zack Scholl | 4fc40274db | |
Zack Scholl | 8c116cf66a | |
Zack | acda80d35c | |
Alessandro Pezzè | 1534e913a3 | |
Zack | b9e834104a | |
Zack | 0aabd23c61 | |
jolheiser | ccba416e85 | |
jolheiser | 8258222cc5 | |
Zack | f70eb17afb | |
jolheiser | 5ad8bd8e2c | |
jolheiser | 9a20d507a7 | |
Zack | f2c5ca820a | |
Herby Gillot | ce7c497c09 | |
Zack | 1226a802e4 | |
Ilya Mateyko | f20c42319b | |
Zack Scholl | a5e882ec25 | |
Zack Scholl | 9d5302b1b7 | |
Zack Scholl | bdb859c15c | |
Zack Scholl | 22c35412ac | |
Zack Scholl | f2af0b7ed5 | |
Zack Scholl | 5d6546ba58 | |
Zack Scholl | f4fe211bbe | |
Zack Scholl | f950633a86 | |
Zack Scholl | 65b6111512 | |
Zack Scholl | 2db2a914f2 | |
Zack Scholl | 345ef0567e | |
Zack | 189890259e | |
Zack Scholl | e771e20ac4 | |
Zack Scholl | 5e78725afa | |
Zack Scholl | fc14ef25ed | |
Zack Scholl | b48c96fc37 | |
Zack Scholl | 87e54acd8a | |
Zack Scholl | 555ddec5e3 | |
Zack Scholl | ff99b07ef1 | |
Zack Scholl | 26056a8366 | |
Zack Scholl | 3692e32dfb | |
Zack Scholl | 11efb6be5e | |
Zack Scholl | 900424ca66 | |
Zack Scholl | bd2376e920 | |
Zack Scholl | def4f94ffd | |
Zack Scholl | bbc0ce4e9c | |
Zack Scholl | 8b6c0d1c39 | |
Zack Scholl | d55327d344 | |
Zack Scholl | c7ba55fc83 | |
Zack Scholl | 85b370291d | |
Zack Scholl | 81ad30c633 | |
Zack | ba2c46c1cb | |
Zack Scholl | 0eb095bf76 | |
Zack Scholl | 8fbb7e5019 | |
Zack Scholl | 48d63e4854 | |
Zack Scholl | ea781e569b | |
Zack Scholl | a8c5bef7c0 | |
Zack Scholl | b188a2691e | |
Zack Scholl | 20967519fd | |
Zack Scholl | 09a37a2be4 | |
Zack Scholl | 7bc9dc1826 | |
Zack Scholl | 2345148fde | |
Zack Scholl | 50bf003022 | |
Zack Scholl | 695444159e | |
Zack Scholl | 0533f0c96e | |
Zack Scholl | 52182099eb | |
Zack Scholl | e250eeede6 | |
Zack Scholl | 4aa7f05630 | |
Zack Scholl | 3a805f8be3 | |
Zack Scholl | 588e650d80 | |
Zack Scholl | baddb93c4e | |
Zack Scholl | c9a58513e1 | |
Zack Scholl | 8f13aedc19 | |
Zack Scholl | c0a05401c9 | |
Zack Scholl | ae384490d6 | |
Zack | e3ed4ba998 | |
Zack | 43480dd997 | |
mathew | 6874d9eede | |
mathew | 1b646d5528 | |
mathew | 827b5d3a86 | |
Zack Scholl | 0beeebc351 | |
Zack Scholl | f4047d5500 | |
Zack Scholl | 4deced8827 | |
Zack Scholl | c260147714 | |
Zack Scholl | 81e9f772a1 | |
Zack Scholl | 8b11c5b661 | |
Zack Scholl | 009f36f16c | |
Zack Scholl | 73dec72a74 | |
Zack Scholl | ee9188b082 | |
Zack Scholl | fdf383b2d7 | |
Zack Scholl | 7fd8fda107 | |
Zack Scholl | 1255a79a57 | |
Zack Scholl | b4e1ed2697 | |
Zack Scholl | 0377223a67 | |
Zack | dd9cd82732 | |
Zack | 91468709aa | |
Zack Scholl | 23c21bcafd | |
Zack Scholl | 78b643792e | |
Zack Scholl | 532cfa9cd9 | |
Zack Scholl | cf6b13d156 | |
Zack | 9440d8eb04 | |
Zack Scholl | 14f6a4a8be | |
Zack Scholl | 93e45c6942 | |
Zack Scholl | 6dc44ec002 | |
Zack Scholl | 4211cea92e | |
Zack Scholl | 1740bda985 | |
Zack Scholl | 189d1f7b2a | |
Zack Scholl | 86ebbaa618 | |
Zack Scholl | 2e7708e3cd | |
Zack Scholl | cb522f2f06 | |
Zack Scholl | 97f7f28f6e | |
Zack Scholl | 3ed036b51d | |
Zack Scholl | a3e81e1d87 | |
Zack Scholl | e56e65207f | |
Zack Scholl | f6ea7243ea | |
Zack Scholl | f9f4f291da | |
Zack | d1c2777906 | |
Zack | 0860283407 | |
Maxim Baz | cb025ae8d4 | |
Maxim Baz | c058bcbb6b | |
Zack Scholl | 9bf470ef29 | |
Zack Scholl | 11d4fead2c | |
Zack Scholl | 9c0bc3efc3 | |
Zack Scholl | 386f4ff958 | |
Zack Scholl | 97bf35bf93 | |
Zack Scholl | f8fd239eec | |
Zack Scholl | 72378947b5 | |
Zack Scholl | b0125b63e7 | |
Zack Scholl | b60a841044 | |
Zack Scholl | 44c3d43fa0 | |
Zack Scholl | 7a605dd5c8 | |
Zack Scholl | aae56043cf | |
Zack Scholl | 09d35d248f | |
Zack Scholl | 03fe1c770a | |
Zack Scholl | 1dd75c4bc3 | |
Zack Scholl | 8b6500adfd | |
Zack Scholl | 3cf474e6e8 | |
Zack Scholl | 820225b684 | |
Zack Scholl | eddf361893 | |
Zack Scholl | 2d6206b84f | |
Zack Scholl | 074a869d6e | |
Zack | 0c305fee08 | |
Zack Scholl | 5e37a308fe | |
Zack Scholl | bc1f89ff68 | |
Zack Scholl | 788a63cfa6 | |
Zack Scholl | a87b571376 | |
Zack Scholl | 0b99be5b30 | |
Zack Scholl | c3adc2981f | |
Zack Scholl | 5fce2a2e27 | |
Zack Scholl | d5846bc88d | |
Zack | 2217f7ca61 | |
Zack Scholl | 4c56ec283d | |
Zack Scholl | bc0841d8a1 | |
Zack | d4cbe03d0d | |
Roman | df79b2ed8e | |
Zack Scholl | 29882db39b | |
Zack Scholl | 073569f292 | |
Zack Scholl | d6694a9b41 | |
Zack Scholl | 5369fbc3aa | |
Zack Scholl | 03d9785408 | |
Zack Scholl | fbcad738bb | |
Zack Scholl | fd20b25a30 | |
Zack Scholl | aa54ff18e5 | |
Zack Scholl | 44286ce473 | |
Zack Scholl | 4824033619 | |
Zack | 493ae9306d | |
Zack Scholl | 25902edf5b | |
Zack Scholl | 3d9bbff7a7 | |
Zack Scholl | 02be7abe32 | |
Zack Scholl | 4dd2922b5f | |
Thorben Günther | 06dd3ba62f | |
Thorben Günther | 1ae85758f0 | |
Zack | f12940bbcd | |
Micheal Quinn | 247a698757 | |
Micheal Quinn | 47d84b9947 | |
Micheal Quinn | f6ad8f57cf | |
Zack | 24cb8f0103 | |
Zack Scholl | f52447acd9 | |
Zack Scholl | 3c03719863 | |
Zack Scholl | 5bafc99741 | |
Zack Scholl | 20061b484e | |
Zack | 88c4257816 | |
Maxim Baz | 1a544e4624 | |
Zack Scholl | eb85477086 | |
Zack Scholl | 5faacd3328 | |
Zack | 04cad7b9a5 | |
Zack Scholl | 60d6489c69 | |
Zack | 0e555179f8 | |
Zack | 3d672d6ac2 | |
Maxim Baz | b27b814b49 | |
Maxim Baz | cc6af6e5e8 | |
Maxim Baz | 252475f72e | |
Zack Scholl | e3ba938582 | |
Maxim Baz | 1cdccfa884 | |
Zack Scholl | e84e89b786 | |
Zack Scholl | fb35da42ed | |
Zack Scholl | d91ce7e469 | |
Zack Scholl | cbc55a84a7 | |
Zack Scholl | 0694d61294 | |
Zack | f102195354 | |
Alain | a5571306c2 | |
Zack Scholl | 60030cdfbb | |
Zack Scholl | 181dee9991 | |
Zack Scholl | bcb2b5d783 | |
Zack Scholl | af95cddd8d | |
Zack Scholl | 6b26af4dca | |
Zack Scholl | b9b3995f02 | |
Zack Scholl | d0c2dc1be8 | |
Zack | 544d3e0144 | |
Zack Scholl | c369f67745 | |
Zack Scholl | 76fc3f1a77 | |
Zack Scholl | baf5d6f27f | |
Zack Scholl | 497ffb8c2f | |
Zack Scholl | 7b5acc89a4 | |
Zack Scholl | eb22400db0 | |
Zack Scholl | 79e5f4445b | |
Zack Scholl | 0f15698970 | |
Zack Scholl | 419a766c6f | |
Zack Scholl | 19b5923f48 | |
Zack Scholl | 2f2e19aa91 | |
Zack Scholl | ae412375ef | |
Zack Scholl | 21c1efe319 | |
Zack | 4206605a2d | |
Zack Scholl | 9d5eec4246 | |
Zack Scholl | 3941762bf3 | |
Zack Scholl | 648c41d707 | |
Zack Scholl | c7f419ca2d | |
Zack Scholl | fe8dad42f7 | |
Zack Scholl | 6a036d68f6 | |
rain1 | b755d3f827 | |
rain1 | 25776a1d96 | |
rain1 | 9ec23ae367 | |
Zack | 43927a6dbb | |
Zack Scholl | b8bcbb3ecc | |
Zack Scholl | b7625ef55c | |
Zack Scholl | b7565177be | |
Zack Scholl | a7aed67c65 | |
Zack Scholl | b4f8fafb63 | |
Zack Scholl | 04724f4900 | |
Zack Scholl | b2e81fb50d | |
Zack | 1754333ac0 | |
Maxim Baz | f3799213b6 | |
Zack | bfa7768781 | |
Zack Scholl | acd7d8d8ec | |
Zack Scholl | bdb8bf3ac4 | |
Zack | a0d9d74d5d | |
Tai Groot | 68f039fbb0 | |
Zack | 9afae3b6b0 | |
Zack Scholl | c7624535e9 | |
Zack Scholl | da626334e0 | |
Zack Scholl | aaeda77af4 | |
Zack Scholl | ffddd3e989 | |
Zack | 4b8d32ff75 | |
Zack Scholl | 9aa327a0a4 | |
Zack Scholl | 42c2d5c6c1 | |
Zack | 41b91a3baf | |
Micheal Quinn | 183791d4c4 | |
Zack | c4d8c168a3 | |
Zack Scholl | e286321d39 | |
Zack Scholl | af39b8ca14 | |
Zack | 810a04509a | |
Zack Scholl | 59666c564d | |
Zack Scholl | 00e6ac9316 | |
Zack Scholl | 7a1fb8db24 | |
Zack Scholl | 5d89bdb6f9 | |
Zack Scholl | c46a9d3ae0 | |
Zack Scholl | ae81dd9a7b | |
Zack Scholl | 4e94a22300 | |
Zack Scholl | 8be63bed43 | |
Zack Scholl | a7435a08bd | |
Zack Scholl | 22ead388cd | |
Zack Scholl | 7ac7e5d56d | |
Zack Scholl | cfbd65be31 | |
Zack Scholl | 07c20e2cde | |
Zack Scholl | 24abf4fdd8 | |
Zack Scholl | 768ad9b739 | |
Zack Scholl | 6202d0d932 | |
Zack Scholl | 049a4aaffc | |
Zack Scholl | 1fe8ccf3ab | |
Zack Scholl | 9fb54a3952 | |
Zack | a0e2876741 | |
Micheal Quinn | 932f037b17 | |
Micheal Quinn | fe4e9c357a | |
Micheal Quinn | c0f7c3c250 | |
Micheal Quinn | f01165e9f4 | |
Micheal Quinn | 5fbb787a84 | |
Zack Scholl | 24801dfc24 | |
Zack Scholl | 5ad631e7b6 | |
Zack Scholl | cc728a1138 | |
Zack Scholl | b979607c42 | |
Zack Scholl | f89dd01e87 | |
Zack Scholl | 6287a7b7f7 | |
Zack Scholl | 22f6a13a9f | |
Zack Scholl | a82dd2f284 | |
Zack Scholl | 33aa006c26 | |
Zack Scholl | 845dabbae0 | |
Zack Scholl | 31c1a37b38 | |
Zack Scholl | 0277abe5d4 | |
Zack | 175b9a3f76 | |
Zack Scholl | 9a70ea90d1 | |
Zack | bc6803eeef | |
Zack Scholl | fb658ccde2 | |
Zack Scholl | de7a29470b | |
Zack Scholl | 4150feddf0 | |
Micheal Quinn | 97f65bf51d | |
Micheal Quinn | 07a2abd808 | |
Zack Scholl | 7515c92a8b | |
Micheal Quinn | 7a6c4db5f9 | |
Micheal Quinn | e3e7ada3e6 | |
Micheal Quinn | 105357bcbf | |
Micheal Quinn | d6d96e487f | |
Micheal Quinn | d3207e6190 | |
Quinn | 51e77ba0c4 | |
Quinn | f3efbb34af | |
Quinn | 9e9f43c352 | |
Quinn | 27bc2e8c8a | |
Quinn | 559807f97d | |
Quinn | a31bc56c23 | |
Quinn | a9caea4d64 | |
Quinn | ec09a990af | |
Micheal Quinn | a72172d11e | |
Hugo Reeves | 53328fa58b | |
Quinn | 081e267a19 | |
Quinn | d429e09e95 | |
Micheal Quinn | 588dfe6c00 | |
Micheal Quinn | e2e48d9e84 | |
Micheal Quinn | b8d1d11b68 | |
Micheal Quinn | 34743118de | |
Micheal Quinn | 9c6f01ca91 | |
Micheal Quinn | d39266a36d | |
Micheal Quinn | bf15c4c424 | |
Micheal Quinn | 2c2920eb9c | |
Micheal Quinn | e7d6d096a8 | |
Micheal Quinn | b0ac5024a4 | |
Micheal Quinn | 9530952eb6 | |
Micheal Quinn | eaf448ddac | |
Micheal Quinn | c82dda45d9 | |
Micheal Quinn | 7a684e7266 | |
Micheal Quinn | 6178a7e16b | |
Zack | 02f89b1854 | |
Micheal Quinn | d5a8d07d1c | |
Micheal Quinn | 2c27d7aabd | |
Micheal Quinn | f8d60c0011 | |
Zack Scholl | e1e722fc5b | |
Zack Scholl | 82f2c8cd2f | |
Zack Scholl | 88b4001dd7 | |
Zack Scholl | 2735c42a9d | |
Zack Scholl | 83db5bdc68 | |
Zack Scholl | fe1329c909 | |
Zack Scholl | 336f314854 | |
Zack Scholl | 6f31418330 | |
Zack | bd7ab1a9be | |
Zack Scholl | edc97915bd | |
Zack Scholl | 256e0c51fa | |
Zack Scholl | 7fcf45cece | |
Zack Scholl | 6a8bdccb66 | |
Zack Scholl | 252e96a670 | |
Zack Scholl | 97d18e1fbf | |
Zack Scholl | 54fb8aec9d | |
Zack Scholl | 1d7976a61a | |
Zack Scholl | f18c2eae7e | |
Zack Scholl | 68ddbe7f14 | |
Zack Scholl | 8b178881cf | |
Zack Scholl | 9968bbf824 | |
Zack Scholl | 928ee1f496 | |
Zack Scholl | fbbdd1bcfe | |
Zack Scholl | f6cafd76fd | |
Zack Scholl | fe231fa67a | |
Zack Scholl | 6091e9348e | |
Zack Scholl | 57564314b0 | |
Zack | 6d9105abfb | |
Zack Scholl | ba00eda176 | |
Zack Scholl | 1d744f1b4a | |
Zack Scholl | 0519850bc3 | |
Matthew Helmke | e5d59fe563 | |
Zack Scholl | f35c5b88b6 | |
Zack Scholl | 2e2700988b | |
Zack Scholl | 15034cc6d2 | |
Zack Scholl | b1022a3408 | |
Zack Scholl | 1975745d5f | |
Zack Scholl | 6b288356b9 | |
Zack | bcb348a776 | |
Zack Scholl | 59b6558ce9 | |
Zack | 29915596ed | |
Zack Scholl | 8bdc71a06c | |
meyermarcel | cba650193d | |
Zack | acae0abb24 | |
meyermarcel | a23347a823 | |
Zack Scholl | c45d3c01e5 | |
Zack | 59736eefd8 | |
meyermarcel | f5e1bab6b5 | |
Zack Scholl | ae6ce06836 | |
Zack Scholl | 30f1233f74 | |
Zack Scholl | f91d52bead | |
Zack Scholl | b71e1e78ca | |
Zack Scholl | 0388e07689 | |
Zack Scholl | da54ae060a | |
Zack Scholl | 257607a26f | |
Zack Scholl | 8d3b240563 | |
Zack Scholl | ba96be5024 | |
Zack Scholl | bd9c7b5ae9 | |
Zack Scholl | cb69e49283 | |
Zack Scholl | a6512975ff | |
Zack | 1f3d30c78e | |
Zack Scholl | fef1c3ca3b | |
Zack Scholl | 9f9b93cf47 | |
Zack Scholl | 7d91f8200c | |
Zack Scholl | 1802ebcd00 | |
Zack Scholl | ee38e7243e | |
Zack Scholl | da09151db2 | |
Zack Scholl | 63f4553e4b | |
Zack Scholl | 8beaf65007 | |
Zack Scholl | 5e8ef27b85 | |
Zack Scholl | 09575a0c88 | |
Zack Scholl | aeb30ef187 | |
Zack Scholl | 711690a0f2 | |
Zack Scholl | ea115908fc | |
Zack Scholl | 3e6a554532 | |
Zack Scholl | 38e7cae977 | |
Zack Scholl | 6f5225ac91 | |
Zack Scholl | 19efe8b77a | |
Zack Scholl | b7c817a04a | |
Zack Scholl | dccaff32cf | |
Zack | f2e420cd2c | |
Zack | 02b0cbed06 | |
Zack Scholl | e29179dd9c | |
Zack Scholl | 2da7fa22ad | |
Zack Scholl | 2515cfa31e | |
Zack Scholl | 78e4ee2b68 | |
Zack Scholl | 2f4e5cdec4 | |
Zack Scholl | 37b29aebce | |
Zack Scholl | 9e61f38707 | |
Zack Scholl | 7377f536fc | |
Zack Scholl | 462fad8d8e | |
Zack Scholl | 9cf4050913 | |
Zack Scholl | aae63876d6 | |
Zack Scholl | 7af57b3390 | |
Zack Scholl | 2e5f33c081 | |
Zack Scholl | 95c2b0f48a | |
Zack Scholl | abc5e3f6c0 | |
Zack Scholl | 2b42c5365c | |
Zack Scholl | 67f69c892d | |
Zack Scholl | 359dc4549b | |
Zack Scholl | b109b419de | |
Zack Scholl | 7162e5e45b | |
Zack Scholl | 494b224db0 | |
Zack Scholl | 868051bc2a | |
Zack Scholl | 77b11e3c3f | |
Zack Scholl | ecfdb2d440 | |
Zack Scholl | eceddb364e | |
Zack Scholl | 4a27806ffe | |
Zack Scholl | b55af54b10 | |
Zack Scholl | e1745757ac | |
Zack Scholl | be1c1d6206 | |
Zack Scholl | 655ffdb4c0 | |
Zack Scholl | 9f6936e1ff | |
Zack Scholl | 48819a9e03 | |
Zack Scholl | 6ac4343213 | |
Zack Scholl | 29dceee8f4 | |
Zack Scholl | 5bffeabbba | |
Zack Scholl | f3e2a260d9 | |
Zack Scholl | 883dff96b8 | |
Zack Scholl | 4be02ad249 | |
Zack Scholl | 2c2c3f58ac | |
Zack Scholl | ec8768bc70 | |
Zack Scholl | 1275c6b1b5 | |
Zack Scholl | c5bbdb4cb5 | |
Zack Scholl | 63ec16f7fb | |
Zack Scholl | 23c9a9cff8 | |
Zack Scholl | 9952da9f6d | |
Zack Scholl | e72795985b | |
Zack Scholl | a1e5a283f9 | |
Zack Scholl | e8f465060f | |
Zack Scholl | 002cc0050c | |
Zack Scholl | a58a0df910 | |
Zack Scholl | 89e4871af2 | |
Zack Scholl | f4d24a1f72 | |
Zack Scholl | 7060eec8d1 | |
Zack Scholl | a70aa0a4fc | |
Zack Scholl | 8f816304f9 | |
Zack Scholl | ce45c53b38 | |
Zack Scholl | a35779f718 | |
Zack Scholl | d1780e70b7 | |
Zack Scholl | 3e9283dc01 | |
Zack Scholl | 2583313602 | |
Zack Scholl | b9d2be5378 | |
Zack Scholl | 962ea50aee | |
Zack Scholl | d56a25cf76 | |
Zack Scholl | 346b2d9881 | |
Zack Scholl | c8fdd4726a | |
Zack Scholl | 3bf7635f33 | |
Zack Scholl | 87d295f62b | |
Zack Scholl | be7c2ad1fb | |
Zack Scholl | 04844cf72c | |
Zack Scholl | 6da93ae8da | |
Zack Scholl | 4f20f3ce43 | |
Zack Scholl | e54045ef5a | |
Zack Scholl | 3c051f4283 | |
Zack Scholl | 000dfc15be | |
Zack Scholl | 330e76e09c | |
Zack Scholl | 42d35898b5 | |
Zack Scholl | c1c8e39499 | |
Zack Scholl | 859130a988 | |
Zack Scholl | 7d1f7adddb | |
Zack Scholl | ac113dfe47 | |
Zack Scholl | 972dce1ec5 | |
Zack Scholl | 662bce58a3 | |
Zack Scholl | 9223fc79e9 | |
Zack Scholl | a17f3096a0 | |
Zack Scholl | 6b149480d4 | |
Zack Scholl | a7f12ca179 | |
Zack Scholl | 7d9b199ca3 | |
Zack Scholl | 06541ee0d3 | |
Zack Scholl | bbd7caa148 | |
Zack Scholl | ca0b898ca3 | |
Zack Scholl | 3685a887b8 | |
Zack Scholl | 005a206460 | |
Zack Scholl | c1d66d5301 | |
Zack Scholl | cee4c36f8b | |
Zack Scholl | 1661bbb221 | |
Zack Scholl | 7a8801f8f1 | |
Zack Scholl | b52001e064 | |
Zack Scholl | 1045bd17b5 | |
Zack Scholl | 1f49966bb1 | |
Zack Scholl | dcc7689816 | |
Zack Scholl | 376591384a | |
Zack Scholl | 9030aae880 | |
Zack Scholl | 2414593c91 | |
Zack Scholl | 5b0c52a97b | |
Zack Scholl | ef25c556a9 | |
Zack Scholl | b9a5f450c5 | |
Zack Scholl | 8dc8783bd4 | |
Zack Scholl | f238c4b22c | |
Zack Scholl | 249c0d8ab0 | |
Zack Scholl | 14dd892377 | |
Zack | 951b2a2548 | |
Zack Scholl | 7c731a90dc | |
Zack Scholl | 003a2344e4 | |
Zack | 566694f381 | |
Zack Scholl | 9643fe748b | |
Zack | caaaff4bd5 | |
wxitcode | e272af8794 | |
Zack | bcce2c021d | |
Zack | 148b10d666 | |
Zack Scholl | af6cc64786 | |
Zack Scholl | fd8a1ebe7b | |
Zack Scholl | 6fa1b4c080 | |
Zack Scholl | 7839d7f6e2 | |
Zack Scholl | 43dbf98705 | |
Zack Scholl | e58fc14218 | |
Zack | d3fdb4257d | |
Lars | d42a4f82a1 | |
Zack Scholl | eaa3624977 | |
Zack Scholl | 068fdb77fc | |
Zack Scholl | 8a882cc174 | |
Zack Scholl | be70c8a83d | |
Zack Scholl | cf2363fd4e | |
Zack Scholl | 92648e0112 | |
Zack Scholl | 5ad34e1034 | |
Zack | 09f89a4958 | |
Zack | 73aeaec822 | |
Zack Scholl | e0081ea8a8 | |
Zack Scholl | a56334fc60 | |
Zack Scholl | aefb15105a | |
Zack Scholl | 2b8f581ca4 | |
Zack | 737049694b | |
Zack Scholl | 818e590d92 | |
Zack Scholl | 034074d7a3 | |
Zack Scholl | e57f3e3861 | |
Zack Scholl | 0ca74b010e | |
Zack Scholl | fea07cab6c | |
Zack Scholl | 562925b5e5 | |
Zack | c33ac025a0 | |
Zack | 3a53b572f4 | |
Zack | 8ae342e128 | |
Zack Scholl | 44f482937d | |
Zack | 8d43a1f63a | |
Zack | fc38472015 | |
Zack Scholl | 61d14be70d | |
nicolashardy | 20497437fe | |
Zack Scholl | 44af4a8ae3 | |
Zack Scholl | 824c49254f | |
Zack | 721567fc72 | |
Zack Scholl | 08c84e9f85 | |
Zack Scholl | edc206e73e | |
Zack Scholl | c2dd9091ff | |
Zack Scholl | e5fcc0d3ee | |
Zack Scholl | ea9aa3f8ec | |
Zack Scholl | 6a07e1538d | |
Zack Scholl | ab5df93d10 | |
Zack Scholl | db42e96b7e | |
Zack Scholl | 3d9bc7193d | |
Zack Scholl | 3064ceef81 | |
Zack Scholl | 47931d6ba2 | |
Zack Scholl | 3dc6d44c41 | |
Zack Scholl | 380ecc7ff9 | |
Zack Scholl | 708e6fab8b | |
Zack Scholl | 45b7eebbb9 | |
Zack Scholl | 69b19c7e78 | |
Zack Scholl | dd03cdc6e3 | |
Zack Scholl | 81bc06eabb | |
Zack Scholl | 1ae117166a | |
Zack Scholl | 0d17219c87 | |
Zack Scholl | 9e7661c1b0 | |
Zack Scholl | ace9b35e34 | |
Zack Scholl | 7371f6f238 | |
Zack Scholl | 23d451540c | |
Zack Scholl | e591c6afb6 | |
Zack Scholl | 9349496111 | |
Zack Scholl | 6e10ed25af | |
Zack Scholl | d05e196139 | |
Zack Scholl | 304355af48 | |
Zack | 50a64d04da | |
Zack Scholl | 4aab058344 | |
Zack Scholl | 06e8260e50 | |
Zack Scholl | dc6493374a | |
Zack Scholl | 5ecbab6673 | |
Zack Scholl | f94790dc75 | |
Zack Scholl | 92af0b46ab | |
Zack Scholl | 4e55b1efd1 | |
Zack Scholl | 0e4fff7378 | |
Zack Scholl | 79f7e9af41 | |
Zack Scholl | 62bf674365 | |
Zack Scholl | 2fc216cb1b | |
Zack Scholl | ec148d98c1 | |
Zack Scholl | 365eca9653 | |
Zack Scholl | 987a21fb8f | |
Zack Scholl | e85eaac9e3 | |
Zack Scholl | 366d3c527c | |
Zack Scholl | 3b498d57cf | |
Zack Scholl | 82e274875d | |
Zack Scholl | e47393031b | |
Zack Scholl | 4d35b3a397 | |
Zack Scholl | 48933b5ecc | |
Zack Scholl | f9dfa6bc05 | |
Zack Scholl | 4ee008225e | |
Zack Scholl | 24e0573116 | |
Zack | 62e7629466 | |
Zack Scholl | 56e0068129 | |
Zack Scholl | 93942f4e0c | |
Zack Scholl | 157ab169aa | |
Zack Scholl | 6a899492f5 | |
Zack Scholl | 47ab799c6f | |
Zack Scholl | 55d9137b6d | |
Zack Scholl | 3bed0bc8bf | |
Zack Scholl | 8734394bb8 | |
Zack Scholl | 0bbf2841ca | |
Zack Scholl | 144102a721 | |
Zack Scholl | 9a41d9099b | |
Zack Scholl | 49a73d7183 | |
Zack | 76da134a2f | |
Zack | f7487e8589 | |
Zack | ad697febf4 | |
Zack | 8662961e93 | |
Zack | 28083f3204 | |
Zack | da94e54d7d | |
Zack | 267134dc6c | |
Zack Scholl | 9826023f22 | |
Zack Scholl | 09a90a322a | |
Zack Scholl | 74f0ac5950 | |
Zack Scholl | 2fb21245ab | |
Zack Scholl | cc6edd24d1 | |
Zack Scholl | 70fb9b77ca | |
Zack | e46ac60c1b | |
Zack Scholl | a63666cf24 | |
Zack Scholl | 81bc14f29b | |
Zack | 6fc7b9b2d6 | |
Zack | 0f0b6c5b29 | |
Zack Scholl | af11143cba | |
Zack Scholl | f9b79ed371 | |
Zack Scholl | 91e065529b | |
Zack Scholl | 42344a4c6e | |
Zack Scholl | d1e1694631 | |
Zack Scholl | 9d2f07f478 | |
Zack Scholl | b0164920c7 | |
Zack Scholl | 8d1783fdc3 | |
Zack Scholl | 6955f19439 | |
Zack Scholl | 9439b8ea2d | |
Zack Scholl | d50c8f271f | |
Zack Scholl | 3984aaeee2 | |
Zack Scholl | 71ba80cee0 | |
Zack Scholl | 9335aca877 | |
Zack Scholl | edda09df00 | |
Zack | 5b5c05d694 | |
Zack Scholl | c600b51888 | |
Zack Scholl | e18938fb70 | |
Zack Scholl | 9940856317 | |
Zack Scholl | 61d57ad0af | |
Zack Scholl | 66a37ae5b2 | |
Zack Scholl | fdfa7f209d | |
Zack Scholl | bc4bd2b0a8 | |
Zack Scholl | 8ddd7aff06 | |
Zack Scholl | cc0ef58b9f | |
Zack Scholl | 21cfcc0bc2 | |
Zack Scholl | 928b28dc80 | |
Zack Scholl | fecc8d5897 | |
Zack Scholl | 420d2be271 | |
Zack Scholl | 1261a16b55 | |
Zack Scholl | 558d9133a6 | |
Zack Scholl | 267056b3fd | |
Zack Scholl | 4e3138e099 | |
Zack Scholl | a6b33fe046 | |
Zack Scholl | ecf8cbcdda | |
Zack Scholl | f6e2560742 | |
Zack Scholl | ea7ed1a2db | |
Zack Scholl | e92c7f15f1 | |
Zack Scholl | 3fc8edfef9 | |
Zack Scholl | 089d2bbe1a | |
Zack Scholl | 0bd4a0ac75 | |
Zack Scholl | 7a961ecffd | |
Zack Scholl | d2a708eaaa | |
Zack Scholl | b573f5c853 | |
Zack Scholl | 1049e9c8c8 | |
Zack Scholl | 113452d459 | |
Zack Scholl | 1954691f44 | |
Zack Scholl | 182a9fe2f4 | |
Zack Scholl | 42c0240e50 | |
Zack Scholl | 73d8eadce4 | |
Zack Scholl | c0e9d478e7 | |
Zack Scholl | 82b0d2d4c0 | |
Zack Scholl | ab70b53998 | |
Zack Scholl | 085dd4e4c3 | |
Zack Scholl | ea548f290c | |
Zack Scholl | 1a7b123519 | |
Zack Scholl | ae13f82c9e | |
Zack Scholl | f483a6ab9c | |
Zack Scholl | 02196654d9 | |
Zack Scholl | 7d5d7e6cfb | |
Zack Scholl | 082dcd2037 | |
Zack Scholl | 62d9d74753 | |
Zack Scholl | f2f1b31fbc | |
Zack Scholl | 397ceea34c | |
Zack Scholl | 0b3d19a52e | |
Zack Scholl | da2b22f73d | |
Zack Scholl | 5a94062171 | |
Zack Scholl | a59399630d | |
Zack | f555dc8dbf | |
Zack Scholl | 162b12f42b | |
Zack Scholl | efa66f7cbe | |
Zack Scholl | 7f0b919b0b | |
Zack Scholl | c8c532c5dd | |
Zack Scholl | 71fee31da0 | |
Zack Scholl | ec5d45307a | |
Zack Scholl | 02e8021735 | |
Zack Scholl | 667edd0373 | |
Zack Scholl | a7b8488040 | |
Zack Scholl | 7fea858252 | |
Zack Scholl | 68896f6ef7 | |
Zack Scholl | f62459e1a4 | |
Zack Scholl | 75f7cdcf65 | |
Zack Scholl | ee7518c7bd | |
Zack Scholl | 02e63b6473 | |
Zack Scholl | 196c9253b8 | |
Zack Scholl | 1a22d37c2d | |
Zack Scholl | f169b12109 | |
Zack Scholl | 44fad198e7 | |
Zack Scholl | 7df06272b8 | |
Zack Scholl | f56d5c797c | |
Zack Scholl | 7147980eac | |
Zack Scholl | 8bfbe36dd9 | |
Zack Scholl | 2f47dd3178 | |
Zack Scholl | d0e75370b2 | |
Zack Scholl | 6635c84f70 | |
Zack Scholl | 15ecc73f67 | |
Zack Scholl | 7891f27b62 | |
Zack Scholl | f6c2382c0a | |
Zack Scholl | 26f0fc3319 | |
Zack Scholl | 5397634217 | |
Zack Scholl | 02f4b08929 | |
Zack Scholl | 55b1fa35e4 | |
Zack Scholl | b202682596 | |
Zack Scholl | b14f1c35a3 | |
Zack Scholl | e10789b558 | |
Zack Scholl | 80d48d45d5 | |
Zack Scholl | e8ef8e1c61 | |
Zack Scholl | 90b08abf39 | |
Zack Scholl | e6b9655387 | |
Zack Scholl | 50c29e7334 | |
Zack Scholl | 711f8dfeec | |
Zack Scholl | ba20bb63ca | |
Zack Scholl | 45fe9b422d | |
Zack Scholl | 975bab61fb | |
Zack Scholl | b7c447473d | |
Zack Scholl | 73c33ebe02 | |
Zack Scholl | fe38107c42 | |
Zack Scholl | 7651ed0562 | |
Zack Scholl | 2ebe3c1328 | |
Zack Scholl | 47fe8a2116 | |
Zack Scholl | a8c0c02755 | |
Zack Scholl | 8fab2299a1 | |
Zack Scholl | 4f9532b761 | |
Zack Scholl | da9634c949 | |
Zack Scholl | b2939a0452 | |
Zack Scholl | 2788a4e742 | |
Zack Scholl | 384de31c5a | |
Zack Scholl | 177612f112 | |
Zack Scholl | 2f800f1de3 | |
Zack Scholl | 6c94ece008 | |
Zack Scholl | 8797d35fa1 | |
Zack Scholl | c9dab1ca44 | |
Zack Scholl | 76cb610ffd | |
Zack Scholl | d860fe1e9e | |
Zack Scholl | 8afc75f1fc | |
Zack Scholl | 0a0c0bfd32 | |
Zack Scholl | f3df177ba2 | |
Zack Scholl | 7799d2fb58 | |
Zack Scholl | 3847ba2bb1 | |
Zack Scholl | 232f3375b1 | |
Zack Scholl | c6374da410 | |
Zack Scholl | f977ac24c4 | |
Zack Scholl | 1f64d1c93a | |
Zack Scholl | edfb018ae2 | |
Zack Scholl | 9a33954d42 | |
Zack Scholl | 10111b92cd | |
Zack Scholl | e13f8e7067 | |
Zack Scholl | 1acbe9c475 | |
Zack Scholl | 73791a4dc7 | |
Zack Scholl | b469d5db4b | |
Zack Scholl | 2946922fb5 | |
Zack Scholl | 742a935ea9 | |
Zack Scholl | 54b22cd732 | |
Zack Scholl | 9cf6bdf4a8 | |
Zack Scholl | 4d9455c24b | |
Zack Scholl | 7513702e9c | |
Zack Scholl | 66eeab25fb | |
Zack Scholl | 82315ca413 | |
Zack Scholl | cd3a936795 | |
Zack Scholl | b1aeedb107 | |
Zack Scholl | 6d9c829579 | |
Zack Scholl | 1add7e94c9 | |
Zack Scholl | e5b3e6e06e | |
Zack Scholl | 9d537c42e5 | |
Zack Scholl | f709df4d3e | |
Zack Scholl | 55bd83576a | |
Zack Scholl | 91532085c7 | |
Zack Scholl | f3eb85dd95 | |
Zack Scholl | ebac8beaaa | |
Zack Scholl | d555a81d30 | |
Zack Scholl | 40672a4eaa | |
Zack Scholl | 0528bb22ea | |
Zack Scholl | 4fd172b6fc | |
Zack Scholl | 6911848141 | |
Zack Scholl | 9af7c05dc6 | |
Zack Scholl | c4cfa9b07f | |
Zack Scholl | 11ab8a8078 | |
Zack Scholl | a225a08dbc | |
Zack Scholl | 80471c7d02 | |
Zack Scholl | f9ed7553af | |
Zack Scholl | 83f199b213 | |
Zack Scholl | 5bf4ff1866 | |
Zack Scholl | caf5511805 | |
Zack Scholl | b6b6b754c9 | |
Zack Scholl | 24493cf7f5 | |
Zack Scholl | 828f83601d | |
Zack Scholl | 274fab92a1 | |
Zack Scholl | b9dad87526 | |
Zack | 6af10ad871 | |
Zack Scholl | 87a17fb9e6 | |
Zack Scholl | 53e0585f21 | |
Zack Scholl | b8f647291a | |
Zack Scholl | 53db42fe5f | |
Zack Scholl | 2081b4e17e | |
Zack Scholl | 9e02fa746c | |
Zack Scholl | ffbc607303 | |
Zack Scholl | c01bc56079 | |
Zack Scholl | d906630f3a | |
Zack Scholl | 085550c94b | |
Zack Scholl | 0c920a27ab | |
Zack Scholl | db50a7eee0 | |
Zack Scholl | 68d42cdadd | |
Zack Scholl | 2ce63b5cda | |
Zack Scholl | 11566c710d | |
Zack Scholl | 4e88975ed4 | |
Zack Scholl | 09a16cacd5 | |
Zack Scholl | 34d499127d | |
Zack Scholl | 52ab6be29a | |
Zack Scholl | 02e6aa6ec7 | |
Zack Scholl | 9c22753ae4 | |
Zack Scholl | 594029891e | |
Zack Scholl | 280faf7657 | |
Zack Scholl | 846e3d5d00 | |
Zack Scholl | 967d3d7c56 | |
Zack Scholl | f2646eb8b3 | |
Zack Scholl | bba0bca15e | |
Zack Scholl | 8373e374cd | |
Zack Scholl | 80c129b411 | |
Zack Scholl | efee59f541 | |
Zack Scholl | 7be11a6fda | |
Zack Scholl | bc34811750 | |
Zack Scholl | ba3fa6ffce | |
Zack Scholl | a1611ccc88 | |
Zack Scholl | 0ec32e3f9a | |
Zack Scholl | 551160d601 | |
Zack Scholl | 7f423cda26 | |
Zack Scholl | 91c95a5a8c | |
Zack Scholl | 22220e54a7 | |
Zack Scholl | a6bc4973aa | |
Zack Scholl | f315a0bb79 | |
Zack Scholl | ebad469554 | |
Zack Scholl | d255f3bba0 | |
Zack Scholl | 909e82323d | |
Zack Scholl | 1ceed5de35 | |
Zack Scholl | 0539e425d2 | |
Zack Scholl | b09955561f | |
Zack Scholl | 5ca6a6455e | |
Zack Scholl | 2e545e6594 | |
Zack Scholl | 51f3de95a7 | |
Zack Scholl | f4d607a4cf | |
Zack Scholl | bc5f4b7131 | |
Zack Scholl | 5a21ff6729 | |
Zack Scholl | 66e510f735 | |
Zack Scholl | 6164bc37d5 | |
Zack Scholl | ca9fafed55 | |
Zack Scholl | 3d24c0e258 | |
Zack Scholl | 816c25b05b | |
Zack Scholl | 43026b9c2e | |
Zack Scholl | b7f40b2181 | |
Zack Scholl | dd8d8ee4ce | |
Zack Scholl | e055918656 | |
Zack Scholl | 1223b3e51d | |
Zack Scholl | ae7ceca3b7 | |
Zack Scholl | f6abf9c4e8 | |
Zack Scholl | a9969b161f | |
Zack Scholl | d1d5b02f27 | |
Zack Scholl | 0d71cde941 | |
Zack Scholl | bf7a1bbdff | |
Zack Scholl | 1cdbdcb08e | |
Zack Scholl | 2b61224707 | |
Zack Scholl | bda038b8fc | |
Zack Scholl | 1d316e6fbe | |
Zack Scholl | b2885a7d95 | |
Zack Scholl | 2c444ad400 | |
Zack Scholl | f7c22067d4 | |
Zack Scholl | aa0c5acc60 | |
Zack Scholl | d95c90de44 | |
Zack Scholl | f1b62ea30d | |
Zack Scholl | 0d5836b4e8 | |
Zack Scholl | adff8400be | |
Zack Scholl | 919de910cb | |
Zack Scholl | 837a66e975 | |
Zack Scholl | fa1b2af314 | |
Zack Scholl | eb3251a93f | |
Zack Scholl | 0ab25185d3 | |
Zack Scholl | 59fc697b4f | |
Zack Scholl | 6e27bfebdd | |
Zack Scholl | 98374c31cd | |
Zack Scholl | 0bf384dd82 | |
Zack Scholl | 72e2d4d3d8 | |
Zack Scholl | f6751dadb9 | |
Zack Scholl | 283bf704a2 | |
Zack Scholl | f64427f70d | |
Zack Scholl | 7d07ccfe40 | |
Zack Scholl | 87c935dc1e | |
Zack Scholl | b32d7060bf | |
Zack Scholl | 22cd0afd11 | |
Zack Scholl | 7a1f0f66cd | |
Zack Scholl | 6f1e6f3da8 | |
Zack Scholl | ffebb472b9 | |
Zack Scholl | b2dc1f32f8 | |
Zack Scholl | e3ebed186d | |
Zack Scholl | e557869afd | |
Zack Scholl | 7656b102c7 | |
Zack Scholl | ce8135002c | |
Zack Scholl | 5ede3186e5 | |
Zack Scholl | 84f732b662 | |
Zack | 606918b14e | |
Zack | 3335250e92 | |
Zack Scholl | 778e8dda4d | |
Zack | 3bab088f98 | |
Zack Scholl | 0ccc2685cf | |
Zack Scholl | db911d1bce | |
Zack Scholl | 2b8d8670bd | |
Zack Scholl | af92a47450 | |
Zack Scholl | fc7dcc5e0a | |
Zack Scholl | 44c08329fb | |
Zack Scholl | e438b0a270 | |
Zack Scholl | f3523ebf36 | |
Zack Scholl | 1866b610fd | |
Zack Scholl | 277e187abb | |
Zack Scholl | 541ab998bd | |
Zack Scholl | 07ecef8d87 | |
Zack Scholl | 143f3dba14 | |
Zack Scholl | 13bf5994dc | |
Zack Scholl | fa522c3e54 | |
Zack Scholl | 6d7911ab24 | |
Zack Scholl | 3e5aa91f34 | |
Zack Scholl | 181de46313 | |
Zack Scholl | 42b9a00102 | |
Zack Scholl | c70327ff82 | |
Zack Scholl | de2de7b75c | |
Zack Scholl | 003aa64d2b | |
Zack Scholl | c5b7ba0905 | |
Zack Scholl | e084186b5e | |
Zack Scholl | 5ca9b06eb7 | |
Zack Scholl | 13670d683c | |
Zack Scholl | 97f99e60f2 | |
Zack Scholl | ba3accb9d7 | |
Zack Scholl | 44e169464c | |
Zack Scholl | 0e99540655 | |
Zack Scholl | 9746014f0b | |
Zack Scholl | 04dc07913e | |
Zack Scholl | a29a7e2f04 | |
Zack Scholl | 83f9005a1d | |
Zack Scholl | 9acaa988ae | |
Zack Scholl | 26ef99c170 | |
Zack Scholl | b3847798a4 | |
Zack | 88c2fcea0b | |
Zack Scholl | de7e2a6885 | |
Zack Scholl | 992e109279 | |
Zack Scholl | cc79aaeecb | |
Zack Scholl | 67bceee220 | |
Zack Scholl | 3c22789e3b | |
Zack Scholl | 6c3bb17d7c | |
Zack Scholl | 7c06c56700 | |
Zack Scholl | 901b0af827 | |
Zack Scholl | 6e69b2760e | |
Zack Scholl | 51a87f1110 | |
Zack Scholl | 30c9c3317f | |
Zack | 60e31803c9 | |
Zack | cd26b2437d | |
Zack | c2927ffaee | |
Zack Scholl | 349e3a81c7 | |
Zack Scholl | 4840c33eee | |
Zack | 08319cfd3f | |
Zack | f9c570e942 | |
Zack Scholl | 90809a80a1 | |
Zack Scholl | 7f5e3f50a0 | |
Zack Scholl | ad2ab1d8da | |
Zack | 488b88f9dd | |
Zack Scholl | 346284cbed | |
Zack Scholl | 90f1277ee5 | |
Zack Scholl | 2bd8504a57 | |
Zack Scholl | bea025fbe4 | |
Zack Scholl | 4e7f50c927 | |
Zack Scholl | 882d63a9c3 | |
Zack Scholl | 9837763bed | |
Zack Scholl | 13a35795be | |
Zack Scholl | bd4eaf90df | |
Zack Scholl | f0f7a43888 | |
Zack Scholl | 0e3ee4c343 | |
Zack Scholl | 0127df5f04 | |
Zack Scholl | d4fa4bc9c0 | |
Zack Scholl | e7d2f05399 | |
Zack Scholl | 091e8aecac | |
Zack Scholl | 00dfe62033 | |
Zack Scholl | 67526279ce | |
Zack Scholl | 7cea7ea9e2 | |
Zack Scholl | 1308694711 | |
Zack Scholl | 638ca9ef27 | |
Zack Scholl | f0ed13c392 | |
Zack Scholl | f689ca3909 | |
Zack Scholl | 1ced0aa532 | |
Zack Scholl | 13b7f4afcb | |
Zack Scholl | 95bec30262 | |
Zack Scholl | b6df5839b8 | |
Zack Scholl | c74c9b8faf | |
Zack Scholl | 22d7a1d42d | |
Zack Scholl | 3701240b10 | |
Zack Scholl | 68b937f4cb | |
Zack Scholl | 39e2a20a6f | |
Zack Scholl | 78df00f7ab | |
Zack Scholl | ef03f577f6 | |
Zack Scholl | 774312cb9d | |
Travis | 19bfbcb9c2 | |
Zack | a35612abfe | |
Zack Scholl | 3037385cb6 | |
Zack Scholl | 15f9acc441 | |
smileboywtu | 04ef2c4158 | |
smileboywtu | e1f48721a4 | |
smileboywtu | d518cf0c2b | |
smileboywtu | d60d81759f | |
smileboywtu | 97857ff4c2 | |
smileboywtu | 1eb07db618 | |
Zack | 93d0846e30 | |
Zack | 743cc2e22e | |
陈博 | 21a0e1778e | |
陈博 | 63ba167880 | |
Zack Scholl | 389ecf6e60 | |
Zack Scholl | e8d5afe1fa | |
陈博 | 407d85270c | |
Zack Scholl | 33e41b2b0a | |
Zack | da9668af19 | |
Zack Scholl | 4779c0ac0a | |
Zack Scholl | 152c27a5e9 | |
Zack | d105c253c1 | |
Zack Scholl | 8ff1fe1d2a | |
Zack Scholl | bab9ceb1bb | |
Zack | 4574e4816f | |
Zack | fa2bb39dae | |
Alex Seiler | cfad4c8010 |
|
@ -0,0 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: schollz
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
|
||||
|
||||
<!-- Please try to download latest, https://github.com/schollz/croc/releases/latest of croc before reporting a bug! -->
|
||||
|
||||
## Describe the bug
|
||||
|
||||
|
||||
## To Reproduce
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Expected behaviour
|
||||
|
||||
|
||||
## Version
|
||||
|
||||
|
||||
## Additional context
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
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.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
|
@ -0,0 +1,10 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
|
@ -0,0 +1,18 @@
|
|||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Go unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.20'
|
||||
- run: go version
|
||||
- run: go test -v ./...
|
|
@ -0,0 +1,64 @@
|
|||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: CD
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the main branch
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: schollz/croc
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm,linux/arm64,linux/386
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
|
@ -0,0 +1,123 @@
|
|||
name: Make release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.20'
|
||||
- name: Build Windows 7
|
||||
run: |
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
zip croc_${{ github.event.release.name }}_Windows7-64bit.zip croc.exe
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
zip croc_${{ github.event.release.name }}_Windows7-32bit.zip croc.exe
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- name: Prepare source tarball
|
||||
run: |
|
||||
git clone -b ${{ github.event.release.name }} --depth 1 https://github.com/schollz/croc croc-${{ github.event.release.name }}
|
||||
cd croc-${{ github.event.release.name }} && go mod tidy && go mod vendor
|
||||
cd .. && tar -czvf croc_${{ github.event.release.name }}_src.tar.gz croc-${{ github.event.release.name }}
|
||||
- name: Build files
|
||||
run: |
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
zip croc_${{ github.event.release.name }}_Windows-64bit.zip croc.exe LICENSE
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
zip croc_${{ github.event.release.name }}_Windows-32bit.zip croc.exe LICENSE
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
zip croc_${{ github.event.release.name }}_Windows-ARM.zip croc.exe LICENSE
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc.exe
|
||||
zip croc_${{ github.event.release.name }}_Windows-ARM64.zip croc.exe LICENSE
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_Linux-64bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags '-extldflags "-static"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_Linux-32bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags '-extldflags "-static"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_Linux-ARM.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags '-extldflags "-static"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_Linux-ARM64.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_macOS-64bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags '-s -extldflags "-sectcreate __TEXT __info_plist Info.plist"' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_macOS-ARM64.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=dragonfly GOARCH=amd64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_DragonFlyBSD-64bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_FreeBSD-64bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_FreeBSD-ARM64.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=386 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_NetBSD-32bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_NetBSD-64bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_NetBSD-ARM64.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_OpenBSD-64bit.tar.gz croc LICENSE
|
||||
CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 go build -ldflags '' -o croc
|
||||
tar -czvf croc_${{ github.event.release.name }}_OpenBSD-ARM64.tar.gz croc LICENSE
|
||||
- name: Create checksums.txt
|
||||
run: |
|
||||
touch croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_src.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Windows-64bit.zip >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Windows-32bit.zip >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Windows-ARM.zip >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Windows-ARM64.zip >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Windows7-64bit.zip >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Windows7-32bit.zip >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Linux-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Linux-32bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Linux-ARM.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_Linux-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_macOS-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_macOS-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_DragonFlyBSD-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_FreeBSD-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_FreeBSD-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_NetBSD-32bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_NetBSD-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_NetBSD-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_OpenBSD-64bit.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
sha256sum croc_${{ github.event.release.name }}_OpenBSD-ARM64.tar.gz >> croc_${{ github.event.release.name }}_checksums.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
croc_${{ github.event.release.name }}_checksums.txt
|
||||
croc_${{ github.event.release.name }}_src.tar.gz
|
||||
croc_${{ github.event.release.name }}_Windows-64bit.zip
|
||||
croc_${{ github.event.release.name }}_Windows-32bit.zip
|
||||
croc_${{ github.event.release.name }}_Windows-ARM.zip
|
||||
croc_${{ github.event.release.name }}_Windows-ARM64.zip
|
||||
croc_${{ github.event.release.name }}_Windows7-64bit.zip
|
||||
croc_${{ github.event.release.name }}_Windows7-32bit.zip
|
||||
croc_${{ github.event.release.name }}_Linux-64bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_Linux-32bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_Linux-ARM.tar.gz
|
||||
croc_${{ github.event.release.name }}_Linux-ARM64.tar.gz
|
||||
croc_${{ github.event.release.name }}_macOS-64bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_macOS-ARM64.tar.gz
|
||||
croc_${{ github.event.release.name }}_DragonFlyBSD-64bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_FreeBSD-64bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_FreeBSD-ARM64.tar.gz
|
||||
croc_${{ github.event.release.name }}_NetBSD-32bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_NetBSD-64bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_NetBSD-ARM64.tar.gz
|
||||
croc_${{ github.event.release.name }}_OpenBSD-64bit.tar.gz
|
||||
croc_${{ github.event.release.name }}_OpenBSD-ARM64.tar.gz
|
|
@ -0,0 +1,27 @@
|
|||
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
|
||||
#
|
||||
# You can adjust the behavior by modifying this file.
|
||||
# For more information, see:
|
||||
# https://github.com/actions/stale
|
||||
name: Mark stale issues and pull requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '19 12 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'Stale issue message'
|
||||
stale-pr-message: 'Stale pull request message'
|
||||
stale-issue-label: 'no-issue-activity'
|
||||
stale-pr-label: 'no-pr-activity'
|
|
@ -0,0 +1,14 @@
|
|||
name: Publish to Winget
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: schollz.croc
|
||||
installers-regex: '_Windows-\w+\.zip$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
|
@ -1,3 +1,13 @@
|
|||
# IDEs
|
||||
.idea/
|
||||
# Binaries
|
||||
/croc
|
||||
/croc.exe
|
||||
zsh_autocomplete
|
||||
bash_autocomplete
|
||||
dist
|
||||
bin
|
||||
croc-stdin*
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
src/utils/bigfile.test
|
||||
test1/
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
project_name: croc
|
||||
build:
|
||||
main: main.go
|
||||
binary: croc
|
||||
ldflags: -s -w -X main.Version="v{{.Version}}-{{.Date}}"
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
- netbsd
|
||||
- openbsd
|
||||
- dragonfly
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm:
|
||||
- 7
|
||||
nfpms:
|
||||
-
|
||||
formats:
|
||||
- deb
|
||||
vendor: "schollz.com"
|
||||
homepage: "https://schollz.com/software/croc/"
|
||||
maintainer: "Zack Scholl <zack.scholl@gmail.com>"
|
||||
description: "A simple, secure, and fast way to transfer data."
|
||||
license: "MIT"
|
||||
file_name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
|
||||
replacements:
|
||||
amd64: 64bit
|
||||
386: 32bit
|
||||
arm: ARM
|
||||
arm64: ARM64
|
||||
darwin: macOS
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
openbsd: OpenBSD
|
||||
netbsd: NetBSD
|
||||
freebsd: FreeBSD
|
||||
dragonfly: DragonFlyBSD
|
||||
archives:
|
||||
-
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
|
||||
replacements:
|
||||
amd64: 64bit
|
||||
386: 32bit
|
||||
arm: ARM
|
||||
arm64: ARM64
|
||||
darwin: macOS
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
openbsd: OpenBSD
|
||||
netbsd: NetBSD
|
||||
freebsd: FreeBSD
|
||||
dragonfly: DragonFlyBSD
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
- zsh_autocomplete
|
||||
- bash_autocomplete
|
||||
|
||||
brews:
|
||||
-
|
||||
tap:
|
||||
owner: schollz
|
||||
name: homebrew-tap
|
||||
folder: Formula
|
||||
description: "croc is a tool that allows any two computers to simply and securely transfer files and folders."
|
||||
homepage: "https://schollz.com/software/croc/"
|
||||
install: |
|
||||
bin.install "croc"
|
||||
|
||||
test: |
|
||||
system "#{bin}/croc --version"
|
||||
|
||||
scoop:
|
||||
bucket:
|
||||
owner: schollz
|
||||
name: scoop-bucket
|
||||
homepage: "https://schollz.com/software/croc/"
|
||||
description: "croc is a tool that allows any two computers to simply and securely transfer files and folders."
|
||||
license: MIT
|
||||
announce:
|
||||
twitter:
|
||||
# Wether its enabled or not.
|
||||
# Defaults to false.
|
||||
enabled: false
|
23
.travis.yml
23
.travis.yml
|
@ -1,16 +1,23 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.9
|
||||
- tip
|
||||
|
||||
env:
|
||||
- "PATH=/home/travis/gopath/bin:$PATH"
|
||||
|
||||
before_install:
|
||||
- go get github.com/gosuri/uiprogress
|
||||
- go get github.com/schollz/mnemonicode
|
||||
- go get github.com/pkg/errors
|
||||
- go get github.com/sirupsen/logrus
|
||||
- go get github.com/verybluebot/tarinator-go
|
||||
install: true
|
||||
|
||||
script:
|
||||
- go test -v
|
||||
- env GO111MODULE=on go build -v
|
||||
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/compress
|
||||
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/croc
|
||||
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/crypt
|
||||
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/tcp
|
||||
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/utils
|
||||
- env GO111MODULE=on go test -v -cover github.com/schollz/croc/v10/src/comm
|
||||
|
||||
branches:
|
||||
except:
|
||||
- dev
|
||||
- win
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at zack.scholl@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
|
@ -0,0 +1,16 @@
|
|||
FROM golang:1.22-alpine as builder
|
||||
RUN apk add --no-cache git gcc musl-dev
|
||||
WORKDIR /go/croc
|
||||
COPY . .
|
||||
RUN go build -v -ldflags="-s -w"
|
||||
|
||||
FROM alpine:latest
|
||||
EXPOSE 9009
|
||||
EXPOSE 9010
|
||||
EXPOSE 9011
|
||||
EXPOSE 9012
|
||||
EXPOSE 9013
|
||||
COPY --from=builder /go/croc/croc /go/croc/croc-entrypoint.sh /
|
||||
USER nobody
|
||||
ENTRYPOINT ["/croc-entrypoint.sh"]
|
||||
CMD ["relay"]
|
|
@ -1,69 +0,0 @@
|
|||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
packages = ["."]
|
||||
revision = "77ed807830b4df581417e7f89eb81d4872832b72"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mars9/crypt"
|
||||
packages = ["."]
|
||||
revision = "65899cf653ff022fe5c7fe504b439feed9e7e0fc"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/schollz/mnemonicode"
|
||||
packages = ["."]
|
||||
revision = "15c9654387fad6d257aa28f9be57b9f124101955"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/schollz/progressbar"
|
||||
packages = ["."]
|
||||
revision = "b8e001516e13bd132ed253bf1b175dbfb5cdf308"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e"
|
||||
version = "v1.0.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/verybluebot/tarinator-go"
|
||||
packages = ["."]
|
||||
revision = "f75724675c91d0c731b69c81e0985de07663f007"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["pbkdf2","scrypt","ssh/terminal"]
|
||||
revision = "2509b142fb2b797aa7587dad548f113b2c0f20ce"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix","windows"]
|
||||
revision = "0649f9fe46d917da5591af4d4784b6badd31d7e5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/text"
|
||||
packages = ["transform"]
|
||||
revision = "6eab0e8f74e86c598ec3b6fad4888e0c11482d48"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "bdbf624014ff879def63602194ddf9c775d9d8461e0b2e687314dc8b855ffc50"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
38
Gopkg.toml
38
Gopkg.toml
|
@ -1,38 +0,0 @@
|
|||
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/gosuri/uiprogress"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/schollz/mnemonicode"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
version = "1.0.3"
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Zack
|
||||
Copyright (c) 2017-2024 Zack
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
366
README.md
366
README.md
|
@ -1,116 +1,250 @@
|
|||
<p align="center">
|
||||
<img
|
||||
src="https://user-images.githubusercontent.com/6550035/31846899-2b8a7034-b5cf-11e7-9643-afe552226c59.png"
|
||||
width="100%" border="0" alt="croc">
|
||||
<br>
|
||||
<a href="https://travis-ci.org/schollz/croc"><img src="https://travis-ci.org/schollz/croc.svg?branch=master" alt="Build Status"></a>
|
||||
<a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/badge/version-0.2.0-green.svg?style=flat-square" alt="Version"></a>
|
||||
<a href="https://gitter.im/schollz/croc?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge"><img src="https://img.shields.io/badge/chat-on%20gitter-green.svg?style=flat-square" alt="Gitter"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/schollz/croc"><img src="https://goreportcard.com/badge/github.com/schollz/croc" alt="Go Report Card"></a>
|
||||
<a href="https://godoc.org/github.com/schollz/croc"><img src="https://godoc.org/github.com/schollz/croc?status.svg" alt="GoDoc"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Secure transfer of stuff from one side of the internet to the other.</p>
|
||||
|
||||
This is more or less (but mostly *less*) a Golang port of [@warner's](https://github.com/warner) [*magic-wormhole*](https://github.com/warner/magic-wormhole) which allows you to directly transfer files and folders between computers. I decided to make this because I wanted to send my friend Jessie a file using *magic-wormhole* and when I told Jessie how to install the dependencies she made this face: :sob:. So, nominally, *croc* does the same thing (encrypted file transfer directly between computers) without dependencies so you can just double-click on your computer, even if you use Windows.
|
||||
|
||||
**Don't we have enough open-source peer-to-peer file-transfer utilities?**
|
||||
|
||||
[There](https://github.com/cowbell/sharedrop) [are](https://github.com/webtorrent/instant.io) [great](https://github.com/kern/filepizza) [tools](https://github.com/warner/magic-wormhole) [that](https://github.com/zerotier/toss) [already](https://github.com/ipfs/go-ipfs) [do](https://github.com/zerotier/toss) [this](https://github.com/nils-werner/zget). But, no we don't, because after review, [I found it was useful to make a new one](https://schollz.github.io/sending-a-file/).
|
||||
|
||||
# Example
|
||||
|
||||
_These two gifs should run in sync if you force-reload (Ctl+F5)_
|
||||
|
||||
**Sender:**
|
||||
|
||||
![send](https://user-images.githubusercontent.com/6550035/31864532-ad80c6ae-b71b-11e7-91f9-bcba8143d3cf.gif)
|
||||
|
||||
**Receiver:**
|
||||
|
||||
![receive](https://user-images.githubusercontent.com/6550035/31864531-ad6e22c4-b71b-11e7-901a-02a210057cf1.gif)
|
||||
|
||||
|
||||
**Sender:**
|
||||
|
||||
```
|
||||
$ croc -send croc.exe
|
||||
Sending 4.4 MB file named 'croc.exe'
|
||||
Code is: 4-cement-galaxy-alpha
|
||||
|
||||
Sending (->24.65.41.43:50843)..
|
||||
0s [==========================================================] 100%
|
||||
File sent.
|
||||
```
|
||||
|
||||
**Receiver:**
|
||||
|
||||
```
|
||||
$ croc
|
||||
Enter receive code: 4-cement-galaxy-alpha
|
||||
Receiving file (4.4 MB) into: croc.exe
|
||||
ok? (y/n): y
|
||||
|
||||
Receiving (<-50.32.38.188:50843)..
|
||||
0s [==========================================================] 100%
|
||||
Received file written to croc.exe
|
||||
```
|
||||
|
||||
Note, by default, you don't need any arguments for receiving! This makes it possible for you to just double click the executable to run (nice for those of us that aren't computer wizards).
|
||||
|
||||
|
||||
# Install
|
||||
|
||||
[Download the latest release for your system](https://github.com/schollz/croc/releases/latest).
|
||||
|
||||
Or, you can [install Go](https://golang.org/dl/) and build from source with `go get github.com/schollz/croc`.
|
||||
|
||||
|
||||
|
||||
# How does it work?
|
||||
|
||||
*croc* is similar to [magic-wormhole](https://github.com/warner/magic-wormhole#design) in spirit and design. Like *magic-wormhole*, *croc* generates a code phrase for you to share with your friend which allows secure end-to-end transfering of files and folders through a intermediary relay that connects the TCP ports between the two computers.
|
||||
|
||||
In *croc*, code phrase is 16 random bits that are [menemonic encoded](http://web.archive.org/web/20101031205747/http://www.tothink.com/mnemonic/) plus a prepended integer to specify number of threads. This code phrase is hashed using sha256 and sent to a relay which maps that key to that connection. When the relay finds a matching key for both the receiver and the sender (i.e. they both have the same code phrase), then the sender transmits the encrypted metadata to the receiver through the relay. Then the receiver decrypts and reviews the metadata (file name, size), and chooses whether to consent to the transfer.
|
||||
|
||||
After the receiver consents to the transfer, the sender transmits encrypted data through the relay. The relay setups up [Go channels](https://golang.org/doc/effective_go.html?h=chan#channels) for each connection which pipes all the data incoming from that sender's connection out to the receiver's connection. After the transmission the channels are destroyed and all the connection and meta data information is wiped from the relay server. The encrypted file data never is stored on the relay.
|
||||
|
||||
**Encryption**
|
||||
|
||||
Encryption uses PBKDF2 (see [RFC2898](http://www.ietf.org/rfc/rfc2898.txt)) where the code phrase shared between the sender and receiver is used as the passphrase. For each of the two encrypted data blocks (metadata stored on relay server, and file data transmitted), a random 8-byte salt is used and a IV is generated according to [NIST Recommendation for Block ciphers, Section 8.2](http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf).
|
||||
|
||||
|
||||
**Decryption**
|
||||
|
||||
On the receiver's computer, each piece of received encrypted data is written to a separate file. These files are concatenated and then decrypted. The hash of the decrypted file is then checked against the hash transmitted from the sender (part of the meta data block).
|
||||
|
||||
## Run your own relay
|
||||
|
||||
*croc* relies on a TCP relay to staple the parallel incoming and outgoing connections. The relay temporarily stores connection information and the encrypted meta information. The default uses a public relay at, `cowyo.com`, which has no guarantees except that I guarantee to turn if off as soon as it gets abused ([click here to check the current status of the public relay](https://stats.uptimerobot.com/lOwJYIgRm)).
|
||||
|
||||
I recommend you run your own relay, it is very easy. On your server, `your-server.com`, just run
|
||||
|
||||
```
|
||||
$ croc -relay
|
||||
```
|
||||
|
||||
Now, when you use *croc* to send and receive you should add `-server your-server.com` to use your relay server.
|
||||
|
||||
_Note:_ If you are behind a firewall, make sure to open up TCP ports 27001-27009.
|
||||
|
||||
# Contribute
|
||||
|
||||
I am awed by all the [great contributions](#acknowledgements) made! If you feel like contributing, in any way, by all means you can send an Issue, a PR, ask a question, or tweet me ([@yakczar](http://ctt.ec/Rq054)).
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
|
||||
# Acknowledgements
|
||||
|
||||
Thanks...
|
||||
|
||||
- ...[@warner](https://github.com/warner) for the [idea](https://github.com/warner/magic-wormhole).
|
||||
- ...[@tscholl2](https://github.com/tscholl2) for the [encryption gists](https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28).
|
||||
- ...[@skorokithakis](https://github.com/skorokithakis) for [code on proxying two connections](https://www.stavros.io/posts/proxying-two-connections-go/).
|
||||
- ...for making pull requests [@Girbons](https://github.com/Girbons), [@techtide](https://github.com/techtide), [@heymatthew](https://github.com/heymatthew), [@Lunsford94](https://github.com/Lunsford94), [@lummie](https://github.com/lummie), [@jesuiscamille](https://github.com/jesuiscamille), [@threefjord](https://github.com/threefjord), [@marcossegovia](https://github.com/marcossegovia), [@csleong98](https://github.com/csleong98), [@afotescu](https://github.com/afotescu), [@callmefever](https://github.com/callmefever), [@El-JojA](https://github.com/El-JojA), [@anatolyyyyyy](https://github.com/anatolyyyyyy)!
|
||||
|
||||
<p align="center">
|
||||
<img
|
||||
src="https://user-images.githubusercontent.com/6550035/46709024-9b23ad00-cbf6-11e8-9fb2-ca8b20b7dbec.jpg"
|
||||
width="408px" border="0" alt="croc">
|
||||
<br>
|
||||
<a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/badge/version-v10.0.7-brightgreen.svg?style=flat-square" alt="Version"></a>
|
||||
<a href="https://github.com/schollz/croc/actions/workflows/ci.yml"><img
|
||||
src="https://github.com/schollz/croc/actions/workflows/ci.yml/badge.svg" alt="Build
|
||||
Status"></a>
|
||||
<p align="center">This project is supported by <a href="https://github.com/sponsors/schollz">Github sponsors</a>.</p>
|
||||
|
||||
`croc` is a tool that allows any two computers to simply and securely transfer files and folders. AFAIK, *croc* is the only CLI file-transfer tool that does **all** of the following:
|
||||
|
||||
- allows **any two computers** to transfer data (using a relay)
|
||||
- provides **end-to-end encryption** (using PAKE)
|
||||
- enables easy **cross-platform** transfers (Windows, Linux, Mac)
|
||||
- allows **multiple file** transfers
|
||||
- allows **resuming transfers** that are interrupted
|
||||
- local server or port-forwarding **not needed**
|
||||
- **ipv6-first** with ipv4 fallback
|
||||
- can **use proxy**, like tor
|
||||
|
||||
For more information about `croc`, see [my blog post](https://schollz.com/software/croc6) or read a [recent interview I did](https://console.substack.com/p/console-91).
|
||||
|
||||
![Example](src/install/customization.gif)
|
||||
|
||||
## Install
|
||||
|
||||
Download [the latest release for your system](https://github.com/schollz/croc/releases/latest), or install a release from the command-line:
|
||||
|
||||
```
|
||||
curl https://getcroc.schollz.com | bash
|
||||
```
|
||||
|
||||
|
||||
On macOS you can install the latest release with [Homebrew](https://brew.sh/):
|
||||
|
||||
```
|
||||
brew install croc
|
||||
```
|
||||
|
||||
On macOS you can also install the latest release with [MacPorts](https://macports.org/):
|
||||
|
||||
```
|
||||
sudo port selfupdate
|
||||
sudo port install croc
|
||||
```
|
||||
|
||||
On Windows you can install the latest release with [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org), or [Winget](https://learn.microsoft.com/en-us/windows/package-manager/):
|
||||
|
||||
```
|
||||
scoop install croc
|
||||
```
|
||||
|
||||
```
|
||||
choco install croc
|
||||
```
|
||||
|
||||
```
|
||||
winget install schollz.croc
|
||||
```
|
||||
|
||||
On Unix you can install the latest release with [Nix](https://nixos.org/nix):
|
||||
|
||||
```
|
||||
nix-env -i croc
|
||||
```
|
||||
|
||||
|
||||
On Alpine Linux you have to install dependencies first:
|
||||
|
||||
```
|
||||
apk add bash coreutils
|
||||
wget -qO- https://getcroc.schollz.com | bash
|
||||
```
|
||||
|
||||
On Arch Linux you can install the latest release with `pacman`:
|
||||
|
||||
```
|
||||
pacman -S croc
|
||||
```
|
||||
|
||||
On Fedora you can install with `dnf`:
|
||||
|
||||
```
|
||||
dnf install croc
|
||||
```
|
||||
|
||||
On Gentoo you can install with `portage`:
|
||||
```
|
||||
emerge net-misc/croc
|
||||
```
|
||||
|
||||
On Termux you can install with `pkg`:
|
||||
|
||||
```
|
||||
pkg install croc
|
||||
```
|
||||
|
||||
On FreeBSD you can install with `pkg`:
|
||||
|
||||
```
|
||||
pkg install croc
|
||||
```
|
||||
|
||||
Or, you can [install Go](https://golang.org/dl/) and build from source (requires Go 1.17+):
|
||||
|
||||
```
|
||||
go install github.com/schollz/croc/v10@latest
|
||||
```
|
||||
|
||||
On Android there is a 3rd party F-Droid app [available to download](https://f-droid.org/en/packages/com.github.howeyc.crocgui/).
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
To send a file, simply do:
|
||||
|
||||
```
|
||||
$ croc send [file(s)-or-folder]
|
||||
Sending 'file-or-folder' (X MB)
|
||||
Code is: code-phrase
|
||||
```
|
||||
|
||||
Then to receive the file (or folder) on another computer, you can just do
|
||||
|
||||
```
|
||||
croc code-phrase
|
||||
```
|
||||
|
||||
The code phrase is used to establish password-authenticated key agreement ([PAKE](https://en.wikipedia.org/wiki/Password-authenticated_key_agreement)) which generates a secret key for the sender and recipient to use for end-to-end encryption.
|
||||
|
||||
There are a number of configurable options (see `--help`). A set of options (like custom relay, ports, and code phrase) can be set using `--remember`.
|
||||
|
||||
### Custom code phrase
|
||||
|
||||
You can send with your own code phrase (must be more than 6 characters).
|
||||
|
||||
```
|
||||
croc send --code [code-phrase] [file(s)-or-folder]
|
||||
```
|
||||
|
||||
### Allow overwriting without prompt
|
||||
|
||||
By default, croc will prompt whether to overwrite a file. You can automatically overwrite files by using the `--overwrite` flag (recipient only). For example, receive a file to automatically overwrite:
|
||||
|
||||
```
|
||||
croc --yes --overwrite <code>
|
||||
```
|
||||
|
||||
|
||||
### Use pipes - stdin and stdout
|
||||
|
||||
You can pipe to `croc`:
|
||||
|
||||
```
|
||||
cat [filename] | croc send
|
||||
```
|
||||
|
||||
In this case `croc` will automatically use the stdin data and send and assign a filename like "croc-stdin-123456789". To receive to `stdout` at you can always just use the `--yes` will automatically approve the transfer and pipe it out to `stdout`.
|
||||
|
||||
```
|
||||
croc --yes [code-phrase] > out
|
||||
```
|
||||
|
||||
All of the other text printed to the console is going to `stderr` so it will not interfere with the message going to `stdout`.
|
||||
|
||||
|
||||
### Send text
|
||||
|
||||
Sometimes you want to send URLs or short text. In addition to piping, you can easily send text with `croc`:
|
||||
|
||||
```
|
||||
croc send --text "hello world"
|
||||
```
|
||||
|
||||
This will automatically tell the receiver to use `stdout` when they receive the text so it will be displayed.
|
||||
|
||||
|
||||
### Use a proxy
|
||||
|
||||
You can use a proxy as your connection to the relay by adding a proxy address with `--socks5`. For example, you can send via a tor relay:
|
||||
|
||||
```
|
||||
croc --socks5 "127.0.0.1:9050" send SOMEFILE
|
||||
```
|
||||
|
||||
### Change encryption curve
|
||||
|
||||
You can choose from several different elliptic curves to use for encryption by using the `--curve` flag. Only the recipient can choose the curve. For example, receive a file using the P-521 curve:
|
||||
|
||||
```
|
||||
croc --curve p521 <codephrase>
|
||||
```
|
||||
|
||||
Available curves are P-256, P-348, P-521 and SIEC. P-256 is the default curve.
|
||||
|
||||
### Change hash algorithm
|
||||
|
||||
You can choose from several different hash algorithms. The default is the `xxhash` algorithm which is fast and thorough. If you want to optimize for speed you can use the `imohash` algorithm which is even faster, but since it samples files (versus reading the whole file) it can mistakenly determine that a file is the same on the two computers transferring - though this is only a problem if you are syncing files versus sending a new file to a computer.
|
||||
|
||||
```
|
||||
croc send --hash imohash SOMEFILE
|
||||
```
|
||||
|
||||
### Self-host relay
|
||||
|
||||
The relay is needed to staple the parallel incoming and outgoing connections. By default, `croc` uses a public relay but you can also run your own relay:
|
||||
|
||||
```
|
||||
croc relay
|
||||
```
|
||||
|
||||
By default it uses TCP ports 9009-9013. Make sure to open those up. You can customize the ports (e.g. `croc relay --ports 1111,1112`), but you must have a minimum of **2** ports for the relay. The first port is for communication and the subsequent ports are used for the multiplexed data transfer.
|
||||
|
||||
You can send files using your relay by entering `--relay` to change the relay that you are using if you want to custom host your own.
|
||||
|
||||
```
|
||||
croc --relay "myrelay.example.com:9009" send [filename]
|
||||
```
|
||||
|
||||
Note, when sending, you only need to include the first port (the communication port). The subsequent ports for data transfer will be transmitted back to the user from the relay.
|
||||
|
||||
#### Self-host relay (docker)
|
||||
|
||||
If it's easier you can also run a relay with Docker:
|
||||
|
||||
|
||||
```
|
||||
docker run -d -p 9009-9013:9009-9013 -e CROC_PASS='YOURPASSWORD' schollz/croc
|
||||
```
|
||||
|
||||
Be sure to include the password for the relay otherwise any requests will be rejected.
|
||||
|
||||
```
|
||||
croc --pass YOURPASSWORD --relay "myreal.example.com:9009" send [filename]
|
||||
```
|
||||
|
||||
Note: when including `--pass YOURPASSWORD` you can instead pass a file with the password, e.g. `--pass FILEWITHPASSWORD`.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
`croc` has gone through many iterations, and I am awed by all the great contributions! If you feel like contributing, in any way, by all means you can send an Issue, a PR, or ask a question.
|
||||
|
||||
Thanks [@warner](https://github.com/warner) for the [idea](https://github.com/warner/magic-wormhole), [@tscholl2](https://github.com/tscholl2) for the [encryption gists](https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28), [@skorokithakis](https://github.com/skorokithakis) for [code on proxying two connections](https://www.stavros.io/posts/proxying-two-connections-go/). Finally thanks for making pull requests [@maximbaz](https://github.com/maximbaz), [@meyermarcel](https://github.com/meyermarcel), [@Girbons](https://github.com/Girbons), [@techtide](https://github.com/techtide), [@heymatthew](https://github.com/heymatthew), [@Lunsford94](https://github.com/Lunsford94), [@lummie](https://github.com/lummie), [@jesuiscamille](https://github.com/jesuiscamille), [@threefjord](https://github.com/threefjord), [@marcossegovia](https://github.com/marcossegovia), [@csleong98](https://github.com/csleong98), [@afotescu](https://github.com/afotescu), [@callmefever](https://github.com/callmefever), [@El-JojA](https://github.com/El-JojA), [@anatolyyyyyy](https://github.com/anatolyyyyyy), [@goggle](https://github.com/goggle), [@smileboywtu](https://github.com/smileboywtu), [@nicolashardy](https://github.com/nicolashardy), [@fbartels](https://github.com/fbartels), [@rkuprov](https://github.com/rkuprov), [@hreese](https://github.com/hreese), [@xenrox](https://github.com/xenrox) and [Ipar](https://github.com/lpar)!
|
||||
|
|
609
connect.go
609
connect.go
|
@ -1,609 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/schollz/progressbar"
|
||||
"github.com/verybluebot/tarinator-go"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Connection struct {
|
||||
Server string
|
||||
File FileMetaData
|
||||
NumberOfConnections int
|
||||
Code string
|
||||
HashedCode string
|
||||
Path string
|
||||
IsSender bool
|
||||
AskPath bool
|
||||
Debug bool
|
||||
DontEncrypt bool
|
||||
Wait bool
|
||||
bar *progressbar.ProgressBar
|
||||
rate int
|
||||
}
|
||||
|
||||
type FileMetaData struct {
|
||||
Name string
|
||||
Size int
|
||||
Hash string
|
||||
Path string
|
||||
IsDir bool
|
||||
IsEncrypted bool
|
||||
}
|
||||
|
||||
const (
|
||||
crocReceiveDir = "croc_received"
|
||||
tmpTarGzFileName = "to_send.tmp.tar.gz"
|
||||
)
|
||||
|
||||
func NewConnection(flags *Flags) (*Connection, error) {
|
||||
c := new(Connection)
|
||||
c.Debug = flags.Debug
|
||||
c.DontEncrypt = flags.DontEncrypt
|
||||
c.Wait = flags.Wait
|
||||
c.Server = flags.Server
|
||||
c.Code = flags.Code
|
||||
c.NumberOfConnections = flags.NumberOfConnections
|
||||
c.rate = flags.Rate
|
||||
if len(flags.File) > 0 {
|
||||
// check wether the file is a dir
|
||||
info, err := os.Stat(flags.File)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
if info.Mode().IsDir() { // if our file is a dir
|
||||
fmt.Println("compressing directory...")
|
||||
|
||||
// we "tarify" the file
|
||||
err = tarinator.Tarinate([]string{flags.File}, path.Base(flags.File)+".tar")
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
// now, we change the target file name to match the new archive created
|
||||
flags.File = path.Base(flags.File) + ".tar"
|
||||
// we set the value IsDir to true
|
||||
c.File.IsDir = true
|
||||
}
|
||||
c.File.Name = path.Base(flags.File)
|
||||
c.File.Path = path.Dir(flags.File)
|
||||
c.IsSender = true
|
||||
} else {
|
||||
c.IsSender = false
|
||||
c.AskPath = flags.PathSpec
|
||||
c.Path = flags.Path
|
||||
}
|
||||
|
||||
log.SetFormatter(&log.TextFormatter{})
|
||||
if c.Debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
} else {
|
||||
log.SetLevel(log.WarnLevel)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Connection) Run() error {
|
||||
forceSingleThreaded := false
|
||||
if c.IsSender {
|
||||
fsize, err := FileSize(path.Join(c.File.Path, c.File.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fsize < MAX_NUMBER_THREADS*BUFFERSIZE {
|
||||
forceSingleThreaded = true
|
||||
log.Debug("forcing single thread")
|
||||
}
|
||||
}
|
||||
log.Debug("checking code validity")
|
||||
for {
|
||||
// check code
|
||||
goodCode := true
|
||||
m := strings.Split(c.Code, "-")
|
||||
log.Debug(m)
|
||||
numThreads, errParse := strconv.Atoi(m[0])
|
||||
if len(m) < 2 {
|
||||
goodCode = false
|
||||
log.Debug("code too short")
|
||||
} else if numThreads > MAX_NUMBER_THREADS || numThreads < 1 || (forceSingleThreaded && numThreads != 1) {
|
||||
c.NumberOfConnections = MAX_NUMBER_THREADS
|
||||
goodCode = false
|
||||
log.Debug("incorrect number of threads")
|
||||
} else if errParse != nil {
|
||||
goodCode = false
|
||||
log.Debug("problem parsing threads")
|
||||
}
|
||||
log.Debug(m)
|
||||
log.Debug(goodCode)
|
||||
if !goodCode {
|
||||
if c.IsSender {
|
||||
if forceSingleThreaded {
|
||||
c.NumberOfConnections = 1
|
||||
}
|
||||
c.Code = strconv.Itoa(c.NumberOfConnections) + "-" + GetRandomName()
|
||||
} else {
|
||||
if len(c.Code) != 0 {
|
||||
fmt.Println("Code must begin with number of threads (e.g. 3-some-code)")
|
||||
}
|
||||
c.Code = getInput("Enter receive code: ")
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
// assign number of connections
|
||||
c.NumberOfConnections, _ = strconv.Atoi(strings.Split(c.Code, "-")[0])
|
||||
|
||||
if c.IsSender {
|
||||
if c.DontEncrypt {
|
||||
// don't encrypt
|
||||
CopyFile(path.Join(c.File.Path, c.File.Name), c.File.Name+".enc")
|
||||
c.File.IsEncrypted = false
|
||||
} else {
|
||||
// encrypt
|
||||
log.Debug("encrypting...")
|
||||
if err := EncryptFile(path.Join(c.File.Path, c.File.Name), c.File.Name+".enc", c.Code); err != nil {
|
||||
return err
|
||||
}
|
||||
c.File.IsEncrypted = true
|
||||
}
|
||||
// split file into pieces to send
|
||||
if err := SplitFile(c.File.Name+".enc", c.NumberOfConnections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get file hash
|
||||
var err error
|
||||
c.File.Hash, err = HashFile(path.Join(c.File.Path, c.File.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// get file size
|
||||
c.File.Size, err = FileSize(c.File.Name + ".enc")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// remove the file now since we still have pieces
|
||||
if err := os.Remove(c.File.Name + ".enc"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove compressed archive
|
||||
if c.File.IsDir {
|
||||
log.Debug("removing archive: " + c.File.Name)
|
||||
if err := os.Remove(c.File.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.File.IsDir {
|
||||
fmt.Printf("Sending %s folder named '%s'\n", humanize.Bytes(uint64(c.File.Size)), c.File.Name[:len(c.File.Name)-4])
|
||||
} else {
|
||||
fmt.Printf("Sending %s file named '%s'\n", humanize.Bytes(uint64(c.File.Size)), c.File.Name)
|
||||
|
||||
}
|
||||
fmt.Printf("Code is: %s\n", c.Code)
|
||||
}
|
||||
|
||||
return c.runClient()
|
||||
}
|
||||
|
||||
// runClient spawns threads for parallel uplink/downlink via TCP
|
||||
func (c *Connection) runClient() error {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"code": c.Code,
|
||||
"sender?": c.IsSender,
|
||||
})
|
||||
|
||||
startTime := time.Now()
|
||||
c.HashedCode = Hash(c.Code)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(c.NumberOfConnections)
|
||||
|
||||
if !c.Debug {
|
||||
c.bar = progressbar.New(c.File.Size)
|
||||
}
|
||||
type responsesStruct struct {
|
||||
gotTimeout bool
|
||||
gotOK bool
|
||||
gotResponse bool
|
||||
gotConnectionInUse bool
|
||||
notPresent bool
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
responses := new(responsesStruct)
|
||||
for id := 0; id < c.NumberOfConnections; id++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
port := strconv.Itoa(27001 + id)
|
||||
connection, err := net.Dial("tcp", c.Server+":"+port)
|
||||
if err != nil {
|
||||
if c.Server == "cowyo.com" {
|
||||
fmt.Println("\nCheck http://bit.ly/croc-relay to see if the public server is down or contact the webmaster: @yakczar")
|
||||
} else {
|
||||
fmt.Printf("\nCould not connect to relay %s\n", c.Server)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
message := receiveMessage(connection)
|
||||
logger.Debugf("relay says: %s", message)
|
||||
if c.IsSender {
|
||||
logger.Debugf("telling relay: %s", "s."+c.Code)
|
||||
metaData, err := json.Marshal(c.File)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
encryptedMetaData, salt, iv := Encrypt(metaData, c.Code)
|
||||
sendMessage("s."+c.HashedCode+"."+hex.EncodeToString(encryptedMetaData)+"-"+salt+"-"+iv, connection)
|
||||
} else {
|
||||
logger.Debugf("telling relay: %s", "r."+c.Code)
|
||||
if c.Wait {
|
||||
// tell server to wait for sender
|
||||
sendMessage("r."+c.HashedCode+".0.0.0", connection)
|
||||
} else {
|
||||
// tell server to cancel if sender doesn't exist
|
||||
sendMessage("c."+c.HashedCode+".0.0.0", connection)
|
||||
}
|
||||
}
|
||||
if c.IsSender { // this is a sender
|
||||
logger.Debug("waiting for ok from relay")
|
||||
message = receiveMessage(connection)
|
||||
if message == "timeout" {
|
||||
responses.Lock()
|
||||
responses.gotTimeout = true
|
||||
responses.Unlock()
|
||||
fmt.Println("You've just exceeded limit waiting time.")
|
||||
return
|
||||
}
|
||||
if message == "no" {
|
||||
if id == 0 {
|
||||
fmt.Println("The specifed code is already in use by a sender.")
|
||||
}
|
||||
responses.Lock()
|
||||
responses.gotConnectionInUse = true
|
||||
responses.Unlock()
|
||||
} else {
|
||||
logger.Debug("got ok from relay")
|
||||
if id == 0 {
|
||||
fmt.Printf("\nSending (->%s)..\n", message)
|
||||
}
|
||||
// wait for pipe to be made
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// Write data from file
|
||||
logger.Debug("send file")
|
||||
startTime = time.Now()
|
||||
c.bar.Reset()
|
||||
if err := c.sendFile(id, connection); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
} else { // this is a receiver
|
||||
logger.Debug("waiting for meta data from sender")
|
||||
message = receiveMessage(connection)
|
||||
if message == "no" {
|
||||
if id == 0 {
|
||||
fmt.Println("The specifed code is already in use by a sender.")
|
||||
}
|
||||
responses.Lock()
|
||||
responses.gotConnectionInUse = true
|
||||
responses.Unlock()
|
||||
} else {
|
||||
m := strings.Split(message, "-")
|
||||
encryptedData, salt, iv, sendersAddress := m[0], m[1], m[2], m[3]
|
||||
if sendersAddress == "0.0.0.0" {
|
||||
responses.Lock()
|
||||
responses.notPresent = true
|
||||
responses.Unlock()
|
||||
time.Sleep(1 * time.Second)
|
||||
return
|
||||
}
|
||||
// have the main thread ask for the okay
|
||||
if id == 0 {
|
||||
encryptedBytes, err := hex.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
decryptedBytes, _ := Decrypt(encryptedBytes, c.Code, salt, iv, c.DontEncrypt)
|
||||
err = json.Unmarshal(decryptedBytes, &c.File)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
log.Debugf("meta data received: %v", c.File)
|
||||
fType := "file"
|
||||
fName := path.Join(c.Path, c.File.Name)
|
||||
if c.File.IsDir {
|
||||
fType = "folder"
|
||||
fName = fName[:len(fName)-4]
|
||||
}
|
||||
if _, err := os.Stat(path.Join(c.Path, c.File.Name)); os.IsNotExist(err) {
|
||||
fmt.Printf("Receiving %s (%s) into: %s\n", fType, humanize.Bytes(uint64(c.File.Size)), fName)
|
||||
} else {
|
||||
fmt.Printf("Overwriting %s %s (%s)\n", fType, fName, humanize.Bytes(uint64(c.File.Size)))
|
||||
}
|
||||
var sentFileNames []string
|
||||
|
||||
if c.AskPath {
|
||||
getPath := getInput("path: ")
|
||||
if len(getPath) > 0 {
|
||||
c.Path = path.Clean(getPath)
|
||||
}
|
||||
}
|
||||
if fileAlreadyExists(sentFileNames, c.File.Name) {
|
||||
fmt.Printf("Will not overwrite file!")
|
||||
os.Exit(1)
|
||||
}
|
||||
getOK := getInput("ok? (y/n): ")
|
||||
if getOK == "y" {
|
||||
responses.Lock()
|
||||
responses.gotOK = true
|
||||
responses.Unlock()
|
||||
sentFileNames = append(sentFileNames, c.File.Name)
|
||||
}
|
||||
responses.Lock()
|
||||
responses.gotResponse = true
|
||||
responses.Unlock()
|
||||
}
|
||||
// wait for the main thread to get the okay
|
||||
for limit := 0; limit < 1000; limit++ {
|
||||
responses.Lock()
|
||||
gotResponse := responses.gotResponse
|
||||
responses.Unlock()
|
||||
if gotResponse {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
responses.RLock()
|
||||
gotOK := responses.gotOK
|
||||
responses.RUnlock()
|
||||
if !gotOK {
|
||||
sendMessage("not ok", connection)
|
||||
} else {
|
||||
sendMessage("ok", connection)
|
||||
logger.Debug("receive file")
|
||||
if id == 0 {
|
||||
fmt.Printf("\n\nReceiving (<-%s)..\n", sendersAddress)
|
||||
}
|
||||
startTime = time.Now()
|
||||
c.bar.SetMax(c.File.Size)
|
||||
c.bar.Reset()
|
||||
if err := c.receiveFile(id, connection); err != nil {
|
||||
log.Error(errors.Wrap(err, "Problem receiving the file: "))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}(id)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
responses.Lock()
|
||||
defer responses.Unlock()
|
||||
if responses.gotConnectionInUse {
|
||||
return nil // connection was in use, just quit cleanly
|
||||
}
|
||||
|
||||
if c.IsSender {
|
||||
if responses.gotTimeout {
|
||||
fmt.Println("Timeout waiting for receiver")
|
||||
return nil
|
||||
}
|
||||
fmt.Print("\nFile sent")
|
||||
} else { // Is a Receiver
|
||||
if responses.notPresent {
|
||||
fmt.Println("Sender is not ready. Use -wait to wait until sender connects.")
|
||||
return nil
|
||||
}
|
||||
if !responses.gotOK {
|
||||
return errors.New("Transfer interrupted")
|
||||
}
|
||||
if err := c.catFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("Code: [%s]", c.Code)
|
||||
if c.DontEncrypt {
|
||||
if err := CopyFile(path.Join(c.Path, c.File.Name+".enc"), path.Join(c.Path, c.File.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if c.File.IsEncrypted {
|
||||
if err := DecryptFile(path.Join(c.Path, c.File.Name+".enc"), path.Join(c.Path, c.File.Name), c.Code); err != nil {
|
||||
return errors.Wrap(err, "Problem decrypting file")
|
||||
}
|
||||
} else {
|
||||
if err := CopyFile(path.Join(c.Path, c.File.Name+".enc"), path.Join(c.Path, c.File.Name)); err != nil {
|
||||
return errors.Wrap(err, "Problem copying file")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !c.Debug {
|
||||
os.Remove(path.Join(c.Path, c.File.Name+".enc"))
|
||||
}
|
||||
|
||||
fileHash, err := HashFile(path.Join(c.Path, c.File.Name))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
log.Debugf("\n\n\ndownloaded hash: [%s]", fileHash)
|
||||
log.Debugf("\n\n\nrelayed hash: [%s]", c.File.Hash)
|
||||
|
||||
if c.File.Hash != fileHash {
|
||||
return fmt.Errorf("\nUh oh! %s is corrupted! Sorry, try again.\n", c.File.Name)
|
||||
}
|
||||
if c.File.IsDir { // if the file was originally a dir
|
||||
fmt.Print("\ndecompressing folder")
|
||||
log.Debug("untarring " + c.File.Name)
|
||||
err := tarinator.UnTarinate(c.Path, path.Join(c.Path, c.File.Name))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// we remove the old tar.gz file
|
||||
err = os.Remove(path.Join(c.Path, c.File.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("\nReceived folder written to %s", path.Join(c.Path, c.File.Name[:len(c.File.Name)-4]))
|
||||
} else {
|
||||
fmt.Printf("\nReceived file written to %s", path.Join(c.Path, c.File.Name))
|
||||
}
|
||||
}
|
||||
timeSinceStart := time.Since(startTime) / time.Second
|
||||
fmt.Printf(" (%s/s)\n", humanize.Bytes(uint64(float64(c.File.Size)/float64(timeSinceStart))))
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileAlreadyExists(s []string, f string) bool {
|
||||
for _, a := range s {
|
||||
if a == f {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Connection) catFile() error {
|
||||
// cat the file
|
||||
files := make([]string, c.NumberOfConnections)
|
||||
for id := range files {
|
||||
files[id] = path.Join(c.Path, c.File.Name+".enc."+strconv.Itoa(id))
|
||||
}
|
||||
toRemove := !c.Debug
|
||||
return CatFiles(files, path.Join(c.Path, c.File.Name+".enc"), toRemove)
|
||||
}
|
||||
|
||||
func (c *Connection) receiveFile(id int, connection net.Conn) error {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"function": "receiveFile #" + strconv.Itoa(id),
|
||||
})
|
||||
|
||||
logger.Debug("waiting for chunk size from sender")
|
||||
fileSizeBuffer := make([]byte, 10)
|
||||
connection.Read(fileSizeBuffer)
|
||||
fileDataString := strings.Trim(string(fileSizeBuffer), ":")
|
||||
fileSizeInt, _ := strconv.Atoi(fileDataString)
|
||||
chunkSize := int64(fileSizeInt)
|
||||
logger.Debugf("chunk size: %d", chunkSize)
|
||||
if chunkSize == 0 {
|
||||
logger.Debug(fileSizeBuffer)
|
||||
return errors.New("chunk size is empty!")
|
||||
}
|
||||
|
||||
os.Remove(path.Join(c.Path, c.File.Name+".enc."+strconv.Itoa(id)))
|
||||
log.Debug("Making " + c.File.Name + ".enc." + strconv.Itoa(id))
|
||||
newFile, err := os.Create(path.Join(c.Path, c.File.Name+".enc."+strconv.Itoa(id)))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer newFile.Close()
|
||||
|
||||
logger.Debug("waiting for file")
|
||||
var receivedBytes int64
|
||||
receivedFirstBytes := false
|
||||
for {
|
||||
if (chunkSize - receivedBytes) < BUFFERSIZE {
|
||||
logger.Debug("at the end")
|
||||
io.CopyN(newFile, connection, (chunkSize - receivedBytes))
|
||||
// Empty the remaining bytes that we don't need from the network buffer
|
||||
if (receivedBytes+BUFFERSIZE)-chunkSize < BUFFERSIZE {
|
||||
logger.Debug("empty remaining bytes from network buffer")
|
||||
connection.Read(make([]byte, (receivedBytes+BUFFERSIZE)-chunkSize))
|
||||
}
|
||||
if !c.Debug {
|
||||
c.bar.Add(int((chunkSize - receivedBytes)))
|
||||
}
|
||||
break
|
||||
}
|
||||
io.CopyN(newFile, connection, BUFFERSIZE)
|
||||
receivedBytes += BUFFERSIZE
|
||||
if !receivedFirstBytes {
|
||||
receivedFirstBytes = true
|
||||
logger.Debug("Receieved first bytes!")
|
||||
}
|
||||
if !c.Debug {
|
||||
c.bar.Add(BUFFERSIZE)
|
||||
}
|
||||
}
|
||||
logger.Debug("received file")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Connection) sendFile(id int, connection net.Conn) error {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"function": "sendFile #" + strconv.Itoa(id),
|
||||
})
|
||||
defer connection.Close()
|
||||
|
||||
// open encrypted file chunk
|
||||
logger.Debug("opening encrypted file chunk: " + c.File.Name + ".enc." + strconv.Itoa(id))
|
||||
file, err := os.Open(c.File.Name + ".enc." + strconv.Itoa(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// determine and send the file size to client
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debugf("sending chunk size: %d", fi.Size())
|
||||
_, err = connection.Write([]byte(fillString(strconv.FormatInt(int64(fi.Size()), 10), 10)))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Problem sending chunk data: ")
|
||||
}
|
||||
|
||||
// rate limit the bandwidth
|
||||
logger.Debug("determining rate limiting")
|
||||
bufferSizeInKilobytes := BUFFERSIZE / 1024
|
||||
rate := float64(c.rate) / float64(c.NumberOfConnections*bufferSizeInKilobytes)
|
||||
throttle := time.NewTicker(time.Second / time.Duration(rate))
|
||||
defer throttle.Stop()
|
||||
|
||||
// send the file
|
||||
sendBuffer := make([]byte, BUFFERSIZE)
|
||||
totalBytesSent := 0
|
||||
for range throttle.C {
|
||||
n, err := file.Read(sendBuffer)
|
||||
connection.Write(sendBuffer)
|
||||
totalBytesSent += n
|
||||
if !c.Debug {
|
||||
c.bar.Add(n)
|
||||
}
|
||||
if err == io.EOF {
|
||||
//End of file reached, break out of for loop
|
||||
logger.Debug("EOF")
|
||||
break
|
||||
}
|
||||
}
|
||||
logger.Debug("file is sent")
|
||||
logger.Debug("removing piece")
|
||||
if !c.Debug {
|
||||
file.Close()
|
||||
err = os.Remove(c.File.Name + ".enc." + strconv.Itoa(id))
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
if [ -n "$CROC_PASS" ]; then
|
||||
set -- --pass "$CROC_PASS" "$@"
|
||||
fi
|
||||
exec /croc "$@"
|
|
@ -0,0 +1,12 @@
|
|||
[Unit]
|
||||
Description=croc relay
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
DynamicUser=yes
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
ExecStart=/usr/bin/croc relay
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
125
crypto.go
125
crypto.go
|
@ -1,125 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
mathrand "math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mars9/crypt"
|
||||
"github.com/schollz/mnemonicode"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mathrand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
||||
|
||||
func GetRandomName() string {
|
||||
result := []string{}
|
||||
bs := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(bs, mathrand.Uint32())
|
||||
result = mnemonicode.EncodeWordList(result, bs)
|
||||
return strings.Join(result, "-")
|
||||
}
|
||||
|
||||
func Encrypt(plaintext []byte, passphrase string, dontencrypt ...bool) (encrypted []byte, salt string, iv string) {
|
||||
if len(dontencrypt) > 0 && dontencrypt[0] {
|
||||
return plaintext, "salt", "iv"
|
||||
}
|
||||
key, saltBytes := deriveKey(passphrase, nil)
|
||||
ivBytes := make([]byte, 12)
|
||||
// http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
|
||||
// Section 8.2
|
||||
rand.Read(ivBytes)
|
||||
b, _ := aes.NewCipher(key)
|
||||
aesgcm, _ := cipher.NewGCM(b)
|
||||
encrypted = aesgcm.Seal(nil, ivBytes, plaintext, nil)
|
||||
salt = hex.EncodeToString(saltBytes)
|
||||
iv = hex.EncodeToString(ivBytes)
|
||||
return
|
||||
}
|
||||
|
||||
func Decrypt(data []byte, passphrase string, salt string, iv string, dontencrypt ...bool) (plaintext []byte, err error) {
|
||||
if len(dontencrypt) > 0 && dontencrypt[0] {
|
||||
return data, nil
|
||||
}
|
||||
saltBytes, _ := hex.DecodeString(salt)
|
||||
ivBytes, _ := hex.DecodeString(iv)
|
||||
key, _ := deriveKey(passphrase, saltBytes)
|
||||
b, _ := aes.NewCipher(key)
|
||||
aesgcm, _ := cipher.NewGCM(b)
|
||||
plaintext, err = aesgcm.Open(nil, ivBytes, data, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func deriveKey(passphrase string, salt []byte) ([]byte, []byte) {
|
||||
if salt == nil {
|
||||
salt = make([]byte, 8)
|
||||
// http://www.ietf.org/rfc/rfc2898.txt
|
||||
// Salt.
|
||||
rand.Read(salt)
|
||||
}
|
||||
return pbkdf2.Key([]byte(passphrase), salt, 1000, 32, sha256.New), salt
|
||||
}
|
||||
|
||||
func Hash(data string) string {
|
||||
return HashBytes([]byte(data))
|
||||
}
|
||||
|
||||
func HashBytes(data []byte) string {
|
||||
sum := sha256.Sum256(data)
|
||||
return fmt.Sprintf("%x", sum)
|
||||
}
|
||||
|
||||
func EncryptFile(inputFilename string, outputFilename string, password string) error {
|
||||
return cryptFile(inputFilename, outputFilename, password, true)
|
||||
}
|
||||
|
||||
func DecryptFile(inputFilename string, outputFilename string, password string) error {
|
||||
return cryptFile(inputFilename, outputFilename, password, false)
|
||||
}
|
||||
|
||||
func cryptFile(inputFilename string, outputFilename string, password string, encrypt bool) error {
|
||||
in, err := os.Open(inputFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(outputFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := out.Sync(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if err := out.Close(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}()
|
||||
c := &crypt.Crypter{
|
||||
HashFunc: sha1.New,
|
||||
HashSize: sha1.Size,
|
||||
Key: crypt.NewPbkdf2Key([]byte(password), 32),
|
||||
}
|
||||
if encrypt {
|
||||
if err := c.Encrypt(out, in); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := c.Decrypt(out, in); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncrypt(t *testing.T) {
|
||||
key := GetRandomName()
|
||||
encrypted, salt, iv := Encrypt([]byte("hello, world"), key)
|
||||
decrypted, err := Decrypt(encrypted, key, salt, iv)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if string(decrypted) != "hello, world" {
|
||||
t.Error("problem decrypting")
|
||||
}
|
||||
_, err = Decrypt(encrypted, "wrong passphrase", salt, iv)
|
||||
if err == nil {
|
||||
t.Error("should not work!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptFiles(t *testing.T) {
|
||||
key := GetRandomName()
|
||||
if err := ioutil.WriteFile("temp", []byte("hello, world!"), 0644); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := EncryptFile("temp", "temp.enc", key); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := DecryptFile("temp.enc", "temp.dec", key); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
data, err := ioutil.ReadFile("temp.dec")
|
||||
if string(data) != "hello, world!" {
|
||||
t.Errorf("Got something weird: " + string(data))
|
||||
}
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := DecryptFile("temp.enc", "temp.dec", key+"wrong password"); err == nil {
|
||||
t.Error("should throw error!")
|
||||
}
|
||||
os.Remove("temp.dec")
|
||||
os.Remove("temp.enc")
|
||||
os.Remove("temp")
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
module github.com/schollz/croc/v10
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash v1.1.0
|
||||
github.com/chzyer/readline v1.5.1
|
||||
github.com/denisbrodbeck/machineid v1.0.1
|
||||
github.com/kalafut/imohash v1.0.3
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
|
||||
github.com/schollz/cli/v2 v2.2.1
|
||||
github.com/schollz/logger v1.2.0
|
||||
github.com/schollz/mnemonicode v1.0.2-0.20190421205639-63fa713ece0d
|
||||
github.com/schollz/pake/v3 v3.0.5
|
||||
github.com/schollz/peerdiscovery v1.7.3
|
||||
github.com/schollz/progressbar/v3 v3.14.3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/time v0.5.0
|
||||
)
|
||||
|
||||
require github.com/minio/highwayhash v1.0.2
|
||||
|
||||
require (
|
||||
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406 // indirect
|
||||
github.com/twmb/murmur3 v1.1.8 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/term v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
|
@ -0,0 +1,132 @@
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
github.com/kalafut/imohash v1.0.3 h1:p9c61km8+6ZMqKRnERwdoxp/CztrdLNEbpsyGgf+A4M=
|
||||
github.com/kalafut/imohash v1.0.3/go.mod h1:6cn9lU0Sj8M4eu9UaQm1kR/5y3k/ayB68yntRhGloL4=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b h1:xZ59n7Frzh8CwyfAapUZLSg+gXH5m63YEaFCMpDHhpI=
|
||||
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b/go.mod h1:uDd4sYVYsqcxAB8j+Q7uhL6IJCs/r1kxib1HV4bgOMg=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
|
||||
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
|
||||
github.com/schollz/cli/v2 v2.2.1 h1:ou22Mj7ZPjrKz+8k2iDTWaHskEEV5NiAxGrdsCL36VU=
|
||||
github.com/schollz/cli/v2 v2.2.1/go.mod h1:My6bfphRLZUhZdlFUK8scAxMWHydE7k4s2ed2Dtnn+s=
|
||||
github.com/schollz/logger v1.2.0 h1:5WXfINRs3lEUTCZ7YXhj0uN+qukjizvITLm3Ca2m0Ho=
|
||||
github.com/schollz/logger v1.2.0/go.mod h1:P6F4/dGMGcx8wh+kG1zrNEd4vnNpEBY/mwEMd/vn6AM=
|
||||
github.com/schollz/mnemonicode v1.0.2-0.20190421205639-63fa713ece0d h1:3zCjdgCJbo9Fot3UoqZkpGiDgT6Nf+iUnOsDEJQay+c=
|
||||
github.com/schollz/mnemonicode v1.0.2-0.20190421205639-63fa713ece0d/go.mod h1:cl4UAOhUV0mkdjMj/QYaUZbZZdF8BnOqoz8rHMzwboY=
|
||||
github.com/schollz/pake/v3 v3.0.5 h1:MnZVdI987lkjln9BSx/zUb724TZISa2jbO+dPj6BvgQ=
|
||||
github.com/schollz/pake/v3 v3.0.5/go.mod h1:OGbG6htRwSKo6V8R5tg61ufpFmZM1b/PrrSp6g2ZLLc=
|
||||
github.com/schollz/peerdiscovery v1.7.3 h1:/pt1G0rZ80fSPoI/FgGC5P7MxpkRXD6u0pe6PJbYcIE=
|
||||
github.com/schollz/peerdiscovery v1.7.3/go.mod h1:mVlPNJ5DWbMi52VzpXxGbqXKdFANx3qw0Jsp3EQMCrE=
|
||||
github.com/schollz/progressbar/v3 v3.14.3 h1:oOuWW19ka12wxYU1XblR4n16wF/2Y1dBLMarMo6p4xU=
|
||||
github.com/schollz/progressbar/v3 v3.14.3/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tscholl2/siec v0.0.0-20210707234609-9bdfc483d499/go.mod h1:KL9+ubr1JZdaKjgAaHr+tCytEncXBa1pR6FjbTsOJnw=
|
||||
github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406 h1:sDWDZkwYqX0jvLWstKzFwh+pYhQNaVg65BgSkCP/f7U=
|
||||
github.com/tscholl2/siec v0.0.0-20240310163802-c2c6f6198406/go.mod h1:KL9+ubr1JZdaKjgAaHr+tCytEncXBa1pR6FjbTsOJnw=
|
||||
github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
|
||||
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
|
||||
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -1,34 +0,0 @@
|
|||
# .goreleaser.yml
|
||||
# Build customization
|
||||
builds:
|
||||
- binary: croc
|
||||
goos:
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- 386
|
||||
goarm:
|
||||
- 6
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
archive:
|
||||
replacements:
|
||||
amd64: 64bit
|
||||
386: 32bit
|
||||
darwin: OSX
|
||||
linux_arm: raspberry_pi
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
- goos: darwin
|
||||
format: zip
|
Binary file not shown.
Before Width: | Height: | Size: 6.9 KiB |
95
main.go
95
main.go
|
@ -1,78 +1,33 @@
|
|||
package main
|
||||
|
||||
//go:generate go run src/install/updateversion.go
|
||||
//go:generate git commit -am "bump $VERSION"
|
||||
//go:generate git tag -af v$VERSION -m "v$VERSION"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"log"
|
||||
|
||||
"github.com/schollz/croc/v10/src/cli"
|
||||
)
|
||||
|
||||
const BUFFERSIZE = 1024
|
||||
|
||||
var oneGigabytePerSecond = 1000000 // expressed as kbps
|
||||
|
||||
type Flags struct {
|
||||
Relay bool
|
||||
Debug bool
|
||||
Wait bool
|
||||
PathSpec bool
|
||||
DontEncrypt bool
|
||||
Server string
|
||||
File string
|
||||
Path string
|
||||
Code string
|
||||
Rate int
|
||||
NumberOfConnections int
|
||||
}
|
||||
|
||||
var version string
|
||||
|
||||
func main() {
|
||||
fmt.Println(`
|
||||
,_
|
||||
>' )
|
||||
croc version ` + fmt.Sprintf("%5s", version) + ` ( ( \
|
||||
|| \
|
||||
/^^^^\ ||
|
||||
/^^\________/0 \ ||
|
||||
( ` + "`" + `~+++,,_||__,,++~^^^^^^^
|
||||
...V^V^V^V^V^V^\...............................
|
||||
|
||||
`)
|
||||
flags := new(Flags)
|
||||
flag.BoolVar(&flags.Relay, "relay", false, "run as relay")
|
||||
flag.BoolVar(&flags.Debug, "debug", false, "debug mode")
|
||||
flag.BoolVar(&flags.Wait, "wait", false, "wait for code to be sent")
|
||||
flag.BoolVar(&flags.PathSpec, "ask-save", false, "ask for path to save to")
|
||||
flag.StringVar(&flags.Server, "server", "cowyo.com", "address of relay server")
|
||||
flag.StringVar(&flags.File, "send", "", "file to send")
|
||||
flag.StringVar(&flags.Path, "save", "", "path to save to")
|
||||
flag.StringVar(&flags.Code, "code", "", "use your own code phrase")
|
||||
flag.IntVar(&flags.Rate, "rate", oneGigabytePerSecond, "throttle down to speed in kbps")
|
||||
flag.BoolVar(&flags.DontEncrypt, "no-encrypt", false, "turn off encryption")
|
||||
flag.IntVar(&flags.NumberOfConnections, "threads", 4, "number of threads to use")
|
||||
flag.Parse()
|
||||
|
||||
if flags.Relay {
|
||||
r := NewRelay(flags)
|
||||
r.Run()
|
||||
} else {
|
||||
c, err := NewConnection(flags)
|
||||
if err != nil {
|
||||
fmt.Printf("Error! Please submit the following error to https://github.com/schollz/croc/issues:\n\n'%s'\n\n", err.Error())
|
||||
return
|
||||
}
|
||||
err = c.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Error! Please submit the following error to https://github.com/schollz/croc/issues:\n\n'%s'\n\n", err.Error())
|
||||
}
|
||||
// "github.com/pkg/profile"
|
||||
// go func() {
|
||||
// for {
|
||||
// f, err := os.Create("croc.pprof")
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// runtime.GC() // get up-to-date statistics
|
||||
// if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// f.Close()
|
||||
// time.Sleep(3 * time.Second)
|
||||
// fmt.Println("wrote profile")
|
||||
// }
|
||||
// }()
|
||||
if err := cli.Run(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getInput(prompt string) string {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print(prompt)
|
||||
text, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
|
318
relay.go
318
relay.go
|
@ -1,318 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const MAX_NUMBER_THREADS = 8
|
||||
const CONNECTION_TIMEOUT = time.Hour
|
||||
|
||||
type connectionMap struct {
|
||||
receiver map[string]net.Conn
|
||||
sender map[string]net.Conn
|
||||
metadata map[string]string
|
||||
potentialReceivers map[string]struct{}
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *connectionMap) IsSenderConnected(key string) (found bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
_, found = c.sender[key]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *connectionMap) IsPotentialReceiverConnected(key string) (found bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
_, found = c.potentialReceivers[key]
|
||||
return
|
||||
}
|
||||
|
||||
type Relay struct {
|
||||
connections connectionMap
|
||||
Debug bool
|
||||
NumberOfConnections int
|
||||
}
|
||||
|
||||
func NewRelay(flags *Flags) *Relay {
|
||||
r := &Relay{
|
||||
Debug: flags.Debug,
|
||||
NumberOfConnections: MAX_NUMBER_THREADS,
|
||||
}
|
||||
|
||||
log.SetFormatter(&log.TextFormatter{})
|
||||
if r.Debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
} else {
|
||||
log.SetLevel(log.WarnLevel)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Relay) Run() {
|
||||
r.connections = connectionMap{}
|
||||
r.connections.Lock()
|
||||
r.connections.receiver = make(map[string]net.Conn)
|
||||
r.connections.sender = make(map[string]net.Conn)
|
||||
r.connections.metadata = make(map[string]string)
|
||||
r.connections.potentialReceivers = make(map[string]struct{})
|
||||
r.connections.Unlock()
|
||||
r.runServer()
|
||||
}
|
||||
|
||||
func (r *Relay) runServer() {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"function": "main",
|
||||
})
|
||||
logger.Debug("Initializing")
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(r.NumberOfConnections)
|
||||
for id := 0; id < r.NumberOfConnections; id++ {
|
||||
go r.listenerThread(id, &wg)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (r *Relay) listenerThread(id int, wg *sync.WaitGroup) {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"function": "listenerThread:" + strconv.Itoa(27000+id),
|
||||
})
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
if err := r.listener(id); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Relay) listener(id int) (err error) {
|
||||
port := strconv.Itoa(27001 + id)
|
||||
logger := log.WithFields(log.Fields{
|
||||
"function": "listener:" + port,
|
||||
})
|
||||
server, err := net.Listen("tcp", "0.0.0.0:"+port)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error listening on :"+port)
|
||||
}
|
||||
defer server.Close()
|
||||
logger.Debug("waiting for connections")
|
||||
//Spawn a new goroutine whenever a client connects
|
||||
for {
|
||||
connection, err := server.Accept()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "problem accepting connection")
|
||||
}
|
||||
logger.Debugf("Client %s connected", connection.RemoteAddr().String())
|
||||
go r.clientCommuncation(id, connection)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Relay) clientCommuncation(id int, connection net.Conn) {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"id": id,
|
||||
"ip": connection.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
sendMessage("who?", connection)
|
||||
m := strings.Split(receiveMessage(connection), ".")
|
||||
if len(m) < 3 {
|
||||
logger.Debug("exiting, not enough information")
|
||||
sendMessage("not enough information", connection)
|
||||
return
|
||||
}
|
||||
connectionType, codePhrase, metaData := m[0], m[1], m[2]
|
||||
key := codePhrase + "-" + strconv.Itoa(id)
|
||||
|
||||
switch connectionType {
|
||||
case "s": // sender connection
|
||||
if r.connections.IsSenderConnected(key) {
|
||||
sendMessage("no", connection)
|
||||
return
|
||||
}
|
||||
|
||||
r.connections.Lock()
|
||||
r.connections.metadata[key] = metaData
|
||||
r.connections.sender[key] = connection
|
||||
r.connections.Unlock()
|
||||
// wait for receiver
|
||||
receiversAddress := ""
|
||||
isTimeout := time.Duration(0)
|
||||
for {
|
||||
if CONNECTION_TIMEOUT <= isTimeout {
|
||||
sendMessage("timeout", connection)
|
||||
break
|
||||
}
|
||||
r.connections.RLock()
|
||||
if _, ok := r.connections.receiver[key]; ok {
|
||||
receiversAddress = r.connections.receiver[key].RemoteAddr().String()
|
||||
logger.Debug("got receiver")
|
||||
r.connections.RUnlock()
|
||||
break
|
||||
}
|
||||
r.connections.RUnlock()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
isTimeout += 100 * time.Millisecond
|
||||
}
|
||||
logger.Debug("telling sender ok")
|
||||
sendMessage(receiversAddress, connection)
|
||||
logger.Debug("preparing pipe")
|
||||
r.connections.Lock()
|
||||
con1 := r.connections.sender[key]
|
||||
con2 := r.connections.receiver[key]
|
||||
r.connections.Unlock()
|
||||
logger.Debug("piping connections")
|
||||
Pipe(con1, con2)
|
||||
logger.Debug("done piping")
|
||||
r.connections.Lock()
|
||||
// close connections
|
||||
r.connections.sender[key].Close()
|
||||
r.connections.receiver[key].Close()
|
||||
// delete connctions
|
||||
delete(r.connections.sender, key)
|
||||
delete(r.connections.receiver, key)
|
||||
delete(r.connections.metadata, key)
|
||||
delete(r.connections.potentialReceivers, key)
|
||||
r.connections.Unlock()
|
||||
logger.Debug("deleted sender and receiver")
|
||||
case "r", "c": // receiver
|
||||
if r.connections.IsPotentialReceiverConnected(key) {
|
||||
sendMessage("no", connection)
|
||||
return
|
||||
}
|
||||
|
||||
// add as a potential receiver
|
||||
r.connections.Lock()
|
||||
r.connections.potentialReceivers[key] = struct{}{}
|
||||
r.connections.Unlock()
|
||||
// wait for sender's metadata
|
||||
sendersAddress := ""
|
||||
for {
|
||||
r.connections.RLock()
|
||||
if _, ok := r.connections.metadata[key]; ok {
|
||||
if _, ok2 := r.connections.sender[key]; ok2 {
|
||||
sendersAddress = r.connections.sender[key].RemoteAddr().String()
|
||||
logger.Debug("got sender meta data")
|
||||
r.connections.RUnlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
r.connections.RUnlock()
|
||||
if connectionType == "c" {
|
||||
sendMessage("0-0-0-0.0.0.0", connection)
|
||||
// sender is not ready so delete connection
|
||||
r.connections.Lock()
|
||||
delete(r.connections.potentialReceivers, key)
|
||||
r.connections.Unlock()
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
// send meta data
|
||||
r.connections.RLock()
|
||||
sendMessage(r.connections.metadata[key]+"-"+sendersAddress, connection)
|
||||
r.connections.RUnlock()
|
||||
// check for receiver's consent
|
||||
consent := receiveMessage(connection)
|
||||
logger.Debugf("consent: %s", consent)
|
||||
if consent == "ok" {
|
||||
logger.Debug("got consent")
|
||||
r.connections.Lock()
|
||||
r.connections.receiver[key] = connection
|
||||
r.connections.Unlock()
|
||||
}
|
||||
default:
|
||||
logger.Debugf("Got unknown protocol: '%s'", connectionType)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(message string, connection net.Conn) {
|
||||
message = fillString(message, BUFFERSIZE)
|
||||
connection.Write([]byte(message))
|
||||
}
|
||||
|
||||
func receiveMessage(connection net.Conn) string {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"func": "receiveMessage",
|
||||
"ip": connection.RemoteAddr().String(),
|
||||
})
|
||||
messageByte := make([]byte, BUFFERSIZE)
|
||||
err := connection.SetDeadline(time.Now().Add(60 * time.Minute))
|
||||
if err != nil {
|
||||
logger.Warn(err)
|
||||
}
|
||||
_, err = connection.Read(messageByte)
|
||||
if err != nil {
|
||||
logger.Warn("read deadline, no response")
|
||||
return ""
|
||||
}
|
||||
return strings.Replace(string(messageByte), ":", "", -1)
|
||||
}
|
||||
|
||||
func fillString(retunString string, toLength int) string {
|
||||
for {
|
||||
lengthString := len(retunString)
|
||||
if lengthString < toLength {
|
||||
retunString = retunString + ":"
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return retunString
|
||||
}
|
||||
|
||||
// chanFromConn creates a channel from a Conn object, and sends everything it
|
||||
// Read()s from the socket to the channel.
|
||||
func chanFromConn(conn net.Conn) chan []byte {
|
||||
c := make(chan []byte)
|
||||
|
||||
go func() {
|
||||
b := make([]byte, BUFFERSIZE)
|
||||
|
||||
for {
|
||||
n, err := conn.Read(b)
|
||||
if n > 0 {
|
||||
res := make([]byte, n)
|
||||
// Copy the buffer so it doesn't get changed while read by the recipient.
|
||||
copy(res, b[:n])
|
||||
c <- res
|
||||
}
|
||||
if err != nil {
|
||||
c <- nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Pipe creates a full-duplex pipe between the two sockets and transfers data from one to the other.
|
||||
func Pipe(conn1 net.Conn, conn2 net.Conn) {
|
||||
chan1 := chanFromConn(conn1)
|
||||
chan2 := chanFromConn(conn2)
|
||||
|
||||
for {
|
||||
select {
|
||||
case b1 := <-chan1:
|
||||
if b1 == nil {
|
||||
return
|
||||
}
|
||||
conn2.Write(b1)
|
||||
|
||||
case b2 := <-chan2:
|
||||
if b2 == nil {
|
||||
return
|
||||
}
|
||||
conn1.Write(b2)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,682 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/schollz/cli/v2"
|
||||
"github.com/schollz/croc/v10/src/comm"
|
||||
"github.com/schollz/croc/v10/src/croc"
|
||||
"github.com/schollz/croc/v10/src/models"
|
||||
"github.com/schollz/croc/v10/src/tcp"
|
||||
"github.com/schollz/croc/v10/src/utils"
|
||||
log "github.com/schollz/logger"
|
||||
"github.com/schollz/mnemonicode"
|
||||
"github.com/schollz/pake/v3"
|
||||
)
|
||||
|
||||
// Version specifies the version
|
||||
var Version string
|
||||
|
||||
// Run will run the command line program
|
||||
func Run() (err error) {
|
||||
// use all of the processors
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "croc"
|
||||
if Version == "" {
|
||||
Version = "v10.0.7"
|
||||
}
|
||||
app.Version = Version
|
||||
app.Compiled = time.Now()
|
||||
app.Usage = "easily and securely transfer stuff from one computer to another"
|
||||
app.UsageText = `Send a file:
|
||||
croc send file.txt
|
||||
|
||||
-git to respect your .gitignore
|
||||
Send multiple files:
|
||||
croc send file1.txt file2.txt file3.txt
|
||||
or
|
||||
croc send *.jpg
|
||||
|
||||
Send everything in a folder:
|
||||
croc send example-folder-name
|
||||
|
||||
Send a file with a custom code:
|
||||
croc send --code secret-code file.txt
|
||||
|
||||
Receive a file using code:
|
||||
croc secret-code`
|
||||
app.Commands = []*cli.Command{
|
||||
{
|
||||
Name: "send",
|
||||
Usage: "send file(s), or folder (see options with croc send -h)",
|
||||
Description: "send file(s), or folder, over the relay",
|
||||
ArgsUsage: "[filename(s) or folder]",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{Name: "zip", Usage: "zip folder before sending"},
|
||||
&cli.StringFlag{Name: "code", Aliases: []string{"c"}, Usage: "codephrase used to connect to relay"},
|
||||
&cli.StringFlag{Name: "hash", Value: "xxhash", Usage: "hash algorithm (xxhash, imohash, md5)"},
|
||||
&cli.StringFlag{Name: "text", Aliases: []string{"t"}, Usage: "send some text"},
|
||||
&cli.BoolFlag{Name: "no-local", Usage: "disable local relay when sending"},
|
||||
&cli.BoolFlag{Name: "no-multi", Usage: "disable multiplexing"},
|
||||
&cli.BoolFlag{Name: "git", Usage: "enable .gitignore respect / don't send ignored files"},
|
||||
&cli.IntFlag{Name: "port", Value: 9009, Usage: "base port for the relay"},
|
||||
&cli.IntFlag{Name: "transfers", Value: 4, Usage: "number of ports to use for transfers"},
|
||||
},
|
||||
HelpName: "croc send",
|
||||
Action: send,
|
||||
},
|
||||
{
|
||||
Name: "relay",
|
||||
Usage: "start your own relay (optional)",
|
||||
Description: "start relay",
|
||||
HelpName: "croc relay",
|
||||
Action: relay,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{Name: "host", Usage: "host of the relay"},
|
||||
&cli.StringFlag{Name: "ports", Value: "9009,9010,9011,9012,9013", Usage: "ports of the relay"},
|
||||
},
|
||||
},
|
||||
}
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.BoolFlag{Name: "internal-dns", Usage: "use a built-in DNS stub resolver rather than the host operating system"},
|
||||
&cli.BoolFlag{Name: "classic", Usage: "toggle between the classic mode (insecure due to local attack vector) and new mode (secure)"},
|
||||
&cli.BoolFlag{Name: "remember", Usage: "save these settings to reuse next time"},
|
||||
&cli.BoolFlag{Name: "debug", Usage: "toggle debug mode"},
|
||||
&cli.BoolFlag{Name: "yes", Usage: "automatically agree to all prompts"},
|
||||
&cli.BoolFlag{Name: "stdout", Usage: "redirect file to stdout"},
|
||||
&cli.BoolFlag{Name: "no-compress", Usage: "disable compression"},
|
||||
&cli.BoolFlag{Name: "ask", Usage: "make sure sender and recipient are prompted"},
|
||||
&cli.BoolFlag{Name: "local", Usage: "force to use only local connections"},
|
||||
&cli.BoolFlag{Name: "ignore-stdin", Usage: "ignore piped stdin"},
|
||||
&cli.BoolFlag{Name: "overwrite", Usage: "do not prompt to overwrite"},
|
||||
&cli.BoolFlag{Name: "testing", Usage: "flag for testing purposes"},
|
||||
&cli.StringFlag{Name: "curve", Value: "p256", Usage: "choose an encryption curve (" + strings.Join(pake.AvailableCurves(), ", ") + ")"},
|
||||
&cli.StringFlag{Name: "ip", Value: "", Usage: "set sender ip if known e.g. 10.0.0.1:9009, [::1]:9009"},
|
||||
&cli.StringFlag{Name: "relay", Value: models.DEFAULT_RELAY, Usage: "address of the relay", EnvVars: []string{"CROC_RELAY"}},
|
||||
&cli.StringFlag{Name: "relay6", Value: models.DEFAULT_RELAY6, Usage: "ipv6 address of the relay", EnvVars: []string{"CROC_RELAY6"}},
|
||||
&cli.StringFlag{Name: "out", Value: ".", Usage: "specify an output folder to receive the file"},
|
||||
&cli.StringFlag{Name: "pass", Value: models.DEFAULT_PASSPHRASE, Usage: "password for the relay", EnvVars: []string{"CROC_PASS"}},
|
||||
&cli.StringFlag{Name: "socks5", Value: "", Usage: "add a socks5 proxy", EnvVars: []string{"SOCKS5_PROXY"}},
|
||||
&cli.StringFlag{Name: "connect", Value: "", Usage: "add a http proxy", EnvVars: []string{"HTTP_PROXY"}},
|
||||
&cli.StringFlag{Name: "throttleUpload", Value: "", Usage: "Throttle the upload speed e.g. 500k"},
|
||||
}
|
||||
app.EnableBashCompletion = true
|
||||
app.HideHelp = false
|
||||
app.HideVersion = false
|
||||
app.Action = func(c *cli.Context) error {
|
||||
allStringsAreFiles := func(strs []string) bool {
|
||||
for _, str := range strs {
|
||||
if !utils.Exists(str) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// check if "classic" is set
|
||||
classicFile := getClassicConfigFile(true)
|
||||
classicInsecureMode := utils.Exists(classicFile)
|
||||
if c.Bool("classic") {
|
||||
if classicInsecureMode {
|
||||
// classic mode not enabled
|
||||
fmt.Print(`Classic mode is currently ENABLED.
|
||||
|
||||
Disabling this mode will prevent the shared secret from being visible
|
||||
on the host's process list when passed via the command line. On a
|
||||
multi-user system, this will help ensure that other local users cannot
|
||||
access the shared secret and receive the files instead of the intended
|
||||
recipient.
|
||||
|
||||
Do you wish to continue to DISABLE the classic mode? (Y/n) `)
|
||||
choice := strings.ToLower(utils.GetInput(""))
|
||||
if choice == "y" || choice == "yes" {
|
||||
os.Remove(classicFile)
|
||||
fmt.Print("\nClassic mode DISABLED.\n\n")
|
||||
fmt.Print(`To send and receive, export the CROC_SECRET variable with the code phrase:
|
||||
|
||||
Send: CROC_SECRET=*** croc send file.txt
|
||||
|
||||
Receive: CROC_SECRET=*** croc` + "\n\n")
|
||||
} else {
|
||||
fmt.Print("\nClassic mode ENABLED.\n")
|
||||
|
||||
}
|
||||
} else {
|
||||
// enable classic mode
|
||||
// touch the file
|
||||
fmt.Print(`Classic mode is currently DISABLED.
|
||||
|
||||
Please note that enabling this mode will make the shared secret visible
|
||||
on the host's process list when passed via the command line. On a
|
||||
multi-user system, this could allow other local users to access the
|
||||
shared secret and receive the files instead of the intended recipient.
|
||||
|
||||
Do you wish to continue to enable the classic mode? (Y/n) `)
|
||||
choice := strings.ToLower(utils.GetInput(""))
|
||||
if choice == "y" || choice == "yes" {
|
||||
fmt.Print("\nClassic mode ENABLED.\n\n")
|
||||
os.WriteFile(classicFile, []byte("enabled"), 0o644)
|
||||
fmt.Print(`To send and receive, use the code phrase:
|
||||
|
||||
Send: croc send --code *** file.txt
|
||||
|
||||
Receive: croc ***` + "\n\n")
|
||||
} else {
|
||||
fmt.Print("\nClassic mode DISABLED.\n")
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// if trying to send but forgot send, let the user know
|
||||
if c.Args().Present() && allStringsAreFiles(c.Args().Slice()) {
|
||||
fnames := []string{}
|
||||
for _, fpath := range c.Args().Slice() {
|
||||
_, basename := filepath.Split(fpath)
|
||||
fnames = append(fnames, "'"+basename+"'")
|
||||
}
|
||||
promptMessage := fmt.Sprintf("Did you mean to send %s? (Y/n) ", strings.Join(fnames, ", "))
|
||||
choice := strings.ToLower(utils.GetInput(promptMessage))
|
||||
if choice == "" || choice == "y" || choice == "yes" {
|
||||
return send(c)
|
||||
}
|
||||
}
|
||||
|
||||
return receive(c)
|
||||
}
|
||||
|
||||
return app.Run(os.Args)
|
||||
}
|
||||
|
||||
func setDebugLevel(c *cli.Context) {
|
||||
if c.Bool("debug") {
|
||||
log.SetLevel("debug")
|
||||
log.Debug("debug mode on")
|
||||
} else {
|
||||
log.SetLevel("info")
|
||||
}
|
||||
}
|
||||
|
||||
func getSendConfigFile(requireValidPath bool) string {
|
||||
configFile, err := utils.GetConfigDir(requireValidPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return ""
|
||||
}
|
||||
return path.Join(configFile, "send.json")
|
||||
}
|
||||
|
||||
func getClassicConfigFile(requireValidPath bool) string {
|
||||
configFile, err := utils.GetConfigDir(requireValidPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return ""
|
||||
}
|
||||
return path.Join(configFile, "classic_enabled")
|
||||
}
|
||||
|
||||
func getReceiveConfigFile(requireValidPath bool) (string, error) {
|
||||
configFile, err := utils.GetConfigDir(requireValidPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return "", err
|
||||
}
|
||||
return path.Join(configFile, "receive.json"), nil
|
||||
}
|
||||
|
||||
func determinePass(c *cli.Context) (pass string) {
|
||||
pass = c.String("pass")
|
||||
b, err := os.ReadFile(pass)
|
||||
if err == nil {
|
||||
pass = strings.TrimSpace(string(b))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func send(c *cli.Context) (err error) {
|
||||
setDebugLevel(c)
|
||||
comm.Socks5Proxy = c.String("socks5")
|
||||
comm.HttpProxy = c.String("connect")
|
||||
|
||||
portParam := c.Int("port")
|
||||
if portParam == 0 {
|
||||
portParam = 9009
|
||||
}
|
||||
transfersParam := c.Int("transfers")
|
||||
if transfersParam == 0 {
|
||||
transfersParam = 4
|
||||
}
|
||||
|
||||
ports := make([]string, transfersParam+1)
|
||||
for i := 0; i <= transfersParam; i++ {
|
||||
ports[i] = strconv.Itoa(portParam + i)
|
||||
}
|
||||
|
||||
crocOptions := croc.Options{
|
||||
SharedSecret: c.String("code"),
|
||||
IsSender: true,
|
||||
Debug: c.Bool("debug"),
|
||||
NoPrompt: c.Bool("yes"),
|
||||
RelayAddress: c.String("relay"),
|
||||
RelayAddress6: c.String("relay6"),
|
||||
Stdout: c.Bool("stdout"),
|
||||
DisableLocal: c.Bool("no-local"),
|
||||
OnlyLocal: c.Bool("local"),
|
||||
IgnoreStdin: c.Bool("ignore-stdin"),
|
||||
RelayPorts: ports,
|
||||
Ask: c.Bool("ask"),
|
||||
NoMultiplexing: c.Bool("no-multi"),
|
||||
RelayPassword: determinePass(c),
|
||||
SendingText: c.String("text") != "",
|
||||
NoCompress: c.Bool("no-compress"),
|
||||
Overwrite: c.Bool("overwrite"),
|
||||
Curve: c.String("curve"),
|
||||
HashAlgorithm: c.String("hash"),
|
||||
ThrottleUpload: c.String("throttleUpload"),
|
||||
ZipFolder: c.Bool("zip"),
|
||||
GitIgnore: c.Bool("git"),
|
||||
}
|
||||
if crocOptions.RelayAddress != models.DEFAULT_RELAY {
|
||||
crocOptions.RelayAddress6 = ""
|
||||
} else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 {
|
||||
crocOptions.RelayAddress = ""
|
||||
}
|
||||
b, errOpen := os.ReadFile(getSendConfigFile(false))
|
||||
if errOpen == nil && !c.Bool("remember") {
|
||||
var rememberedOptions croc.Options
|
||||
err = json.Unmarshal(b, &rememberedOptions)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
// update anything that isn't explicitly set
|
||||
if !c.IsSet("relay") && rememberedOptions.RelayAddress != "" {
|
||||
crocOptions.RelayAddress = rememberedOptions.RelayAddress
|
||||
}
|
||||
if !c.IsSet("no-local") {
|
||||
crocOptions.DisableLocal = rememberedOptions.DisableLocal
|
||||
}
|
||||
if !c.IsSet("ports") && len(rememberedOptions.RelayPorts) > 0 {
|
||||
crocOptions.RelayPorts = rememberedOptions.RelayPorts
|
||||
}
|
||||
if !c.IsSet("code") {
|
||||
crocOptions.SharedSecret = rememberedOptions.SharedSecret
|
||||
}
|
||||
if !c.IsSet("pass") && rememberedOptions.RelayPassword != "" {
|
||||
crocOptions.RelayPassword = rememberedOptions.RelayPassword
|
||||
}
|
||||
if !c.IsSet("relay6") && rememberedOptions.RelayAddress6 != "" {
|
||||
crocOptions.RelayAddress6 = rememberedOptions.RelayAddress6
|
||||
}
|
||||
if !c.IsSet("overwrite") {
|
||||
crocOptions.Overwrite = rememberedOptions.Overwrite
|
||||
}
|
||||
if !c.IsSet("curve") && rememberedOptions.Curve != "" {
|
||||
crocOptions.Curve = rememberedOptions.Curve
|
||||
}
|
||||
if !c.IsSet("local") {
|
||||
crocOptions.OnlyLocal = rememberedOptions.OnlyLocal
|
||||
}
|
||||
if !c.IsSet("hash") {
|
||||
crocOptions.HashAlgorithm = rememberedOptions.HashAlgorithm
|
||||
}
|
||||
if !c.IsSet("git") {
|
||||
crocOptions.GitIgnore = rememberedOptions.GitIgnore
|
||||
}
|
||||
}
|
||||
|
||||
var fnames []string
|
||||
stat, _ := os.Stdin.Stat()
|
||||
if ((stat.Mode() & os.ModeCharDevice) == 0) && !c.Bool("ignore-stdin") {
|
||||
fnames, err = getStdin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
e := os.Remove(fnames[0])
|
||||
if e != nil {
|
||||
log.Error(e)
|
||||
}
|
||||
}()
|
||||
} else if c.String("text") != "" {
|
||||
fnames, err = makeTempFileWithString(c.String("text"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
e := os.Remove(fnames[0])
|
||||
if e != nil {
|
||||
log.Error(e)
|
||||
}
|
||||
}()
|
||||
|
||||
} else {
|
||||
fnames = c.Args().Slice()
|
||||
}
|
||||
if len(fnames) == 0 {
|
||||
return errors.New("must specify file: croc send [filename(s) or folder]")
|
||||
}
|
||||
|
||||
classicInsecureMode := utils.Exists(getClassicConfigFile(true))
|
||||
if !classicInsecureMode {
|
||||
// if operating system is UNIX, then use environmental variable to set the code
|
||||
if (!(runtime.GOOS == "windows") && c.IsSet("code")) || os.Getenv("CROC_SECRET") != "" {
|
||||
crocOptions.SharedSecret = os.Getenv("CROC_SECRET")
|
||||
if crocOptions.SharedSecret == "" {
|
||||
fmt.Printf(`On UNIX systems, to send with a custom code phrase,
|
||||
you need to set the environmental variable CROC_SECRET:
|
||||
|
||||
export CROC_SECRET="****"
|
||||
croc send file.txt
|
||||
|
||||
Or you can have the code phrase automaticlaly generated:
|
||||
|
||||
croc send file.txt
|
||||
|
||||
Or you can go back to the classic croc behavior by enabling classic mode:
|
||||
|
||||
croc --classic
|
||||
|
||||
`)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(crocOptions.SharedSecret) == 0 {
|
||||
// generate code phrase
|
||||
crocOptions.SharedSecret = utils.GetRandomName()
|
||||
}
|
||||
minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders, err := croc.GetFilesInfo(fnames, crocOptions.ZipFolder, crocOptions.GitIgnore)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cr, err := croc.New(crocOptions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// save the config
|
||||
saveConfig(c, crocOptions)
|
||||
|
||||
err = cr.Send(minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getStdin() (fnames []string, err error) {
|
||||
f, err := os.CreateTemp(".", "croc-stdin-")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(f, os.Stdin)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fnames = []string{f.Name()}
|
||||
return
|
||||
}
|
||||
|
||||
func makeTempFileWithString(s string) (fnames []string, err error) {
|
||||
f, err := os.CreateTemp(".", "croc-stdin-")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = f.WriteString(s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fnames = []string{f.Name()}
|
||||
return
|
||||
}
|
||||
|
||||
func saveConfig(c *cli.Context, crocOptions croc.Options) {
|
||||
if c.Bool("remember") {
|
||||
configFile := getSendConfigFile(true)
|
||||
log.Debug("saving config file")
|
||||
var bConfig []byte
|
||||
// if the code wasn't set, don't save it
|
||||
if c.String("code") == "" {
|
||||
crocOptions.SharedSecret = ""
|
||||
}
|
||||
bConfig, err := json.MarshalIndent(crocOptions, "", " ")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
err = os.WriteFile(configFile, bConfig, 0o644)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
log.Debugf("wrote %s", configFile)
|
||||
}
|
||||
}
|
||||
|
||||
type TabComplete struct{}
|
||||
|
||||
func (t TabComplete) Do(line []rune, pos int) ([][]rune, int) {
|
||||
var words = strings.SplitAfter(string(line), "-")
|
||||
var lastPartialWord = words[len(words)-1]
|
||||
var nbCharacter = len(lastPartialWord)
|
||||
if nbCharacter == 0 {
|
||||
// No completion
|
||||
return [][]rune{[]rune("")}, 0
|
||||
}
|
||||
if len(words) == 1 && nbCharacter == utils.NbPinNumbers {
|
||||
// Check if word is indeed a number
|
||||
_, err := strconv.Atoi(lastPartialWord)
|
||||
if err == nil {
|
||||
return [][]rune{[]rune("-")}, nbCharacter
|
||||
}
|
||||
}
|
||||
var strArray [][]rune
|
||||
for _, s := range mnemonicode.WordList {
|
||||
if strings.HasPrefix(s, lastPartialWord) {
|
||||
var completionCandidate = s[nbCharacter:]
|
||||
if len(words) <= mnemonicode.WordsRequired(utils.NbBytesWords) {
|
||||
completionCandidate += "-"
|
||||
}
|
||||
strArray = append(strArray, []rune(completionCandidate))
|
||||
}
|
||||
}
|
||||
return strArray, nbCharacter
|
||||
}
|
||||
|
||||
func receive(c *cli.Context) (err error) {
|
||||
comm.Socks5Proxy = c.String("socks5")
|
||||
comm.HttpProxy = c.String("connect")
|
||||
crocOptions := croc.Options{
|
||||
SharedSecret: c.String("code"),
|
||||
IsSender: false,
|
||||
Debug: c.Bool("debug"),
|
||||
NoPrompt: c.Bool("yes"),
|
||||
RelayAddress: c.String("relay"),
|
||||
RelayAddress6: c.String("relay6"),
|
||||
Stdout: c.Bool("stdout"),
|
||||
Ask: c.Bool("ask"),
|
||||
RelayPassword: determinePass(c),
|
||||
OnlyLocal: c.Bool("local"),
|
||||
IP: c.String("ip"),
|
||||
Overwrite: c.Bool("overwrite"),
|
||||
Curve: c.String("curve"),
|
||||
TestFlag: c.Bool("testing"),
|
||||
}
|
||||
if crocOptions.RelayAddress != models.DEFAULT_RELAY {
|
||||
crocOptions.RelayAddress6 = ""
|
||||
} else if crocOptions.RelayAddress6 != models.DEFAULT_RELAY6 {
|
||||
crocOptions.RelayAddress = ""
|
||||
}
|
||||
|
||||
switch c.Args().Len() {
|
||||
case 1:
|
||||
crocOptions.SharedSecret = c.Args().First()
|
||||
case 3:
|
||||
fallthrough
|
||||
case 4:
|
||||
var phrase []string
|
||||
phrase = append(phrase, c.Args().First())
|
||||
phrase = append(phrase, c.Args().Tail()...)
|
||||
crocOptions.SharedSecret = strings.Join(phrase, "-")
|
||||
}
|
||||
|
||||
// load options here
|
||||
setDebugLevel(c)
|
||||
|
||||
doRemember := c.Bool("remember")
|
||||
configFile, err := getReceiveConfigFile(doRemember)
|
||||
if err != nil && doRemember {
|
||||
return
|
||||
}
|
||||
b, errOpen := os.ReadFile(configFile)
|
||||
if errOpen == nil && !doRemember {
|
||||
var rememberedOptions croc.Options
|
||||
err = json.Unmarshal(b, &rememberedOptions)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
// update anything that isn't explicitly Globally set
|
||||
if !c.IsSet("relay") && rememberedOptions.RelayAddress != "" {
|
||||
crocOptions.RelayAddress = rememberedOptions.RelayAddress
|
||||
}
|
||||
if !c.IsSet("yes") {
|
||||
crocOptions.NoPrompt = rememberedOptions.NoPrompt
|
||||
}
|
||||
if crocOptions.SharedSecret == "" {
|
||||
crocOptions.SharedSecret = rememberedOptions.SharedSecret
|
||||
}
|
||||
if !c.IsSet("pass") && rememberedOptions.RelayPassword != "" {
|
||||
crocOptions.RelayPassword = rememberedOptions.RelayPassword
|
||||
}
|
||||
if !c.IsSet("relay6") && rememberedOptions.RelayAddress6 != "" {
|
||||
crocOptions.RelayAddress6 = rememberedOptions.RelayAddress6
|
||||
}
|
||||
if !c.IsSet("overwrite") {
|
||||
crocOptions.Overwrite = rememberedOptions.Overwrite
|
||||
}
|
||||
if !c.IsSet("curve") && rememberedOptions.Curve != "" {
|
||||
crocOptions.Curve = rememberedOptions.Curve
|
||||
}
|
||||
if !c.IsSet("local") {
|
||||
crocOptions.OnlyLocal = rememberedOptions.OnlyLocal
|
||||
}
|
||||
}
|
||||
|
||||
classicInsecureMode := utils.Exists(getClassicConfigFile(true))
|
||||
if crocOptions.SharedSecret == "" && os.Getenv("CROC_SECRET") != "" {
|
||||
crocOptions.SharedSecret = os.Getenv("CROC_SECRET")
|
||||
} else if !(runtime.GOOS == "windows") && crocOptions.SharedSecret != "" && !classicInsecureMode {
|
||||
crocOptions.SharedSecret = os.Getenv("CROC_SECRET")
|
||||
if crocOptions.SharedSecret == "" {
|
||||
fmt.Printf(`On UNIX systems, to receive with croc you either need
|
||||
to set a code phrase using your environmental variables:
|
||||
|
||||
export CROC_SECRET="****"
|
||||
croc
|
||||
|
||||
Or you can specify the code phrase when you run croc without
|
||||
declaring the secret on the command line:
|
||||
|
||||
croc
|
||||
Enter receive code: ****
|
||||
|
||||
Or you can go back to the classic croc behavior by enabling classic mode:
|
||||
|
||||
croc --classic
|
||||
|
||||
`)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
if crocOptions.SharedSecret == "" {
|
||||
l, err := readline.NewEx(&readline.Config{
|
||||
Prompt: "Enter receive code: ",
|
||||
AutoComplete: TabComplete{},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
crocOptions.SharedSecret, err = l.Readline()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if c.String("out") != "" {
|
||||
if err = os.Chdir(c.String("out")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cr, err := croc.New(crocOptions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// save the config
|
||||
if doRemember {
|
||||
log.Debug("saving config file")
|
||||
var bConfig []byte
|
||||
bConfig, err = json.MarshalIndent(crocOptions, "", " ")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
err = os.WriteFile(configFile, bConfig, 0o644)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
log.Debugf("wrote %s", configFile)
|
||||
}
|
||||
|
||||
err = cr.Receive()
|
||||
return
|
||||
}
|
||||
|
||||
func relay(c *cli.Context) (err error) {
|
||||
log.Infof("starting croc relay version %v", Version)
|
||||
debugString := "info"
|
||||
if c.Bool("debug") {
|
||||
debugString = "debug"
|
||||
}
|
||||
host := c.String("host")
|
||||
ports := strings.Split(c.String("ports"), ",")
|
||||
tcpPorts := strings.Join(ports[1:], ",")
|
||||
for i, port := range ports {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
go func(portStr string) {
|
||||
err := tcp.Run(debugString, host, portStr, determinePass(c))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}(port)
|
||||
}
|
||||
return tcp.Run(debugString, host, ports[0], determinePass(c), tcpPorts)
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
package comm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/magisterquis/connectproxy"
|
||||
"github.com/schollz/croc/v10/src/utils"
|
||||
log "github.com/schollz/logger"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
var Socks5Proxy = ""
|
||||
var HttpProxy = ""
|
||||
|
||||
var MAGIC_BYTES = []byte("croc")
|
||||
|
||||
// Comm is some basic TCP communication
|
||||
type Comm struct {
|
||||
connection net.Conn
|
||||
}
|
||||
|
||||
// NewConnection gets a new comm to a tcp address
|
||||
func NewConnection(address string, timelimit ...time.Duration) (c *Comm, err error) {
|
||||
tlimit := 30 * time.Second
|
||||
if len(timelimit) > 0 {
|
||||
tlimit = timelimit[0]
|
||||
}
|
||||
var connection net.Conn
|
||||
if Socks5Proxy != "" && !utils.IsLocalIP(address) {
|
||||
var dialer proxy.Dialer
|
||||
// prepend schema if no schema is given
|
||||
if !strings.Contains(Socks5Proxy, `://`) {
|
||||
Socks5Proxy = `socks5://` + Socks5Proxy
|
||||
}
|
||||
socks5ProxyURL, urlParseError := url.Parse(Socks5Proxy)
|
||||
if urlParseError != nil {
|
||||
err = fmt.Errorf("unable to parse socks proxy url: %s", urlParseError)
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
dialer, err = proxy.FromURL(socks5ProxyURL, proxy.Direct)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("proxy failed: %w", err)
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
log.Debug("dialing with dialer.Dial")
|
||||
connection, err = dialer.Dial("tcp", address)
|
||||
} else if HttpProxy != "" && !utils.IsLocalIP(address) {
|
||||
var dialer proxy.Dialer
|
||||
// prepend schema if no schema is given
|
||||
if !strings.Contains(HttpProxy, `://`) {
|
||||
HttpProxy = `http://` + HttpProxy
|
||||
}
|
||||
HttpProxyURL, urlParseError := url.Parse(HttpProxy)
|
||||
if urlParseError != nil {
|
||||
err = fmt.Errorf("unable to parse http proxy url: %s", urlParseError)
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
dialer, err = connectproxy.New(HttpProxyURL, proxy.Direct)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("proxy failed: %w", err)
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
log.Debug("dialing with dialer.Dial")
|
||||
connection, err = dialer.Dial("tcp", address)
|
||||
|
||||
} else {
|
||||
log.Debugf("dialing to %s with timelimit %s", address, tlimit)
|
||||
connection, err = net.DialTimeout("tcp", address, tlimit)
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("comm.NewConnection failed: %w", err)
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
c = New(connection)
|
||||
log.Debugf("connected to '%s'", address)
|
||||
return
|
||||
}
|
||||
|
||||
// New returns a new comm
|
||||
func New(c net.Conn) *Comm {
|
||||
if err := c.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil {
|
||||
log.Warnf("error setting read deadline: %v", err)
|
||||
}
|
||||
if err := c.SetDeadline(time.Now().Add(3 * time.Hour)); err != nil {
|
||||
log.Warnf("error setting overall deadline: %v", err)
|
||||
}
|
||||
if err := c.SetWriteDeadline(time.Now().Add(3 * time.Hour)); err != nil {
|
||||
log.Errorf("error setting write deadline: %v", err)
|
||||
}
|
||||
comm := new(Comm)
|
||||
comm.connection = c
|
||||
return comm
|
||||
}
|
||||
|
||||
// Connection returns the net.Conn connection
|
||||
func (c *Comm) Connection() net.Conn {
|
||||
return c.connection
|
||||
}
|
||||
|
||||
// Close closes the connection
|
||||
func (c *Comm) Close() {
|
||||
if err := c.connection.Close(); err != nil {
|
||||
log.Warnf("error closing connection: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Comm) Write(b []byte) (n int, err error) {
|
||||
header := new(bytes.Buffer)
|
||||
err = binary.Write(header, binary.LittleEndian, uint32(len(b)))
|
||||
if err != nil {
|
||||
fmt.Println("binary.Write failed:", err)
|
||||
}
|
||||
tmpCopy := append(header.Bytes(), b...)
|
||||
tmpCopy = append(MAGIC_BYTES, tmpCopy...)
|
||||
n, err = c.connection.Write(tmpCopy)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("connection.Write failed: %w", err)
|
||||
return
|
||||
}
|
||||
if n != len(tmpCopy) {
|
||||
err = fmt.Errorf("wanted to write %d but wrote %d", len(b), n)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Comm) Read() (buf []byte, numBytes int, bs []byte, err error) {
|
||||
// long read deadline in case waiting for file
|
||||
if err = c.connection.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil {
|
||||
log.Warnf("error setting read deadline: %v", err)
|
||||
}
|
||||
// must clear the timeout setting
|
||||
defer c.connection.SetDeadline(time.Time{})
|
||||
|
||||
// read until we get 4 bytes for the magic
|
||||
header := make([]byte, 4)
|
||||
_, err = io.ReadFull(c.connection, header)
|
||||
if err != nil {
|
||||
log.Debugf("initial read error: %v", err)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(header, MAGIC_BYTES) {
|
||||
err = fmt.Errorf("initial bytes are not magic: %x", header)
|
||||
return
|
||||
}
|
||||
|
||||
// read until we get 4 bytes for the header
|
||||
header = make([]byte, 4)
|
||||
_, err = io.ReadFull(c.connection, header)
|
||||
if err != nil {
|
||||
log.Debugf("initial read error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var numBytesUint32 uint32
|
||||
rbuf := bytes.NewReader(header)
|
||||
err = binary.Read(rbuf, binary.LittleEndian, &numBytesUint32)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("binary.Read failed: %w", err)
|
||||
log.Debug(err.Error())
|
||||
return
|
||||
}
|
||||
numBytes = int(numBytesUint32)
|
||||
|
||||
// shorten the reading deadline in case getting weird data
|
||||
if err = c.connection.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
||||
log.Warnf("error setting read deadline: %v", err)
|
||||
}
|
||||
buf = make([]byte, numBytes)
|
||||
_, err = io.ReadFull(c.connection, buf)
|
||||
if err != nil {
|
||||
log.Debugf("consecutive read error: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send a message
|
||||
func (c *Comm) Send(message []byte) (err error) {
|
||||
_, err = c.Write(message)
|
||||
return
|
||||
}
|
||||
|
||||
// Receive a message
|
||||
func (c *Comm) Receive() (b []byte, err error) {
|
||||
b, _, _, err = c.Read()
|
||||
return
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package comm
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
log "github.com/schollz/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestComm(t *testing.T) {
|
||||
token := make([]byte, 3000)
|
||||
if _, err := rand.Read(token); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
port := "8001"
|
||||
go func() {
|
||||
log.Debugf("starting TCP server on " + port)
|
||||
server, err := net.Listen("tcp", "0.0.0.0:"+port)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
defer server.Close()
|
||||
// spawn a new goroutine whenever a client connects
|
||||
for {
|
||||
connection, err := server.Accept()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
log.Debugf("client %s connected", connection.RemoteAddr().String())
|
||||
go func(_ string, connection net.Conn) {
|
||||
c := New(connection)
|
||||
err = c.Send([]byte("hello, world"))
|
||||
assert.Nil(t, err)
|
||||
data, err := c.Receive()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("hello, computer"), data)
|
||||
data, err = c.Receive()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte{'\x00'}, data)
|
||||
data, err = c.Receive()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, token, data)
|
||||
}(port, connection)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
a, err := NewConnection("127.0.0.1:"+port, 10*time.Minute)
|
||||
assert.Nil(t, err)
|
||||
data, err := a.Receive()
|
||||
assert.Equal(t, []byte("hello, world"), data)
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, a.Send([]byte("hello, computer")))
|
||||
assert.Nil(t, a.Send([]byte{'\x00'}))
|
||||
|
||||
assert.Nil(t, a.Send(token))
|
||||
_ = a.Connection()
|
||||
a.Close()
|
||||
assert.NotNil(t, a.Send(token))
|
||||
_, err = a.Write(token)
|
||||
assert.NotNil(t, err)
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package compress
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/flate"
|
||||
"io"
|
||||
|
||||
log "github.com/schollz/logger"
|
||||
)
|
||||
|
||||
// CompressWithOption returns compressed data using the specified level
|
||||
func CompressWithOption(src []byte, level int) []byte {
|
||||
compressedData := new(bytes.Buffer)
|
||||
compress(src, compressedData, level)
|
||||
return compressedData.Bytes()
|
||||
}
|
||||
|
||||
// Compress returns a compressed byte slice.
|
||||
func Compress(src []byte) []byte {
|
||||
compressedData := new(bytes.Buffer)
|
||||
compress(src, compressedData, -2)
|
||||
return compressedData.Bytes()
|
||||
}
|
||||
|
||||
// Decompress returns a decompressed byte slice.
|
||||
func Decompress(src []byte) []byte {
|
||||
compressedData := bytes.NewBuffer(src)
|
||||
deCompressedData := new(bytes.Buffer)
|
||||
decompress(compressedData, deCompressedData)
|
||||
return deCompressedData.Bytes()
|
||||
}
|
||||
|
||||
// compress uses flate to compress a byte slice to a corresponding level
|
||||
func compress(src []byte, dest io.Writer, level int) {
|
||||
compressor, err := flate.NewWriter(dest, level)
|
||||
if err != nil {
|
||||
log.Debugf("error level data: %v", err)
|
||||
return
|
||||
}
|
||||
if _, err := compressor.Write(src); err != nil {
|
||||
log.Debugf("error writing data: %v", err)
|
||||
}
|
||||
compressor.Close()
|
||||
}
|
||||
|
||||
// compress uses flate to decompress an io.Reader
|
||||
func decompress(src io.Reader, dest io.Writer) {
|
||||
decompressor := flate.NewReader(src)
|
||||
if _, err := io.Copy(dest, decompressor); err != nil {
|
||||
log.Debugf("error copying data: %v", err)
|
||||
}
|
||||
decompressor.Close()
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package compress
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var fable = []byte(`The Frog and the Crocodile
|
||||
Once, there was a frog who lived in the middle of a swamp. His entire family had lived in that swamp for generations, but this particular frog decided that he had had quite enough wetness to last him a lifetime. He decided that he was going to find a dry place to live instead.
|
||||
|
||||
The only thing that separated him from dry land was a swampy, muddy, swiftly flowing river. But the river was home to all sorts of slippery, slittering snakes that loved nothing better than a good, plump frog for dinner, so Frog didn't dare try to swim across.
|
||||
|
||||
So for many days, the frog stayed put, hopping along the bank, trying to think of a way to get across.
|
||||
|
||||
The snakes hissed and jeered at him, daring him to come closer, but he refused. Occasionally they would slither closer, jaws open to attack, but the frog always leaped out of the way. But no matter how far upstream he searched or how far downstream, the frog wasn't able to find a way across the water.
|
||||
|
||||
He had felt certain that there would be a bridge, or a place where the banks came together, yet all he found was more reeds and water. After a while, even the snakes stopped teasing him and went off in search of easier prey.
|
||||
|
||||
The frog sighed in frustration and sat to sulk in the rushes. Suddenly, he spotted two big eyes staring at him from the water. The giant log-shaped animal opened its mouth and asked him, "What are you doing, Frog? Surely there are enough flies right there for a meal."
|
||||
|
||||
The frog croaked in surprise and leaped away from the crocodile. That creature could swallow him whole in a moment without thinking about it! Once he was a satisfied that he was a safe distance away, he answered. "I'm tired of living in swampy waters, and I want to travel to the other side of the river. But if I swim across, the snakes will eat me."
|
||||
|
||||
The crocodile harrumphed in agreement and sat, thinking, for a while. "Well, if you're afraid of the snakes, I could give you a ride across," he suggested.
|
||||
|
||||
"Oh no, I don't think so," Frog answered quickly. "You'd eat me on the way over, or go underwater so the snakes could get me!"
|
||||
|
||||
"Now why would I let the snakes get you? I think they're a terrible nuisance with all their hissing and slithering! The river would be much better off without them altogether! Anyway, if you're so worried that I might eat you, you can ride on my tail."
|
||||
|
||||
The frog considered his offer. He did want to get to dry ground very badly, and there didn't seem to be any other way across the river. He looked at the crocodile from his short, squat buggy eyes and wondered about the crocodile's motives. But if he rode on the tail, the croc couldn't eat him anyway. And he was right about the snakes--no self-respecting crocodile would give a meal to the snakes.
|
||||
|
||||
"Okay, it sounds like a good plan to me. Turn around so I can hop on your tail."
|
||||
|
||||
The crocodile flopped his tail into the marshy mud and let the frog climb on, then he waddled out to the river. But he couldn't stick his tail into the water as a rudder because the frog was on it -- and if he put his tail in the water, the snakes would eat the frog. They clumsily floated downstream for a ways, until the crocodile said, "Hop onto my back so I can steer straight with my tail." The frog moved, and the journey smoothed out.
|
||||
|
||||
From where he was sitting, the frog couldn't see much except the back of Crocodile's head. "Why don't you hop up on my head so you can see everything around us?" Crocodile invited. `)
|
||||
|
||||
func BenchmarkCompressLevelMinusTwo(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
CompressWithOption(fable, -2)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompressLevelNine(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
CompressWithOption(fable, 9)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompressLevelMinusTwoBinary(b *testing.B) {
|
||||
data := make([]byte, 1000000)
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
CompressWithOption(data, -2)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompressLevelNineBinary(b *testing.B) {
|
||||
data := make([]byte, 1000000)
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
CompressWithOption(data, 9)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompress(t *testing.T) {
|
||||
compressedB := CompressWithOption(fable, 9)
|
||||
dataRateSavings := 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
|
||||
fmt.Printf("Level 9: %2.0f%% percent space savings\n", dataRateSavings)
|
||||
assert.True(t, len(compressedB) < len(fable))
|
||||
assert.Equal(t, fable, Decompress(compressedB))
|
||||
|
||||
compressedB = CompressWithOption(fable, -2)
|
||||
dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
|
||||
fmt.Printf("Level -2: %2.0f%% percent space savings\n", dataRateSavings)
|
||||
assert.True(t, len(compressedB) < len(fable))
|
||||
|
||||
compressedB = Compress(fable)
|
||||
dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(fable)))
|
||||
fmt.Printf("Level -2: %2.0f%% percent space savings\n", dataRateSavings)
|
||||
assert.True(t, len(compressedB) < len(fable))
|
||||
|
||||
data := make([]byte, 4096)
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compressedB = CompressWithOption(data, -2)
|
||||
dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(data)))
|
||||
fmt.Printf("random, Level -2: %2.0f%% percent space savings\n", dataRateSavings)
|
||||
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compressedB = CompressWithOption(data, 9)
|
||||
dataRateSavings = 100 * (1.0 - float64(len(compressedB))/float64(len(data)))
|
||||
|
||||
fmt.Printf("random, Level 9: %2.0f%% percent space savings\n", dataRateSavings)
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,400 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/schollz/croc/v10/src/tcp"
|
||||
log "github.com/schollz/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetLevel("trace")
|
||||
|
||||
go tcp.Run("debug", "127.0.0.1", "8281", "pass123", "8282,8283,8284,8285")
|
||||
go tcp.Run("debug", "127.0.0.1", "8282", "pass123")
|
||||
go tcp.Run("debug", "127.0.0.1", "8283", "pass123")
|
||||
go tcp.Run("debug", "127.0.0.1", "8284", "pass123")
|
||||
go tcp.Run("debug", "127.0.0.1", "8285", "pass123")
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
func TestCrocReadme(t *testing.T) {
|
||||
defer os.Remove("README.md")
|
||||
|
||||
log.Debug("setting up sender")
|
||||
sender, err := New(Options{
|
||||
IsSender: true,
|
||||
SharedSecret: "8123-testingthecroc",
|
||||
Debug: true,
|
||||
RelayAddress: "127.0.0.1:8281",
|
||||
RelayPorts: []string{"8281"},
|
||||
RelayPassword: "pass123",
|
||||
Stdout: false,
|
||||
NoPrompt: true,
|
||||
DisableLocal: true,
|
||||
Curve: "siec",
|
||||
Overwrite: true,
|
||||
GitIgnore: false,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Debug("setting up receiver")
|
||||
receiver, err := New(Options{
|
||||
IsSender: false,
|
||||
SharedSecret: "8123-testingthecroc",
|
||||
Debug: true,
|
||||
RelayAddress: "127.0.0.1:8281",
|
||||
RelayPassword: "pass123",
|
||||
Stdout: false,
|
||||
NoPrompt: true,
|
||||
DisableLocal: true,
|
||||
Curve: "siec",
|
||||
Overwrite: true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{"../../README.md"}, false, false)
|
||||
if errGet != nil {
|
||||
t.Errorf("failed to get minimal info: %v", errGet)
|
||||
}
|
||||
err := sender.Send(filesInfo, emptyFolders, totalNumberFolders)
|
||||
if err != nil {
|
||||
t.Errorf("send failed: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
go func() {
|
||||
err := receiver.Receive()
|
||||
if err != nil {
|
||||
t.Errorf("receive failed: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestCrocEmptyFolder(t *testing.T) {
|
||||
pathName := "../../testEmpty"
|
||||
defer os.RemoveAll(pathName)
|
||||
defer os.RemoveAll("./testEmpty")
|
||||
os.MkdirAll(pathName, 0o755)
|
||||
|
||||
log.Debug("setting up sender")
|
||||
sender, err := New(Options{
|
||||
IsSender: true,
|
||||
SharedSecret: "8123-testingthecroc",
|
||||
Debug: true,
|
||||
RelayAddress: "127.0.0.1:8281",
|
||||
RelayPorts: []string{"8281"},
|
||||
RelayPassword: "pass123",
|
||||
Stdout: false,
|
||||
NoPrompt: true,
|
||||
DisableLocal: true,
|
||||
Curve: "siec",
|
||||
Overwrite: true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Debug("setting up receiver")
|
||||
receiver, err := New(Options{
|
||||
IsSender: false,
|
||||
SharedSecret: "8123-testingthecroc",
|
||||
Debug: true,
|
||||
RelayAddress: "127.0.0.1:8281",
|
||||
RelayPassword: "pass123",
|
||||
Stdout: false,
|
||||
NoPrompt: true,
|
||||
DisableLocal: true,
|
||||
Curve: "siec",
|
||||
Overwrite: true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{pathName}, false, false)
|
||||
if errGet != nil {
|
||||
t.Errorf("failed to get minimal info: %v", errGet)
|
||||
}
|
||||
err := sender.Send(filesInfo, emptyFolders, totalNumberFolders)
|
||||
if err != nil {
|
||||
t.Errorf("send failed: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
go func() {
|
||||
err := receiver.Receive()
|
||||
if err != nil {
|
||||
t.Errorf("receive failed: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestCrocSymlink(t *testing.T) {
|
||||
pathName := "../link-in-folder"
|
||||
defer os.RemoveAll(pathName)
|
||||
defer os.RemoveAll("./link-in-folder")
|
||||
os.MkdirAll(pathName, 0o755)
|
||||
os.Symlink("../../README.md", filepath.Join(pathName, "README.link"))
|
||||
|
||||
log.Debug("setting up sender")
|
||||
sender, err := New(Options{
|
||||
IsSender: true,
|
||||
SharedSecret: "8124-testingthecroc",
|
||||
Debug: true,
|
||||
RelayAddress: "127.0.0.1:8281",
|
||||
RelayPorts: []string{"8281"},
|
||||
RelayPassword: "pass123",
|
||||
Stdout: false,
|
||||
NoPrompt: true,
|
||||
DisableLocal: true,
|
||||
Curve: "siec",
|
||||
Overwrite: true,
|
||||
GitIgnore: false,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Debug("setting up receiver")
|
||||
receiver, err := New(Options{
|
||||
IsSender: false,
|
||||
SharedSecret: "8124-testingthecroc",
|
||||
Debug: true,
|
||||
RelayAddress: "127.0.0.1:8281",
|
||||
RelayPassword: "pass123",
|
||||
Stdout: false,
|
||||
NoPrompt: true,
|
||||
DisableLocal: true,
|
||||
Curve: "siec",
|
||||
Overwrite: true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{pathName}, false, false)
|
||||
if errGet != nil {
|
||||
t.Errorf("failed to get minimal info: %v", errGet)
|
||||
}
|
||||
err = sender.Send(filesInfo, emptyFolders, totalNumberFolders)
|
||||
if err != nil {
|
||||
t.Errorf("send failed: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
go func() {
|
||||
err = receiver.Receive()
|
||||
if err != nil {
|
||||
t.Errorf("receive failed: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
s, err := filepath.EvalSymlinks(path.Join(pathName, "README.link"))
|
||||
if s != "../../README.md" && s != "..\\..\\README.md" {
|
||||
log.Debug(s)
|
||||
t.Errorf("symlink failed to transfer in folder")
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("symlink transfer failed: %s", err.Error())
|
||||
}
|
||||
}
|
||||
func testCrocIgnoreGit(t *testing.T) {
|
||||
log.SetLevel("trace")
|
||||
defer os.Remove(".gitignore")
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
file, err := os.Create(".gitignore")
|
||||
if err != nil {
|
||||
log.Errorf("error creating file")
|
||||
}
|
||||
_, err = file.WriteString("LICENSE")
|
||||
if err != nil {
|
||||
log.Errorf("error writing to file")
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
// due to how files are ignored in this function, all we have to do to test is make sure LICENSE doesn't get included in FilesInfo.
|
||||
filesInfo, _, _, errGet := GetFilesInfo([]string{"../../LICENSE", ".gitignore", "croc.go"}, false, true)
|
||||
if errGet != nil {
|
||||
t.Errorf("failed to get minimal info: %v", errGet)
|
||||
}
|
||||
for _, file := range filesInfo {
|
||||
if strings.Contains(file.Name, "LICENSE") {
|
||||
t.Errorf("test failed, should ignore LICENSE")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrocLocal(t *testing.T) {
|
||||
log.SetLevel("trace")
|
||||
defer os.Remove("LICENSE")
|
||||
defer os.Remove("touched")
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
log.Debug("setting up sender")
|
||||
sender, err := New(Options{
|
||||
IsSender: true,
|
||||
SharedSecret: "8123-testingthecroc",
|
||||
Debug: true,
|
||||
RelayAddress: "127.0.0.1:8181",
|
||||
RelayPorts: []string{"8181", "8182"},
|
||||
RelayPassword: "pass123",
|
||||
Stdout: true,
|
||||
NoPrompt: true,
|
||||
DisableLocal: false,
|
||||
Curve: "siec",
|
||||
Overwrite: true,
|
||||
GitIgnore: false,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
log.Debug("setting up receiver")
|
||||
receiver, err := New(Options{
|
||||
IsSender: false,
|
||||
SharedSecret: "8123-testingthecroc",
|
||||
Debug: true,
|
||||
RelayAddress: "127.0.0.1:8181",
|
||||
RelayPassword: "pass123",
|
||||
Stdout: true,
|
||||
NoPrompt: true,
|
||||
DisableLocal: false,
|
||||
Curve: "siec",
|
||||
Overwrite: true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
os.Create("touched")
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{"../../LICENSE", "touched"}, false, false)
|
||||
if errGet != nil {
|
||||
t.Errorf("failed to get minimal info: %v", errGet)
|
||||
}
|
||||
err := sender.Send(filesInfo, emptyFolders, totalNumberFolders)
|
||||
if err != nil {
|
||||
t.Errorf("send failed: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
go func() {
|
||||
err := receiver.Receive()
|
||||
if err != nil {
|
||||
t.Errorf("send failed: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestCrocError(t *testing.T) {
|
||||
content := []byte("temporary file's content")
|
||||
tmpfile, err := os.CreateTemp("", "example")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
|
||||
if _, err = tmpfile.Write(content); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = tmpfile.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Debug(false)
|
||||
log.SetLevel("warn")
|
||||
sender, _ := New(Options{
|
||||
IsSender: true,
|
||||
SharedSecret: "8123-testingthecroc2",
|
||||
Debug: true,
|
||||
RelayAddress: "doesntexistok.com:8381",
|
||||
RelayPorts: []string{"8381", "8382"},
|
||||
RelayPassword: "pass123",
|
||||
Stdout: true,
|
||||
NoPrompt: true,
|
||||
DisableLocal: true,
|
||||
Curve: "siec",
|
||||
Overwrite: true,
|
||||
})
|
||||
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{tmpfile.Name()}, false, false)
|
||||
if errGet != nil {
|
||||
t.Errorf("failed to get minimal info: %v", errGet)
|
||||
}
|
||||
err = sender.Send(filesInfo, emptyFolders, totalNumberFolders)
|
||||
log.Debug(err)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestCleanUp(t *testing.T) {
|
||||
// windows allows files to be deleted only if they
|
||||
// are not open by another program so the remove actions
|
||||
// from the above tests will not always do a good clean up
|
||||
// This "test" will make sure
|
||||
operatingSystem := runtime.GOOS
|
||||
log.Debugf("The operating system is %s", operatingSystem)
|
||||
if operatingSystem == "windows" {
|
||||
time.Sleep(1 * time.Second)
|
||||
log.Debug("Full cleanup")
|
||||
var err error
|
||||
|
||||
for _, file := range []string{"README.md", "./README.md"} {
|
||||
err = os.Remove(file)
|
||||
if err == nil {
|
||||
log.Debugf("Successfully purged %s", file)
|
||||
} else {
|
||||
log.Debugf("%s was already purged.", file)
|
||||
}
|
||||
}
|
||||
for _, folder := range []string{"./testEmpty", "./link-in-folder"} {
|
||||
err = os.RemoveAll(folder)
|
||||
if err == nil {
|
||||
log.Debugf("Successfully purged %s", folder)
|
||||
} else {
|
||||
log.Debugf("%s was already purged.", folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package crypt
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
// New generates a new key based on a passphrase and salt
|
||||
func New(passphrase []byte, usersalt []byte) (key []byte, salt []byte, err error) {
|
||||
if len(passphrase) < 1 {
|
||||
err = fmt.Errorf("need more than that for passphrase")
|
||||
return
|
||||
}
|
||||
if usersalt == nil {
|
||||
salt = make([]byte, 8)
|
||||
// http://www.ietf.org/rfc/rfc2898.txt
|
||||
// Salt.
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
log.Fatalf("can't get random salt: %v", err)
|
||||
}
|
||||
} else {
|
||||
salt = usersalt
|
||||
}
|
||||
key = pbkdf2.Key(passphrase, salt, 100, 32, sha256.New)
|
||||
return
|
||||
}
|
||||
|
||||
// Encrypt will encrypt using the pre-generated key
|
||||
func Encrypt(plaintext []byte, key []byte) (encrypted []byte, err error) {
|
||||
// generate a random iv each time
|
||||
// http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
|
||||
// Section 8.2
|
||||
ivBytes := make([]byte, 12)
|
||||
if _, err = rand.Read(ivBytes); err != nil {
|
||||
log.Fatalf("can't initialize crypto: %v", err)
|
||||
}
|
||||
b, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
aesgcm, err := cipher.NewGCM(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encrypted = aesgcm.Seal(nil, ivBytes, plaintext, nil)
|
||||
encrypted = append(ivBytes, encrypted...)
|
||||
return
|
||||
}
|
||||
|
||||
// Decrypt using the pre-generated key
|
||||
func Decrypt(encrypted []byte, key []byte) (plaintext []byte, err error) {
|
||||
if len(encrypted) < 13 {
|
||||
err = fmt.Errorf("incorrect passphrase")
|
||||
return
|
||||
}
|
||||
b, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
aesgcm, err := cipher.NewGCM(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
plaintext, err = aesgcm.Open(nil, encrypted[:12], encrypted[12:], nil)
|
||||
return
|
||||
}
|
||||
|
||||
// NewArgon2 generates a new key based on a passphrase and salt
|
||||
// using argon2
|
||||
// https://pkg.go.dev/golang.org/x/crypto/argon2
|
||||
func NewArgon2(passphrase []byte, usersalt []byte) (aead cipher.AEAD, salt []byte, err error) {
|
||||
if len(passphrase) < 1 {
|
||||
err = fmt.Errorf("need more than that for passphrase")
|
||||
return
|
||||
}
|
||||
if usersalt == nil {
|
||||
salt = make([]byte, 8)
|
||||
// http://www.ietf.org/rfc/rfc2898.txt
|
||||
// Salt.
|
||||
if _, err = rand.Read(salt); err != nil {
|
||||
log.Fatalf("can't get random salt: %v", err)
|
||||
}
|
||||
} else {
|
||||
salt = usersalt
|
||||
}
|
||||
aead, err = chacha20poly1305.NewX(argon2.IDKey(passphrase, salt, 1, 64*1024, 4, 32))
|
||||
return
|
||||
}
|
||||
|
||||
// EncryptChaCha will encrypt ChaCha20-Poly1305 using the pre-generated key
|
||||
// https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305
|
||||
func EncryptChaCha(plaintext []byte, aead cipher.AEAD) (encrypted []byte, err error) {
|
||||
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(plaintext)+aead.Overhead())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Encrypt the message and append the ciphertext to the nonce.
|
||||
encrypted = aead.Seal(nonce, nonce, plaintext, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// DecryptChaCha will encrypt ChaCha20-Poly1305 using the pre-generated key
|
||||
// https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305
|
||||
func DecryptChaCha(encryptedMsg []byte, aead cipher.AEAD) (encrypted []byte, err error) {
|
||||
if len(encryptedMsg) < aead.NonceSize() {
|
||||
err = fmt.Errorf("ciphertext too short")
|
||||
return
|
||||
}
|
||||
|
||||
// Split nonce and ciphertext.
|
||||
nonce, ciphertext := encryptedMsg[:aead.NonceSize()], encryptedMsg[aead.NonceSize():]
|
||||
|
||||
// Decrypt the message and check it wasn't tampered with.
|
||||
encrypted, err = aead.Open(nil, nonce, ciphertext, nil)
|
||||
return
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package crypt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BenchmarkEncrypt(b *testing.B) {
|
||||
bob, _, _ := New([]byte("password"), nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
Encrypt([]byte("hello, world"), bob)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecrypt(b *testing.B) {
|
||||
key, _, _ := New([]byte("password"), nil)
|
||||
msg := []byte("hello, world")
|
||||
enc, _ := Encrypt(msg, key)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Decrypt(enc, key)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncryptChaCha(b *testing.B) {
|
||||
bob, _, _ := NewArgon2([]byte("password"), nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
EncryptChaCha([]byte("hello, world"), bob)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecryptChaCha(b *testing.B) {
|
||||
key, _, _ := NewArgon2([]byte("password"), nil)
|
||||
msg := []byte("hello, world")
|
||||
enc, _ := EncryptChaCha(msg, key)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
DecryptChaCha(enc, key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryption(t *testing.T) {
|
||||
key, salt, err := New([]byte("password"), nil)
|
||||
assert.Nil(t, err)
|
||||
msg := []byte("hello, world")
|
||||
enc, err := Encrypt(msg, key)
|
||||
assert.Nil(t, err)
|
||||
dec, err := Decrypt(enc, key)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, msg, dec)
|
||||
|
||||
// check reusing the salt
|
||||
key2, _, _ := New([]byte("password"), salt)
|
||||
dec, err = Decrypt(enc, key2)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, msg, dec)
|
||||
|
||||
// check reusing the salt
|
||||
key2, _, _ = New([]byte("wrong password"), salt)
|
||||
dec, err = Decrypt(enc, key2)
|
||||
assert.NotNil(t, err)
|
||||
assert.NotEqual(t, msg, dec)
|
||||
|
||||
// error with no password
|
||||
_, err = Decrypt([]byte(""), key)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// error with small password
|
||||
_, _, err = New([]byte(""), nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestEncryptionChaCha(t *testing.T) {
|
||||
key, salt, err := NewArgon2([]byte("password"), nil)
|
||||
fmt.Printf("key: %x\n", key)
|
||||
assert.Nil(t, err)
|
||||
msg := []byte("hello, world")
|
||||
enc, err := EncryptChaCha(msg, key)
|
||||
assert.Nil(t, err)
|
||||
dec, err := DecryptChaCha(enc, key)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, msg, dec)
|
||||
|
||||
// check reusing the salt
|
||||
key2, _, _ := NewArgon2([]byte("password"), salt)
|
||||
dec, err = DecryptChaCha(enc, key2)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, msg, dec)
|
||||
|
||||
// check reusing the salt
|
||||
key2, _, _ = NewArgon2([]byte("wrong password"), salt)
|
||||
dec, err = DecryptChaCha(enc, key2)
|
||||
assert.NotNil(t, err)
|
||||
assert.NotEqual(t, msg, dec)
|
||||
|
||||
// error with no password
|
||||
_, err = DecryptChaCha([]byte(""), key)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// error with small password
|
||||
_, _, err = NewArgon2([]byte(""), nil)
|
||||
assert.NotNil(t, err)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
# VERSION=8.X.Y make release
|
||||
|
||||
release:
|
||||
cd ../../ && go run src/install/updateversion.go
|
||||
git commit -am "bump ${VERSION}"
|
||||
git tag -af v${VERSION} -m "v${VERSION}"
|
||||
git push
|
||||
git push --tags
|
||||
cp zsh_autocomplete ../../
|
||||
cp bash_autocomplete ../../
|
||||
cd ../../ && goreleaser release
|
||||
cd ../../ && ./src/install/prepare-sources-tarball.sh
|
||||
cd ../../ && ./src/install/upload-src-tarball.sh
|
||||
|
||||
test:
|
||||
cp zsh_autocomplete ../../
|
||||
cp bash_autocomplete ../../
|
||||
cd ../../ && go generate
|
||||
cd ../../ && goreleaser release --skip-publish
|
|
@ -0,0 +1,19 @@
|
|||
: ${PROG:=$(basename ${BASH_SOURCE})}
|
||||
|
||||
_cli_bash_autocomplete() {
|
||||
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
|
||||
local cur opts base
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == "-"* ]]; then
|
||||
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
|
||||
else
|
||||
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
|
||||
fi
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG
|
||||
unset PROG
|
Binary file not shown.
After Width: | Height: | Size: 582 KiB |
|
@ -0,0 +1,762 @@
|
|||
#!/bin/bash -
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: default.txt
|
||||
#
|
||||
# USAGE: curl https://getcroc.schollz.com | bash
|
||||
# OR
|
||||
# wget -qO- https://getcroc.schollz.com | bash
|
||||
#
|
||||
# DESCRIPTION: croc Installer Script.
|
||||
#
|
||||
# This script installs croc into a specified prefix.
|
||||
# Default prefix = /usr/local/bin
|
||||
#
|
||||
# OPTIONS: -p, --prefix "${INSTALL_PREFIX}"
|
||||
# Prefix to install croc into. Defaults to /usr/local/bin
|
||||
# REQUIREMENTS: bash, uname, tar/unzip, curl/wget, sudo (if not run
|
||||
# as root), install, mktemp, sha256sum/shasum/sha256
|
||||
#
|
||||
# BUGS: ...hopefully not. Please report.
|
||||
#
|
||||
# NOTES: Homepage: https://schollz.com/software/croc
|
||||
# Issues: https://github.com/schollz/croc/issues
|
||||
#
|
||||
# CREATED: 08/10/2019 16:41
|
||||
# REVISION: 0.9.2
|
||||
#===============================================================================
|
||||
set -o nounset # Treat unset variables as an error
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# DEFAULTS
|
||||
#-------------------------------------------------------------------------------
|
||||
PREFIX="${PREFIX:-}"
|
||||
ANDROID_ROOT="${ANDROID_ROOT:-}"
|
||||
|
||||
# Termux on Android has ${PREFIX} set which already ends with '/usr'
|
||||
if [[ -n "${ANDROID_ROOT}" && -n "${PREFIX}" ]]; then
|
||||
INSTALL_PREFIX="${PREFIX}/bin"
|
||||
else
|
||||
INSTALL_PREFIX="/usr/local/bin"
|
||||
fi
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# FUNCTIONS
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: print_banner
|
||||
# DESCRIPTION: Prints a banner
|
||||
# PARAMETERS: none
|
||||
# RETURNS: 0
|
||||
#-------------------------------------------------------------------------------
|
||||
print_banner() {
|
||||
cat <<-'EOF'
|
||||
=================================================
|
||||
____
|
||||
/ ___|_ __ ___ ___
|
||||
| | | '__/ _ \ / __|
|
||||
| |___| | | (_) | (__
|
||||
\____|_| \___/ \___|
|
||||
|
||||
___ _ _ _
|
||||
|_ _|_ __ ___| |_ __ _| | | ___ _ __
|
||||
| || '_ \/ __| __/ _` | | |/ _ \ '__|
|
||||
| || | | \__ \ || (_| | | | __/ |
|
||||
|___|_| |_|___/\__\__,_|_|_|\___|_|
|
||||
==================================================
|
||||
EOF
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: print_help
|
||||
# DESCRIPTION: Prints out a help message
|
||||
# PARAMETERS: none
|
||||
# RETURNS: 0
|
||||
#-------------------------------------------------------------------------------
|
||||
print_help() {
|
||||
local help_header
|
||||
local help_message
|
||||
|
||||
help_header="croc Installer Script"
|
||||
help_message="Usage:
|
||||
-p INSTALL_PREFIX
|
||||
Prefix to install croc into. Directory must already exist.
|
||||
Default = /usr/local/bin ('\${PREFIX}/bin' on Termux for Android)
|
||||
|
||||
-h
|
||||
Prints this helpful message and exit."
|
||||
|
||||
echo "${help_header}"
|
||||
echo ""
|
||||
echo "${help_message}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: print_message
|
||||
# DESCRIPTION: Prints a message all fancy like
|
||||
# PARAMETERS: $1 = Message to print
|
||||
# $2 = Severity. info, ok, error, warn
|
||||
# RETURNS: Formatted Message to stdout
|
||||
#-------------------------------------------------------------------------------
|
||||
print_message() {
|
||||
local message
|
||||
local severity
|
||||
local red
|
||||
local green
|
||||
local yellow
|
||||
local nc
|
||||
|
||||
message="${1}"
|
||||
severity="${2}"
|
||||
red='\e[0;31m'
|
||||
green='\e[0;32m'
|
||||
yellow='\e[1;33m'
|
||||
nc='\e[0m'
|
||||
|
||||
case "${severity}" in
|
||||
"info" ) echo -e "${nc}${message}${nc}";;
|
||||
"ok" ) echo -e "${green}${message}${nc}";;
|
||||
"error" ) echo -e "${red}${message}${nc}";;
|
||||
"warn" ) echo -e "${yellow}${message}${nc}";;
|
||||
esac
|
||||
|
||||
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: make_tempdir
|
||||
# DESCRIPTION: Makes a temp dir using mktemp if available
|
||||
# PARAMETERS: $1 = Directory template
|
||||
# RETURNS: 0 = Created temp dir. Also prints temp file path to stdout
|
||||
# 1 = Failed to create temp dir
|
||||
# 20 = Failed to find mktemp
|
||||
#-------------------------------------------------------------------------------
|
||||
make_tempdir() {
|
||||
local template
|
||||
local tempdir
|
||||
local tempdir_rcode
|
||||
|
||||
template="${1}.XXXXXX"
|
||||
|
||||
if command -v mktemp >/dev/null 2>&1; then
|
||||
tempdir="$(mktemp -d -t "${template}")"
|
||||
tempdir_rcode="${?}"
|
||||
if [[ "${tempdir_rcode}" == "0" ]]; then
|
||||
echo "${tempdir}"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
return 20
|
||||
fi
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: determine_os
|
||||
# DESCRIPTION: Attempts to determine host os using uname
|
||||
# PARAMETERS: none
|
||||
# RETURNS: 0 = OS Detected. Also prints detected os to stdout
|
||||
# 1 = Unknown OS
|
||||
# 20 = 'uname' not found in path
|
||||
#-------------------------------------------------------------------------------
|
||||
determine_os() {
|
||||
local uname_out
|
||||
|
||||
if command -v uname >/dev/null 2>&1; then
|
||||
uname_out="$(uname)"
|
||||
if [[ "${uname_out}" == "" ]]; then
|
||||
return 1
|
||||
else
|
||||
echo "${uname_out}"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
return 20
|
||||
fi
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: determine_arch
|
||||
# DESCRIPTION: Attempt to determine architecture of host
|
||||
# PARAMETERS: none
|
||||
# RETURNS: 0 = Arch Detected. Also prints detected arch to stdout
|
||||
# 1 = Unknown arch
|
||||
# 20 = 'uname' not found in path
|
||||
#-------------------------------------------------------------------------------
|
||||
determine_arch() {
|
||||
local uname_out
|
||||
|
||||
if command -v uname >/dev/null 2>&1; then
|
||||
uname_out="$(uname -m)"
|
||||
if [[ "${uname_out}" == "" ]]; then
|
||||
return 1
|
||||
else
|
||||
echo "${uname_out}"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
return 20
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: download_file
|
||||
# DESCRIPTION: Downloads a file into the specified directory. Attempts to
|
||||
# use curl, then wget. If neither is found, fail.
|
||||
# PARAMETERS: $1 = url of file to download
|
||||
# $2 = location to download file into on host system
|
||||
# RETURNS: If curl or wget found, returns the return code of curl or wget
|
||||
# 20 = Could not find curl and wget
|
||||
#-------------------------------------------------------------------------------
|
||||
download_file() {
|
||||
local url
|
||||
local dir
|
||||
local filename
|
||||
local rcode
|
||||
|
||||
url="${1}"
|
||||
dir="${2}"
|
||||
filename="${3}"
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL "${url}" -o "${dir}/${filename}"
|
||||
rcode="${?}"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget --quiet "${url}" -O "${dir}/${filename}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="20"
|
||||
fi
|
||||
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: checksum_check
|
||||
# DESCRIPTION: Attempt to verify checksum of downloaded file to ensure
|
||||
# integrity. Tries multiple tools before failing.
|
||||
# PARAMETERS: $1 = path to checksum file
|
||||
# $2 = location of file to check
|
||||
# $3 = working directory
|
||||
# RETURNS: 0 = checkusm verified
|
||||
# 1 = checksum verification failed
|
||||
# 20 = failed to determine tool to use to check checksum
|
||||
# 30 = failed to change into or go back from working dir
|
||||
#-------------------------------------------------------------------------------
|
||||
checksum_check() {
|
||||
local checksum_file
|
||||
local file
|
||||
local dir
|
||||
local rcode
|
||||
local shasum_1
|
||||
local shasum_2
|
||||
local shasum_c
|
||||
|
||||
checksum_file="${1}"
|
||||
file="${2}"
|
||||
dir="${3}"
|
||||
|
||||
cd "${dir}" || return 30
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
## Not all sha256sum versions seem to have --ignore-missing, so filter the checksum file
|
||||
## to only include the file we downloaded.
|
||||
grep "$(basename "${file}")" "${checksum_file}" > filtered_checksum.txt
|
||||
shasum_c="$(sha256sum -c "filtered_checksum.txt")"
|
||||
rcode="${?}"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
## With shasum on FreeBSD, we don't get to --ignore-missing, so filter the checksum file
|
||||
## to only include the file we downloaded.
|
||||
grep "$(basename "${file}")" "${checksum_file}" > filtered_checksum.txt
|
||||
shasum_c="$(shasum -a 256 -c "filtered_checksum.txt")"
|
||||
rcode="${?}"
|
||||
elif command -v sha256 >/dev/null 2>&1; then
|
||||
## With sha256 on FreeBSD, we don't get to --ignore-missing, so filter the checksum file
|
||||
## to only include the file we downloaded.
|
||||
## Also sha256 -c option seems to fail, so fall back to an if statement
|
||||
grep "$(basename "${file}")" "${checksum_file}" > filtered_checksum.txt
|
||||
shasum_1="$(sha256 -q "${file}")"
|
||||
shasum_2="$(awk '{print $1}' filtered_checksum.txt)"
|
||||
if [[ "${shasum_1}" == "${shasum_2}" ]]; then
|
||||
rcode="0"
|
||||
else
|
||||
rcode="1"
|
||||
fi
|
||||
shasum_c="Expected: ${shasum_1}, Got: ${shasum_2}"
|
||||
else
|
||||
return 20
|
||||
fi
|
||||
cd - >/dev/null 2>&1 || return 30
|
||||
|
||||
if [[ "${rcode}" -gt "0" ]]; then
|
||||
echo "${shasum_c}"
|
||||
fi
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: extract_file
|
||||
# DESCRIPTION: Extracts a file into a location. Attempts to determine which
|
||||
# tool to use by checking file extension.
|
||||
# PARAMETERS: $1 = file to extract
|
||||
# $2 = location to extract file into
|
||||
# $3 = extension
|
||||
# RETURNS: Return code of the tool used to extract the file
|
||||
# 20 = Failed to determine which tool to use
|
||||
# 30 = Failed to find tool in path
|
||||
#-------------------------------------------------------------------------------
|
||||
extract_file() {
|
||||
local file
|
||||
local dir
|
||||
local ext
|
||||
local rcode
|
||||
|
||||
file="${1}"
|
||||
dir="${2}"
|
||||
ext="${3}"
|
||||
|
||||
case "${ext}" in
|
||||
"zip" ) if command -v unzip >/dev/null 2>&1; then
|
||||
unzip "${file}" -d "${dir}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="30"
|
||||
fi
|
||||
;;
|
||||
"tar.gz" ) if command -v tar >/dev/null 2>&1; then
|
||||
tar -xf "${file}" -C "${dir}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="31"
|
||||
fi
|
||||
;;
|
||||
* ) rcode="20";;
|
||||
esac
|
||||
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: create_prefix
|
||||
# DESCRIPTION: Creates the install prefix (and any parent directories). If
|
||||
# EUID not 0, then attempt to use sudo.
|
||||
# PARAMETERS: $1 = prefix
|
||||
# RETURNS: Return code of the tool used to make the directory
|
||||
# 0 = Created the directory
|
||||
# >0 = Failed to create directory
|
||||
# 20 = Could not find mkdir command
|
||||
# 21 = Could not find sudo command
|
||||
#-------------------------------------------------------------------------------
|
||||
create_prefix() {
|
||||
local prefix
|
||||
local rcode
|
||||
|
||||
prefix="${1}"
|
||||
|
||||
if command -v mkdir >/dev/null 2>&1; then
|
||||
if [[ "${EUID}" == "0" ]]; then
|
||||
mkdir -p "${prefix}"
|
||||
rcode="${?}"
|
||||
else
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo mkdir -p "${prefix}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="21"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
rcode="20"
|
||||
fi
|
||||
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: install_file_freebsd
|
||||
# DESCRIPTION: Installs a file into a location using 'install'. If EUID not
|
||||
# 0, then attempt to use sudo.
|
||||
# PARAMETERS: $1 = file to install
|
||||
# $2 = location to install file into
|
||||
# RETURNS: 0 = File Installed
|
||||
# 1 = File not installed
|
||||
# 20 = Could not find install command
|
||||
# 21 = Could not find sudo command
|
||||
#-------------------------------------------------------------------------------
|
||||
install_file_freebsd() {
|
||||
local file
|
||||
local prefix
|
||||
local rcode
|
||||
|
||||
file="${1}"
|
||||
prefix="${2}"
|
||||
|
||||
if command -v install >/dev/null 2>&1; then
|
||||
if [[ "${EUID}" == "0" ]]; then
|
||||
install -C -b -B '_old' -m 755 "${file}" "${prefix}"
|
||||
rcode="${?}"
|
||||
else
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo install -C -b -B '_old' -m 755 "${file}" "${prefix}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="21"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
rcode="20"
|
||||
fi
|
||||
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: install_file_linux
|
||||
# DESCRIPTION: Installs a file into a location using 'install'. If EUID not
|
||||
# 0, then attempt to use sudo (unless on android).
|
||||
# PARAMETERS: $1 = file to install
|
||||
# $2 = location to install file into
|
||||
# RETURNS: 0 = File Installed
|
||||
# 1 = File not installed
|
||||
# 20 = Could not find install command
|
||||
# 21 = Could not find sudo command
|
||||
#-------------------------------------------------------------------------------
|
||||
install_file_linux() {
|
||||
local file
|
||||
local prefix
|
||||
local rcode
|
||||
|
||||
file="${1}"
|
||||
prefix="${2}"
|
||||
|
||||
if command -v install >/dev/null 2>&1; then
|
||||
if [[ "${EUID}" == "0" ]]; then
|
||||
install -C -b -S '_old' -m 755 -t "${prefix}" "${file}"
|
||||
rcode="${?}"
|
||||
else
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo install -C -b -S '_old' -m 755 "${file}" "${prefix}"
|
||||
rcode="${?}"
|
||||
elif [[ "${ANDROID_ROOT}" != "" ]]; then
|
||||
install -C -b -S '_old' -m 755 -t "${prefix}" "${file}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="21"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
rcode="20"
|
||||
fi
|
||||
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: install_file_cygwin
|
||||
# DESCRIPTION: Installs a file into a location using 'install'. If EUID not
|
||||
# 0, then attempt to use sudo.
|
||||
# Not really 100% sure this is how to install croc in cygwin.
|
||||
# PARAMETERS: $1 = file to install
|
||||
# $2 = location to install file into
|
||||
# RETURNS: 0 = File Installed
|
||||
# 20 = Could not find install command
|
||||
# 21 = Could not find sudo command
|
||||
#-------------------------------------------------------------------------------
|
||||
install_file_cygwin() {
|
||||
local file
|
||||
local prefix
|
||||
local rcode
|
||||
|
||||
file="${1}"
|
||||
prefix="${2}"
|
||||
|
||||
if command -v install >/dev/null 2>&1; then
|
||||
if [[ "${EUID}" == "0" ]]; then
|
||||
install -m 755 "${prefix}" "${file}"
|
||||
rcode="${?}"
|
||||
else
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo install -m 755 "${file}" "${prefix}"
|
||||
rcode="${?}"
|
||||
else
|
||||
rcode="21"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
rcode="20"
|
||||
fi
|
||||
|
||||
return "${rcode}"
|
||||
}
|
||||
|
||||
#--- FUNCTION ----------------------------------------------------------------
|
||||
# NAME: main
|
||||
# DESCRIPTION: Put it all together in a logical way
|
||||
# ...at least that is the hope...
|
||||
# PARAMETERS: 1 = prefix
|
||||
# RETURNS: 0 = All good
|
||||
# 1 = Something done broke
|
||||
#-------------------------------------------------------------------------------
|
||||
main() {
|
||||
local prefix
|
||||
local tmpdir
|
||||
local tmpdir_rcode
|
||||
local croc_arch
|
||||
local croc_arch_rcode
|
||||
local croc_os
|
||||
local croc_os_rcode
|
||||
local croc_base_url
|
||||
local croc_url
|
||||
local croc_file
|
||||
local croc_checksum_file
|
||||
local croc_bin_name
|
||||
local croc_version
|
||||
local croc_dl_ext
|
||||
local download_file_rcode
|
||||
local download_checksum_file_rcode
|
||||
local checksum_check_rcode
|
||||
local extract_file_rcode
|
||||
local install_file_rcode
|
||||
local create_prefix_rcode
|
||||
local bash_autocomplete_file
|
||||
local bash_autocomplete_prefix
|
||||
local zsh_autocomplete_file
|
||||
local zsh_autocomplete_prefix
|
||||
local autocomplete_install_rcode
|
||||
|
||||
croc_bin_name="croc"
|
||||
croc_version="10.0.7"
|
||||
croc_dl_ext="tar.gz"
|
||||
croc_base_url="https://github.com/schollz/croc/releases/download"
|
||||
prefix="${1}"
|
||||
bash_autocomplete_file="bash_autocomplete"
|
||||
bash_autocomplete_prefix="/etc/bash_completion.d"
|
||||
zsh_autocomplete_file="zsh_autocomplete"
|
||||
zsh_autocomplete_prefix="/etc/zsh"
|
||||
|
||||
print_banner
|
||||
print_message "== Install prefix set to ${prefix}" "info"
|
||||
|
||||
tmpdir="$(make_tempdir "${croc_bin_name}")"
|
||||
tmpdir_rcode="${?}"
|
||||
if [[ "${tmpdir_rcode}" == "0" ]]; then
|
||||
print_message "== Created temp dir at ${tmpdir}" "info"
|
||||
elif [[ "${tmpdir_rcode}" == "1" ]]; then
|
||||
print_message "== Failed to create temp dir at ${tmpdir}" "error"
|
||||
else
|
||||
print_message "== 'mktemp' not found in path. Is it installed?" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
croc_arch="$(determine_arch)"
|
||||
croc_arch_rcode="${?}"
|
||||
if [[ "${croc_arch_rcode}" == "0" ]]; then
|
||||
print_message "== Architecture detected as ${croc_arch}" "info"
|
||||
elif [[ "${croc_arch_rcode}" == "1" ]]; then
|
||||
print_message "== Architecture not detected" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== 'uname' not found in path. Is it installed?" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
croc_os="$(determine_os)"
|
||||
croc_os_rcode="${?}"
|
||||
if [[ "${croc_os_rcode}" == "0" ]]; then
|
||||
print_message "== OS detected as ${croc_os}" "info"
|
||||
elif [[ "${croc_os_rcode}" == "1" ]]; then
|
||||
print_message "== OS not detected" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== 'uname' not found in path. Is it installed?" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${croc_os}" in
|
||||
"Darwin" ) croc_os="macOS";;
|
||||
"CYGWIN"* ) croc_os="Windows";
|
||||
croc_dl_ext="zip";
|
||||
print_message "== Cygwin is currently unsupported." "error";
|
||||
exit 1;;
|
||||
esac
|
||||
|
||||
case "${croc_arch}" in
|
||||
"x86_64" ) croc_arch="64bit";;
|
||||
"amd64" ) croc_arch="64bit";;
|
||||
"aarch64" ) croc_arch="ARM64";;
|
||||
"arm64" ) croc_arch="ARM64";;
|
||||
"armv7l" ) croc_arch="ARM";;
|
||||
"i686" ) croc_arch="32bit";;
|
||||
* ) croc_arch="unknown";;
|
||||
esac
|
||||
|
||||
croc_file="${croc_bin_name}_v${croc_version}_${croc_os}-${croc_arch}.${croc_dl_ext}"
|
||||
croc_checksum_file="${croc_bin_name}_v${croc_version}_checksums.txt"
|
||||
croc_url="${croc_base_url}/v${croc_version}/${croc_file}"
|
||||
croc_checksum_url="${croc_base_url}/v${croc_version}/${croc_checksum_file}"
|
||||
echo "${croc_url}" "${tmpdir}" "${croc_file}"
|
||||
download_file "${croc_url}" "${tmpdir}" "${croc_file}"
|
||||
download_file_rcode="${?}"
|
||||
if [[ "${download_file_rcode}" == "0" ]]; then
|
||||
print_message "== Downloaded croc archive into ${tmpdir}" "info"
|
||||
elif [[ "${download_file_rcode}" == "1" ]]; then
|
||||
print_message "== Failed to download croc archive" "error"
|
||||
exit 1
|
||||
elif [[ "${download_file_rcode}" == "20" ]]; then
|
||||
print_message "== Failed to locate curl or wget" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== Return code of download tool returned an unexpected value of ${download_file_rcode}" "error"
|
||||
exit 1
|
||||
fi
|
||||
download_file "${croc_checksum_url}" "${tmpdir}" "${croc_checksum_file}"
|
||||
download_checksum_file_rcode="${?}"
|
||||
if [[ "${download_checksum_file_rcode}" == "0" ]]; then
|
||||
print_message "== Downloaded croc checksums file into ${tmpdir}" "info"
|
||||
elif [[ "${download_checksum_file_rcode}" == "1" ]]; then
|
||||
print_message "== Failed to download croc checksums" "error"
|
||||
exit 1
|
||||
elif [[ "${download_checksum_file_rcode}" == "20" ]]; then
|
||||
print_message "== Failed to locate curl or wget" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== Return code of download tool returned an unexpected value of ${download_checksum_file_rcode}" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
checksum_check "${tmpdir}/${croc_checksum_file}" "${tmpdir}/${croc_file}" "${tmpdir}"
|
||||
checksum_check_rcode="${?}"
|
||||
if [[ "${checksum_check_rcode}" == "0" ]]; then
|
||||
print_message "== Checksum of ${tmpdir}/${croc_file} verified" "ok"
|
||||
elif [[ "${checksum_check_rcode}" == "1" ]]; then
|
||||
print_message "== Failed to verify checksum of ${tmpdir}/${croc_file}" "error"
|
||||
exit 1
|
||||
elif [[ "${checksum_check_rcode}" == "20" ]]; then
|
||||
print_message "== Failed to find tool to verify sha256 sums" "error"
|
||||
exit 1
|
||||
elif [[ "${checksum_check_rcode}" == "30" ]]; then
|
||||
print_message "== Failed to change into working directory ${tmpdir}" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== Unknown return code returned while checking checksum of ${tmpdir}/${croc_file}. Returned ${checksum_check_rcode}" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
extract_file "${tmpdir}/${croc_file}" "${tmpdir}/" "${croc_dl_ext}"
|
||||
extract_file_rcode="${?}"
|
||||
if [[ "${extract_file_rcode}" == "0" ]]; then
|
||||
print_message "== Extracted ${croc_file} to ${tmpdir}/" "info"
|
||||
elif [[ "${extract_file_rcode}" == "1" ]]; then
|
||||
print_message "== Failed to extract ${croc_file}" "error"
|
||||
exit 1
|
||||
elif [[ "${extract_file_rcode}" == "20" ]]; then
|
||||
print_message "== Failed to determine which extraction tool to use" "error"
|
||||
exit 1
|
||||
elif [[ "${extract_file_rcode}" == "30" ]]; then
|
||||
print_message "== Failed to find 'unzip' in path" "error"
|
||||
exit 1
|
||||
elif [[ "${extract_file_rcode}" == "31" ]]; then
|
||||
print_message "== Failed to find 'tar' in path" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== Unknown error returned from extraction attempt" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "${prefix}" ]]; then
|
||||
create_prefix "${prefix}"
|
||||
create_prefix_rcode="${?}"
|
||||
if [[ "${create_prefix_rcode}" == "0" ]]; then
|
||||
print_message "== Created install prefix at ${prefix}" "info"
|
||||
elif [[ "${create_prefix_rcode}" == "20" ]]; then
|
||||
print_message "== Failed to find mkdir in path" "error"
|
||||
exit 1
|
||||
elif [[ "${create_prefix_rcode}" == "21" ]]; then
|
||||
print_message "== Failed to find sudo in path" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== Failed to create the install prefix: ${prefix}" "error"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_message "== Install prefix already exists. No need to create it." "info"
|
||||
fi
|
||||
|
||||
[ ! -d "/etc/bash_completion.d/croc" ] && mkdir -p "/etc/bash_completion.d/croc"
|
||||
case "${croc_os}" in
|
||||
"Linux" ) install_file_linux "${tmpdir}/${croc_bin_name}" "${prefix}/";
|
||||
install_file_rcode="${?}";;
|
||||
"FreeBSD" ) install_file_freebsd "${tmpdir}/${croc_bin_name}" "${prefix}/";
|
||||
install_file_rcode="${?}";;
|
||||
"macOS" ) install_file_freebsd "${tmpdir}/${croc_bin_name}" "${prefix}/";
|
||||
install_file_rcode="${?}";;
|
||||
"Windows" ) install_file_cygwin "${tmpdir}/${croc_bin_name}" "${prefix}/";
|
||||
install_file_rcode="${?}";;
|
||||
esac
|
||||
|
||||
if [[ "${install_file_rcode}" == "0" ]] ; then
|
||||
print_message "== Installed ${croc_bin_name} to ${prefix}/" "ok"
|
||||
elif [[ "${install_file_rcode}" == "1" ]]; then
|
||||
print_message "== Failed to install ${croc_bin_name}" "error"
|
||||
exit 1
|
||||
elif [[ "${install_file_rcode}" == "20" ]]; then
|
||||
print_message "== Failed to locate 'install' command" "error"
|
||||
exit 1
|
||||
elif [[ "${install_file_rcode}" == "21" ]]; then
|
||||
print_message "== Failed to locate 'sudo' command" "error"
|
||||
exit 1
|
||||
else
|
||||
print_message "== Install attempt returned an unexpected value of ${install_file_rcode}" "error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# case "$(basename ${SHELL})" in
|
||||
# "bash" ) install_file_linux "${tmpdir}/${bash_autocomplete_file}" "${bash_autocomplete_prefix}/croc";
|
||||
# autocomplete_install_rcode="${?}";;
|
||||
# "zsh" ) install_file_linux "${tmpdir}/${zsh_autocomplete_file}" "${zsh_autocomplete_prefix}/zsh_autocomplete_croc";
|
||||
# autocomplete_install_rcode="${?}";
|
||||
# print_message "== You will need to add the following to your ~/.zshrc to enable autocompletion" "info";
|
||||
# print_message "\nPROG=croc\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/zsh_autocomplete_croc\n" "info";;
|
||||
# *) autocomplete_install_rcode="1";;
|
||||
# esac
|
||||
|
||||
# if [[ "${autocomplete_install_rcode}" == "0" ]] ; then
|
||||
# print_message "== Installed autocompletions for $(basename "${SHELL}")" "ok"
|
||||
# elif [[ "${autocomplete_install_rcode}" == "1" ]]; then
|
||||
# print_message "== Failed to install ${bash_autocomplete_file}" "error"
|
||||
# elif [[ "${autocomplete_install_rcode}" == "20" ]]; then
|
||||
# print_message "== Failed to locate 'install' command" "error"
|
||||
# elif [[ "${autocomplete_install_rcode}" == "21" ]]; then
|
||||
# print_message "== Failed to locate 'sudo' command" "error"
|
||||
# else
|
||||
# print_message "== Install attempt returned an unexpected value of ${autocomplete_install_rcode}" "error"
|
||||
# fi
|
||||
|
||||
print_message "== Installation complete" "ok"
|
||||
|
||||
exit 0
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# ARGUMENT PARSING
|
||||
#-------------------------------------------------------------------------------
|
||||
OPTS="hp:"
|
||||
while getopts "${OPTS}" optchar; do
|
||||
case "${optchar}" in
|
||||
'h' ) print_help
|
||||
exit 0
|
||||
;;
|
||||
'p' ) INSTALL_PREFIX="${OPTARG}"
|
||||
;;
|
||||
/? ) print_message "Unknown option ${OPTARG}" "warn"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# CALL MAIN
|
||||
#-------------------------------------------------------------------------------
|
||||
main "${INSTALL_PREFIX}"
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
tmp=$(mktemp -d)
|
||||
echo $VERSION
|
||||
git clone -b v${VERSION} --depth 1 https://github.com/schollz/croc $tmp/croc-${VERSION}
|
||||
(cd $tmp/croc-${VERSION} && go mod tidy && go mod vendor)
|
||||
(cd $tmp && tar -cvzf croc_${VERSION}_src.tar.gz croc-${VERSION})
|
||||
mv $tmp/croc_${VERSION}_src.tar.gz dist/
|
|
@ -0,0 +1,77 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := run()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run() (err error) {
|
||||
versionNew := "v" + os.Getenv("VERSION")
|
||||
versionHash, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
versionHashNew := strings.TrimSpace(string(versionHash))
|
||||
fmt.Println(versionNew)
|
||||
fmt.Println(versionHashNew)
|
||||
|
||||
err = replaceInFile("src/cli/cli.go", `Version = "`, `"`, versionNew+"-"+versionHashNew)
|
||||
if err == nil {
|
||||
fmt.Printf("updated cli.go to version %s\n", versionNew)
|
||||
}
|
||||
err = replaceInFile("README.md", `version-`, `-b`, strings.Split(versionNew, "-")[0])
|
||||
if err == nil {
|
||||
fmt.Printf("updated README to version %s\n", strings.Split(versionNew, "-")[0])
|
||||
}
|
||||
|
||||
err = replaceInFile("src/install/default.txt", `croc_version="`, `"`, strings.Split(versionNew, "-")[0][1:])
|
||||
if err == nil {
|
||||
fmt.Printf("updated default.txt to version %s\n", strings.Split(versionNew, "-")[0][1:])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func replaceInFile(fname, start, end, replacement string) (err error) {
|
||||
b, err := os.ReadFile(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
oldVersion := GetStringInBetween(string(b), start, end)
|
||||
if oldVersion == "" {
|
||||
err = fmt.Errorf("nothing")
|
||||
return
|
||||
}
|
||||
newF := strings.Replace(
|
||||
string(b),
|
||||
fmt.Sprintf("%s%s%s", start, oldVersion, end),
|
||||
fmt.Sprintf("%s%s%s", start, replacement, end),
|
||||
1,
|
||||
)
|
||||
err = os.WriteFile(fname, []byte(newF), 0o644)
|
||||
return
|
||||
}
|
||||
|
||||
// GetStringInBetween Returns empty string if no start string found
|
||||
func GetStringInBetween(str string, start string, end string) (result string) {
|
||||
s := strings.Index(str, start)
|
||||
if s == -1 {
|
||||
return
|
||||
}
|
||||
s += len(start)
|
||||
e := strings.Index(str[s:], end)
|
||||
if e == -1 {
|
||||
return
|
||||
}
|
||||
e += s
|
||||
return str[s:e]
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
#!/bin/bash
|
||||
VERSION=$(cat ./src/cli/cli.go | grep 'Version = "v' | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
|
||||
echo $VERSION
|
||||
|
||||
# Check dependencies.
|
||||
set -e
|
||||
xargs=$(which gxargs || which xargs)
|
||||
|
||||
# Validate settings.
|
||||
[ "$TRACE" ] && set -x
|
||||
|
||||
CONFIG=$@
|
||||
|
||||
for line in $CONFIG; do
|
||||
eval "$line"
|
||||
done
|
||||
|
||||
owner="schollz"
|
||||
repo="croc"
|
||||
tag="v${VERSION}"
|
||||
filename="dist/croc_${VERSION}_src.tar.gz"
|
||||
|
||||
# Define variables.
|
||||
GH_API="https://api.github.com"
|
||||
GH_REPO="$GH_API/repos/$owner/$repo"
|
||||
GH_TAGS="$GH_REPO/releases/tags/$tag"
|
||||
AUTH="Authorization: token $GITHUB_TOKEN"
|
||||
WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie"
|
||||
CURL_ARGS="-LJO#"
|
||||
|
||||
if [[ "$tag" == 'LATEST' ]]; then
|
||||
GH_TAGS="$GH_REPO/releases/latest"
|
||||
fi
|
||||
|
||||
# Validate token.
|
||||
curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; }
|
||||
|
||||
# Read asset tags.
|
||||
response=$(curl -sH "$AUTH" $GH_TAGS)
|
||||
|
||||
# Get ID of the asset based on given filename.
|
||||
eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=')
|
||||
[ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; }
|
||||
|
||||
# Upload asset
|
||||
echo "Uploading asset... "
|
||||
|
||||
# Construct url
|
||||
GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)"
|
||||
|
||||
curl "$GITHUB_OAUTH_BASIC" --data-binary @"$filename" -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/octet-stream" $GH_ASSET
|
|
@ -0,0 +1,23 @@
|
|||
#compdef $PROG
|
||||
|
||||
_cli_zsh_autocomplete() {
|
||||
|
||||
local -a opts
|
||||
local cur
|
||||
cur=${words[-1]}
|
||||
if [[ "$cur" == "-"* ]]; then
|
||||
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
|
||||
else
|
||||
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}")
|
||||
fi
|
||||
|
||||
if [[ "${opts[1]}" != "" ]]; then
|
||||
_describe 'values' opts
|
||||
else
|
||||
_files
|
||||
fi
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
compdef _cli_zsh_autocomplete $PROG
|
|
@ -0,0 +1,84 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/schollz/croc/v10/src/comm"
|
||||
"github.com/schollz/croc/v10/src/compress"
|
||||
"github.com/schollz/croc/v10/src/crypt"
|
||||
log "github.com/schollz/logger"
|
||||
)
|
||||
|
||||
// Type is a message type
|
||||
type Type string
|
||||
|
||||
const (
|
||||
TypePAKE Type = "pake"
|
||||
TypeExternalIP Type = "externalip"
|
||||
TypeFinished Type = "finished"
|
||||
TypeError Type = "error"
|
||||
TypeCloseRecipient Type = "close-recipient"
|
||||
TypeCloseSender Type = "close-sender"
|
||||
TypeRecipientReady Type = "recipientready"
|
||||
TypeFileInfo Type = "fileinfo"
|
||||
)
|
||||
|
||||
// Message is the possible payload for messaging
|
||||
type Message struct {
|
||||
Type Type `json:"t,omitempty"`
|
||||
Message string `json:"m,omitempty"`
|
||||
Bytes []byte `json:"b,omitempty"`
|
||||
Bytes2 []byte `json:"b2,omitempty"`
|
||||
Num int `json:"n,omitempty"`
|
||||
}
|
||||
|
||||
func (m Message) String() string {
|
||||
b, _ := json.Marshal(m)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Send will send out
|
||||
func Send(c *comm.Comm, key []byte, m Message) (err error) {
|
||||
mSend, err := Encode(key, m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Send(mSend)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode will convert to bytes
|
||||
func Encode(key []byte, m Message) (b []byte, err error) {
|
||||
b, err = json.Marshal(m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
b = compress.Compress(b)
|
||||
if key != nil {
|
||||
log.Debugf("writing %s message (encrypted)", m.Type)
|
||||
b, err = crypt.Encrypt(b, key)
|
||||
} else {
|
||||
log.Debugf("writing %s message (unencrypted)", m.Type)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Decode will convert from bytes
|
||||
func Decode(key []byte, b []byte) (m Message, err error) {
|
||||
if key != nil {
|
||||
b, err = crypt.Decrypt(b, key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
b = compress.Decompress(b)
|
||||
err = json.Unmarshal(b, &m)
|
||||
if err == nil {
|
||||
if key != nil {
|
||||
log.Debugf("read %s message (encrypted)", m.Type)
|
||||
} else {
|
||||
log.Debugf("read %s message (unencrypted)", m.Type)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/schollz/croc/v10/src/comm"
|
||||
"github.com/schollz/croc/v10/src/crypt"
|
||||
log "github.com/schollz/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var TypeMessage Type = "message"
|
||||
|
||||
func TestMessage(t *testing.T) {
|
||||
log.SetLevel("debug")
|
||||
m := Message{Type: TypeMessage, Message: "hello, world"}
|
||||
e, salt, err := crypt.New([]byte("pass"), nil)
|
||||
assert.Nil(t, err)
|
||||
fmt.Println(string(salt))
|
||||
b, err := Encode(e, m)
|
||||
assert.Nil(t, err)
|
||||
fmt.Printf("%x\n", b)
|
||||
|
||||
m2, err := Decode(e, b)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, m, m2)
|
||||
assert.Equal(t, `{"t":"message","m":"hello, world"}`, m.String())
|
||||
_, err = Decode([]byte("not pass"), b)
|
||||
assert.NotNil(t, err)
|
||||
_, err = Encode([]byte("0"), m)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestMessageNoPass(t *testing.T) {
|
||||
log.SetLevel("debug")
|
||||
m := Message{Type: TypeMessage, Message: "hello, world"}
|
||||
b, err := Encode(nil, m)
|
||||
assert.Nil(t, err)
|
||||
fmt.Printf("%x\n", b)
|
||||
|
||||
m2, err := Decode(nil, b)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, m, m2)
|
||||
assert.Equal(t, `{"t":"message","m":"hello, world"}`, m.String())
|
||||
}
|
||||
|
||||
func TestSend(t *testing.T) {
|
||||
token := make([]byte, 40000000)
|
||||
rand.Read(token)
|
||||
|
||||
port := "8801"
|
||||
go func() {
|
||||
log.Debugf("starting TCP server on " + port)
|
||||
server, err := net.Listen("tcp", "0.0.0.0:"+port)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
defer server.Close()
|
||||
// spawn a new goroutine whenever a client connects
|
||||
for {
|
||||
connection, err := server.Accept()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
log.Debugf("client %s connected", connection.RemoteAddr().String())
|
||||
go func(_ string, connection net.Conn) {
|
||||
c := comm.New(connection)
|
||||
err = c.Send([]byte("hello, world"))
|
||||
assert.Nil(t, err)
|
||||
data, err := c.Receive()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("hello, computer"), data)
|
||||
data, err = c.Receive()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte{'\x00'}, data)
|
||||
data, err = c.Receive()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, token, data)
|
||||
}(port, connection)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
a, err := comm.NewConnection("127.0.0.1:"+port, 10*time.Minute)
|
||||
assert.Nil(t, err)
|
||||
m := Message{Type: TypeMessage, Message: "hello, world"}
|
||||
e, salt, err := crypt.New([]byte("pass"), nil)
|
||||
log.Debug(salt)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Nil(t, Send(a, e, m))
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/schollz/croc/v10/src/utils"
|
||||
)
|
||||
|
||||
// TCP_BUFFER_SIZE is the maximum packet size
|
||||
const TCP_BUFFER_SIZE = 1024 * 64
|
||||
|
||||
// DEFAULT_RELAY is the default relay used (can be set using --relay)
|
||||
var (
|
||||
DEFAULT_RELAY = "croc.schollz.com"
|
||||
DEFAULT_RELAY6 = "croc6.schollz.com"
|
||||
DEFAULT_PORT = "9009"
|
||||
DEFAULT_PASSPHRASE = "pass123"
|
||||
INTERNAL_DNS = false
|
||||
)
|
||||
|
||||
// publicDNS are servers to be queried if a local lookup fails
|
||||
var publicDNS = []string{
|
||||
"1.0.0.1", // Cloudflare
|
||||
"1.1.1.1", // Cloudflare
|
||||
"[2606:4700:4700::1111]", // Cloudflare
|
||||
"[2606:4700:4700::1001]", // Cloudflare
|
||||
"8.8.4.4", // Google
|
||||
"8.8.8.8", // Google
|
||||
"[2001:4860:4860::8844]", // Google
|
||||
"[2001:4860:4860::8888]", // Google
|
||||
"9.9.9.9", // Quad9
|
||||
"149.112.112.112", // Quad9
|
||||
"[2620:fe::fe]", // Quad9
|
||||
"[2620:fe::fe:9]", // Quad9
|
||||
"8.26.56.26", // Comodo
|
||||
"8.20.247.20", // Comodo
|
||||
"208.67.220.220", // Cisco OpenDNS
|
||||
"208.67.222.222", // Cisco OpenDNS
|
||||
"[2620:119:35::35]", // Cisco OpenDNS
|
||||
"[2620:119:53::53]", // Cisco OpenDNS
|
||||
}
|
||||
|
||||
func getConfigFile(requireValidPath bool) (fname string, err error) {
|
||||
configFile, err := utils.GetConfigDir(requireValidPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fname = path.Join(configFile, "internal-dns")
|
||||
return
|
||||
}
|
||||
|
||||
func init() {
|
||||
doRemember := false
|
||||
for _, flag := range os.Args {
|
||||
if flag == "--internal-dns" {
|
||||
INTERNAL_DNS = true
|
||||
break
|
||||
}
|
||||
if flag == "--remember" {
|
||||
doRemember = true
|
||||
}
|
||||
}
|
||||
if doRemember {
|
||||
// save in config file
|
||||
fname, err := getConfigFile(true)
|
||||
if err == nil {
|
||||
f, _ := os.Create(fname)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
if !INTERNAL_DNS {
|
||||
fname, err := getConfigFile(false)
|
||||
if err == nil {
|
||||
INTERNAL_DNS = utils.Exists(fname)
|
||||
}
|
||||
}
|
||||
var err error
|
||||
var addr string
|
||||
addr, err = lookup(DEFAULT_RELAY)
|
||||
if err == nil {
|
||||
DEFAULT_RELAY = net.JoinHostPort(addr, DEFAULT_PORT)
|
||||
} else {
|
||||
DEFAULT_RELAY = ""
|
||||
}
|
||||
addr, err = lookup(DEFAULT_RELAY6)
|
||||
if err == nil {
|
||||
DEFAULT_RELAY6 = net.JoinHostPort(addr, DEFAULT_PORT)
|
||||
} else {
|
||||
DEFAULT_RELAY6 = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve a hostname to an IP address using DNS.
|
||||
func lookup(address string) (ipaddress string, err error) {
|
||||
if !INTERNAL_DNS {
|
||||
return localLookupIP(address)
|
||||
}
|
||||
type Result struct {
|
||||
s string
|
||||
err error
|
||||
}
|
||||
result := make(chan Result, len(publicDNS))
|
||||
for _, dns := range publicDNS {
|
||||
go func(dns string) {
|
||||
var r Result
|
||||
r.s, r.err = remoteLookupIP(address, dns)
|
||||
result <- r
|
||||
}(dns)
|
||||
}
|
||||
for i := 0; i < len(publicDNS); i++ {
|
||||
ipaddress = (<-result).s
|
||||
if ipaddress != "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("failed to resolve %s: all DNS servers exhausted", address)
|
||||
return
|
||||
}
|
||||
|
||||
// localLookupIP returns a host's IP address based on the local resolver.
|
||||
func localLookupIP(address string) (ipaddress string, err error) {
|
||||
ip, err := net.LookupHost(address)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ipaddress = ip[0]
|
||||
return
|
||||
}
|
||||
|
||||
// remoteLookupIP returns a host's IP address based on a remote DNS server.
|
||||
func remoteLookupIP(address, dns string) (ipaddress string, err error) {
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
d := new(net.Dialer)
|
||||
return d.DialContext(ctx, network, dns+":53")
|
||||
},
|
||||
}
|
||||
ip, err := r.LookupHost(context.Background(), address)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ipaddress = ip[0]
|
||||
return
|
||||
}
|
|
@ -0,0 +1,548 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/schollz/logger"
|
||||
"github.com/schollz/pake/v3"
|
||||
|
||||
"github.com/schollz/croc/v10/src/comm"
|
||||
"github.com/schollz/croc/v10/src/crypt"
|
||||
"github.com/schollz/croc/v10/src/models"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
host string
|
||||
port string
|
||||
debugLevel string
|
||||
banner string
|
||||
password string
|
||||
rooms roomMap
|
||||
}
|
||||
|
||||
type roomInfo struct {
|
||||
first *comm.Comm
|
||||
second *comm.Comm
|
||||
opened time.Time
|
||||
full bool
|
||||
}
|
||||
|
||||
type roomMap struct {
|
||||
rooms map[string]roomInfo
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
const pingRoom = "pinglkasjdlfjsaldjf"
|
||||
|
||||
var timeToRoomDeletion = 10 * time.Minute
|
||||
|
||||
// Run starts a tcp listener, run async
|
||||
func Run(debugLevel, host, port, password string, banner ...string) (err error) {
|
||||
s := new(server)
|
||||
s.host = host
|
||||
s.port = port
|
||||
s.password = password
|
||||
s.debugLevel = debugLevel
|
||||
if len(banner) > 0 {
|
||||
s.banner = banner[0]
|
||||
}
|
||||
return s.start()
|
||||
}
|
||||
|
||||
func (s *server) start() (err error) {
|
||||
log.SetLevel(s.debugLevel)
|
||||
log.Debugf("starting with password '%s'", s.password)
|
||||
s.rooms.Lock()
|
||||
s.rooms.rooms = make(map[string]roomInfo)
|
||||
s.rooms.Unlock()
|
||||
|
||||
// delete old rooms
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(timeToRoomDeletion)
|
||||
var roomsToDelete []string
|
||||
s.rooms.Lock()
|
||||
for room := range s.rooms.rooms {
|
||||
if time.Since(s.rooms.rooms[room].opened) > 3*time.Hour {
|
||||
roomsToDelete = append(roomsToDelete, room)
|
||||
}
|
||||
}
|
||||
s.rooms.Unlock()
|
||||
|
||||
for _, room := range roomsToDelete {
|
||||
s.deleteRoom(room)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = s.run()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *server) run() (err error) {
|
||||
network := "tcp"
|
||||
addr := net.JoinHostPort(s.host, s.port)
|
||||
if s.host != "" {
|
||||
ip := net.ParseIP(s.host)
|
||||
if ip == nil {
|
||||
var tcpIP *net.IPAddr
|
||||
tcpIP, err = net.ResolveIPAddr("ip", s.host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ip = tcpIP.IP
|
||||
}
|
||||
addr = net.JoinHostPort(ip.String(), s.port)
|
||||
if s.host != "" {
|
||||
if ip.To4() != nil {
|
||||
network = "tcp4"
|
||||
} else {
|
||||
network = "tcp6"
|
||||
}
|
||||
}
|
||||
}
|
||||
addr = strings.Replace(addr, "127.0.0.1", "0.0.0.0", 1)
|
||||
log.Infof("starting TCP server on " + addr)
|
||||
server, err := net.Listen(network, addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listening on %s: %w", addr, err)
|
||||
}
|
||||
defer server.Close()
|
||||
// spawn a new goroutine whenever a client connects
|
||||
for {
|
||||
connection, err := server.Accept()
|
||||
if err != nil {
|
||||
return fmt.Errorf("problem accepting connection: %w", err)
|
||||
}
|
||||
log.Debugf("client %s connected", connection.RemoteAddr().String())
|
||||
go func(port string, connection net.Conn) {
|
||||
c := comm.New(connection)
|
||||
room, errCommunication := s.clientCommunication(port, c)
|
||||
log.Debugf("room: %+v", room)
|
||||
log.Debugf("err: %+v", errCommunication)
|
||||
if errCommunication != nil {
|
||||
log.Debugf("relay-%s: %s", connection.RemoteAddr().String(), errCommunication.Error())
|
||||
connection.Close()
|
||||
return
|
||||
}
|
||||
if room == pingRoom {
|
||||
log.Debugf("got ping")
|
||||
connection.Close()
|
||||
return
|
||||
}
|
||||
for {
|
||||
// check connection
|
||||
log.Debugf("checking connection of room %s for %+v", room, c)
|
||||
deleteIt := false
|
||||
s.rooms.Lock()
|
||||
if _, ok := s.rooms.rooms[room]; !ok {
|
||||
log.Debug("room is gone")
|
||||
s.rooms.Unlock()
|
||||
return
|
||||
}
|
||||
log.Debugf("room: %+v", s.rooms.rooms[room])
|
||||
if s.rooms.rooms[room].first != nil && s.rooms.rooms[room].second != nil {
|
||||
log.Debug("rooms ready")
|
||||
s.rooms.Unlock()
|
||||
break
|
||||
} else {
|
||||
if s.rooms.rooms[room].first != nil {
|
||||
errSend := s.rooms.rooms[room].first.Send([]byte{1})
|
||||
if errSend != nil {
|
||||
log.Debug(errSend)
|
||||
deleteIt = true
|
||||
}
|
||||
}
|
||||
}
|
||||
s.rooms.Unlock()
|
||||
if deleteIt {
|
||||
s.deleteRoom(room)
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}(s.port, connection)
|
||||
}
|
||||
}
|
||||
|
||||
var weakKey = []byte{1, 2, 3}
|
||||
|
||||
func (s *server) clientCommunication(port string, c *comm.Comm) (room string, err error) {
|
||||
// establish secure password with PAKE for communication with relay
|
||||
B, err := pake.InitCurve(weakKey, 1, "siec")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
Abytes, err := c.Receive()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Debugf("Abytes: %s", Abytes)
|
||||
if bytes.Equal(Abytes, []byte("ping")) {
|
||||
room = pingRoom
|
||||
log.Debug("sending back pong")
|
||||
c.Send([]byte("pong"))
|
||||
return
|
||||
}
|
||||
err = B.Update(Abytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Send(B.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
strongKey, err := B.SessionKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Debugf("strongkey: %x", strongKey)
|
||||
|
||||
// receive salt
|
||||
salt, err := c.Receive()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
strongKeyForEncryption, _, err := crypt.New(strongKey, salt)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("waiting for password")
|
||||
passwordBytesEnc, err := c.Receive()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
passwordBytes, err := crypt.Decrypt(passwordBytesEnc, strongKeyForEncryption)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(string(passwordBytes)) != s.password {
|
||||
err = fmt.Errorf("bad password")
|
||||
enc, _ := crypt.Encrypt([]byte(err.Error()), strongKeyForEncryption)
|
||||
if err = c.Send(enc); err != nil {
|
||||
return "", fmt.Errorf("send error: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// send ok to tell client they are connected
|
||||
banner := s.banner
|
||||
if len(banner) == 0 {
|
||||
banner = "ok"
|
||||
}
|
||||
log.Debugf("sending '%s'", banner)
|
||||
bSend, err := crypt.Encrypt([]byte(banner+"|||"+c.Connection().RemoteAddr().String()), strongKeyForEncryption)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Send(bSend)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// wait for client to tell me which room they want
|
||||
log.Debug("waiting for answer")
|
||||
enc, err := c.Receive()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
roomBytes, err := crypt.Decrypt(enc, strongKeyForEncryption)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
room = string(roomBytes)
|
||||
|
||||
s.rooms.Lock()
|
||||
// create the room if it is new
|
||||
if _, ok := s.rooms.rooms[room]; !ok {
|
||||
s.rooms.rooms[room] = roomInfo{
|
||||
first: c,
|
||||
opened: time.Now(),
|
||||
}
|
||||
s.rooms.Unlock()
|
||||
// tell the client that they got the room
|
||||
|
||||
bSend, err = crypt.Encrypt([]byte("ok"), strongKeyForEncryption)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Send(bSend)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
s.deleteRoom(room)
|
||||
return
|
||||
}
|
||||
log.Debugf("room %s has 1", room)
|
||||
return
|
||||
}
|
||||
if s.rooms.rooms[room].full {
|
||||
s.rooms.Unlock()
|
||||
bSend, err = crypt.Encrypt([]byte("room full"), strongKeyForEncryption)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Send(bSend)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Debugf("room %s has 2", room)
|
||||
s.rooms.rooms[room] = roomInfo{
|
||||
first: s.rooms.rooms[room].first,
|
||||
second: c,
|
||||
opened: s.rooms.rooms[room].opened,
|
||||
full: true,
|
||||
}
|
||||
otherConnection := s.rooms.rooms[room].first
|
||||
s.rooms.Unlock()
|
||||
|
||||
// second connection is the sender, time to staple connections
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
// start piping
|
||||
go func(com1, com2 *comm.Comm, wg *sync.WaitGroup) {
|
||||
log.Debug("starting pipes")
|
||||
pipe(com1.Connection(), com2.Connection())
|
||||
wg.Done()
|
||||
log.Debug("done piping")
|
||||
}(otherConnection, c, &wg)
|
||||
|
||||
// tell the sender everything is ready
|
||||
bSend, err = crypt.Encrypt([]byte("ok"), strongKeyForEncryption)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Send(bSend)
|
||||
if err != nil {
|
||||
s.deleteRoom(room)
|
||||
return
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// delete room
|
||||
s.deleteRoom(room)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *server) deleteRoom(room string) {
|
||||
s.rooms.Lock()
|
||||
defer s.rooms.Unlock()
|
||||
if _, ok := s.rooms.rooms[room]; !ok {
|
||||
return
|
||||
}
|
||||
log.Debugf("deleting room: %s", room)
|
||||
if s.rooms.rooms[room].first != nil {
|
||||
s.rooms.rooms[room].first.Close()
|
||||
}
|
||||
if s.rooms.rooms[room].second != nil {
|
||||
s.rooms.rooms[room].second.Close()
|
||||
}
|
||||
s.rooms.rooms[room] = roomInfo{first: nil, second: nil}
|
||||
delete(s.rooms.rooms, room)
|
||||
}
|
||||
|
||||
// chanFromConn creates a channel from a Conn object, and sends everything it
|
||||
//
|
||||
// Read()s from the socket to the channel.
|
||||
func chanFromConn(conn net.Conn) chan []byte {
|
||||
c := make(chan []byte, 1)
|
||||
if err := conn.SetReadDeadline(time.Now().Add(3 * time.Hour)); err != nil {
|
||||
log.Warnf("can't set read deadline: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
b := make([]byte, models.TCP_BUFFER_SIZE)
|
||||
for {
|
||||
n, err := conn.Read(b)
|
||||
if n > 0 {
|
||||
res := make([]byte, n)
|
||||
// Copy the buffer so it doesn't get changed while read by the recipient.
|
||||
copy(res, b[:n])
|
||||
c <- res
|
||||
}
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
c <- nil
|
||||
break
|
||||
}
|
||||
}
|
||||
log.Debug("exiting")
|
||||
}()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// pipe creates a full-duplex pipe between the two sockets and
|
||||
// transfers data from one to the other.
|
||||
func pipe(conn1 net.Conn, conn2 net.Conn) {
|
||||
chan1 := chanFromConn(conn1)
|
||||
chan2 := chanFromConn(conn2)
|
||||
|
||||
for {
|
||||
select {
|
||||
case b1 := <-chan1:
|
||||
if b1 == nil {
|
||||
return
|
||||
}
|
||||
if _, err := conn2.Write(b1); err != nil {
|
||||
log.Errorf("write error on channel 1: %v", err)
|
||||
}
|
||||
|
||||
case b2 := <-chan2:
|
||||
if b2 == nil {
|
||||
return
|
||||
}
|
||||
if _, err := conn1.Write(b2); err != nil {
|
||||
log.Errorf("write error on channel 2: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func PingServer(address string) (err error) {
|
||||
log.Debugf("pinging %s", address)
|
||||
c, err := comm.NewConnection(address, 300*time.Millisecond)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
err = c.Send([]byte("ping"))
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
b, err := c.Receive()
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
if bytes.Equal(b, []byte("pong")) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("no pong")
|
||||
}
|
||||
|
||||
// ConnectToTCPServer will initiate a new connection
|
||||
// to the specified address, room with optional time limit
|
||||
func ConnectToTCPServer(address, password, room string, timelimit ...time.Duration) (c *comm.Comm, banner string, ipaddr string, err error) {
|
||||
if len(timelimit) > 0 {
|
||||
c, err = comm.NewConnection(address, timelimit[0])
|
||||
} else {
|
||||
c, err = comm.NewConnection(address)
|
||||
}
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
|
||||
// get PAKE connection with server to establish strong key to transfer info
|
||||
A, err := pake.InitCurve(weakKey, 0, "siec")
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
err = c.Send(A.Bytes())
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
Bbytes, err := c.Receive()
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
err = A.Update(Bbytes)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
strongKey, err := A.SessionKey()
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
log.Debugf("strong key: %x", strongKey)
|
||||
|
||||
strongKeyForEncryption, salt, err := crypt.New(strongKey, nil)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
// send salt
|
||||
err = c.Send(salt)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("sending password")
|
||||
bSend, err := crypt.Encrypt([]byte(password), strongKeyForEncryption)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
err = c.Send(bSend)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
log.Debug("waiting for first ok")
|
||||
enc, err := c.Receive()
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
data, err := crypt.Decrypt(enc, strongKeyForEncryption)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(string(data), "|||") {
|
||||
err = fmt.Errorf("bad response: %s", string(data))
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
banner = strings.Split(string(data), "|||")[0]
|
||||
ipaddr = strings.Split(string(data), "|||")[1]
|
||||
log.Debugf("sending room; %s", room)
|
||||
bSend, err = crypt.Encrypt([]byte(room), strongKeyForEncryption)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
err = c.Send(bSend)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
log.Debug("waiting for room confirmation")
|
||||
enc, err = c.Receive()
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
data, err = crypt.Decrypt(enc, strongKeyForEncryption)
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(data, []byte("ok")) {
|
||||
err = fmt.Errorf("got bad response: %s", data)
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
log.Debug("all set")
|
||||
return
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
log "github.com/schollz/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func BenchmarkConnection(b *testing.B) {
|
||||
log.SetLevel("trace")
|
||||
go Run("debug", "127.0.0.1", "8283", "pass123", "8284")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
c, _, _, _ := ConnectToTCPServer("127.0.0.1:8283", "pass123", fmt.Sprintf("testroom%d", i), 1*time.Minute)
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCP(t *testing.T) {
|
||||
log.SetLevel("error")
|
||||
timeToRoomDeletion = 100 * time.Millisecond
|
||||
go Run("debug", "127.0.0.1", "8381", "pass123", "8382")
|
||||
time.Sleep(timeToRoomDeletion)
|
||||
err := PingServer("127.0.0.1:8381")
|
||||
assert.Nil(t, err)
|
||||
err = PingServer("127.0.0.1:8333")
|
||||
assert.NotNil(t, err)
|
||||
|
||||
time.Sleep(timeToRoomDeletion)
|
||||
c1, banner, _, err := ConnectToTCPServer("127.0.0.1:8381", "pass123", "testRoom", 1*time.Minute)
|
||||
assert.Equal(t, banner, "8382")
|
||||
assert.Nil(t, err)
|
||||
c2, _, _, err := ConnectToTCPServer("127.0.0.1:8381", "pass123", "testRoom")
|
||||
assert.Nil(t, err)
|
||||
_, _, _, err = ConnectToTCPServer("127.0.0.1:8381", "pass123", "testRoom")
|
||||
assert.NotNil(t, err)
|
||||
_, _, _, err = ConnectToTCPServer("127.0.0.1:8381", "pass123", "testRoom", 1*time.Nanosecond)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// try sending data
|
||||
assert.Nil(t, c1.Send([]byte("hello, c2")))
|
||||
var data []byte
|
||||
for {
|
||||
data, err = c2.Receive()
|
||||
if bytes.Equal(data, []byte{1}) {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("hello, c2"), data)
|
||||
|
||||
assert.Nil(t, c2.Send([]byte("hello, c1")))
|
||||
for {
|
||||
data, err = c1.Receive()
|
||||
if bytes.Equal(data, []byte{1}) {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("hello, c1"), data)
|
||||
|
||||
c1.Close()
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
|
@ -0,0 +1,589 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/flate"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/cespare/xxhash"
|
||||
"github.com/kalafut/imohash"
|
||||
"github.com/minio/highwayhash"
|
||||
"github.com/schollz/mnemonicode"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
)
|
||||
|
||||
const NbPinNumbers = 4
|
||||
const NbBytesWords = 4
|
||||
|
||||
// Get or create home directory
|
||||
func GetConfigDir(requireValidPath bool) (homedir string, err error) {
|
||||
if envHomedir, isSet := os.LookupEnv("CROC_CONFIG_DIR"); isSet {
|
||||
homedir = envHomedir
|
||||
} else if xdgConfigHome, isSet := os.LookupEnv("XDG_CONFIG_HOME"); isSet {
|
||||
homedir = path.Join(xdgConfigHome, "croc")
|
||||
} else {
|
||||
homedir, err = os.UserHomeDir()
|
||||
if err != nil {
|
||||
if !requireValidPath {
|
||||
err = nil
|
||||
homedir = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
homedir = path.Join(homedir, ".config", "croc")
|
||||
}
|
||||
|
||||
if requireValidPath {
|
||||
if _, err = os.Stat(homedir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(homedir, 0o700)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Exists reports whether the named file or directory exists.
|
||||
func Exists(name string) bool {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetInput returns the input with a given prompt
|
||||
func GetInput(prompt string) string {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Fprintf(os.Stderr, "%s", prompt)
|
||||
text, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
// HashFile returns the hash of a file or, in case of a symlink, the
|
||||
// SHA256 hash of its target. Takes an argument to specify the algorithm to use.
|
||||
func HashFile(fname string, algorithm string, showProgress ...bool) (hash256 []byte, err error) {
|
||||
doShowProgress := false
|
||||
if len(showProgress) > 0 {
|
||||
doShowProgress = showProgress[0]
|
||||
}
|
||||
var fstats os.FileInfo
|
||||
fstats, err = os.Lstat(fname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fstats.Mode()&os.ModeSymlink != 0 {
|
||||
var target string
|
||||
target, err = os.Readlink(fname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(SHA256(target)), nil
|
||||
}
|
||||
switch algorithm {
|
||||
case "imohash":
|
||||
return IMOHashFile(fname)
|
||||
case "md5":
|
||||
return MD5HashFile(fname, doShowProgress)
|
||||
case "xxhash":
|
||||
return XXHashFile(fname, doShowProgress)
|
||||
case "highway":
|
||||
return HighwayHashFile(fname, doShowProgress)
|
||||
}
|
||||
err = fmt.Errorf("unspecified algorithm")
|
||||
return
|
||||
}
|
||||
|
||||
// HighwayHashFile returns highwayhash of a file
|
||||
func HighwayHashFile(fname string, doShowProgress bool) (hashHighway []byte, err error) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
key, err := hex.DecodeString("1553c5383fb0b86578c3310da665b4f6e0521acf22eb58a99532ffed02a6b115")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h, err := highwayhash.New(key)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not create highwayhash: %s", err.Error())
|
||||
return
|
||||
}
|
||||
if doShowProgress {
|
||||
stat, _ := f.Stat()
|
||||
fnameShort := path.Base(fname)
|
||||
if len(fnameShort) > 20 {
|
||||
fnameShort = fnameShort[:20] + "..."
|
||||
}
|
||||
bar := progressbar.NewOptions64(stat.Size(),
|
||||
progressbar.OptionSetWriter(os.Stderr),
|
||||
progressbar.OptionShowBytes(true),
|
||||
progressbar.OptionSetDescription(fmt.Sprintf("Hashing %s", fnameShort)),
|
||||
progressbar.OptionClearOnFinish(),
|
||||
)
|
||||
if _, err = io.Copy(io.MultiWriter(h, bar), f); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err = io.Copy(h, f); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hashHighway = h.Sum(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// MD5HashFile returns MD5 hash
|
||||
func MD5HashFile(fname string, doShowProgress bool) (hash256 []byte, err error) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := md5.New()
|
||||
if doShowProgress {
|
||||
stat, _ := f.Stat()
|
||||
fnameShort := path.Base(fname)
|
||||
if len(fnameShort) > 20 {
|
||||
fnameShort = fnameShort[:20] + "..."
|
||||
}
|
||||
bar := progressbar.NewOptions64(stat.Size(),
|
||||
progressbar.OptionSetWriter(os.Stderr),
|
||||
progressbar.OptionShowBytes(true),
|
||||
progressbar.OptionSetDescription(fmt.Sprintf("Hashing %s", fnameShort)),
|
||||
progressbar.OptionClearOnFinish(),
|
||||
)
|
||||
if _, err = io.Copy(io.MultiWriter(h, bar), f); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err = io.Copy(h, f); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hash256 = h.Sum(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// IMOHashFile returns imohash
|
||||
func IMOHashFile(fname string) (hash []byte, err error) {
|
||||
b, err := imohash.SumFile(fname)
|
||||
hash = b[:]
|
||||
return
|
||||
}
|
||||
|
||||
var imofull = imohash.NewCustom(0, 0)
|
||||
|
||||
// IMOHashFileFull returns imohash of full file
|
||||
func IMOHashFileFull(fname string) (hash []byte, err error) {
|
||||
b, err := imofull.SumFile(fname)
|
||||
hash = b[:]
|
||||
return
|
||||
}
|
||||
|
||||
// XXHashFile returns the xxhash of a file
|
||||
func XXHashFile(fname string, doShowProgress bool) (hash256 []byte, err error) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := xxhash.New()
|
||||
if doShowProgress {
|
||||
stat, _ := f.Stat()
|
||||
fnameShort := path.Base(fname)
|
||||
if len(fnameShort) > 20 {
|
||||
fnameShort = fnameShort[:20] + "..."
|
||||
}
|
||||
bar := progressbar.NewOptions64(stat.Size(),
|
||||
progressbar.OptionSetWriter(os.Stderr),
|
||||
progressbar.OptionShowBytes(true),
|
||||
progressbar.OptionSetDescription(fmt.Sprintf("Hashing %s", fnameShort)),
|
||||
progressbar.OptionClearOnFinish(),
|
||||
)
|
||||
if _, err = io.Copy(io.MultiWriter(h, bar), f); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err = io.Copy(h, f); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hash256 = h.Sum(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// SHA256 returns sha256 sum
|
||||
func SHA256(s string) string {
|
||||
sha := sha256.New()
|
||||
sha.Write([]byte(s))
|
||||
return hex.EncodeToString(sha.Sum(nil))
|
||||
}
|
||||
|
||||
// PublicIP returns public ip address
|
||||
func PublicIP() (ip string, err error) {
|
||||
resp, err := http.Get("https://canhazip.com")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ip = strings.TrimSpace(string(bodyBytes))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LocalIP returns local ip address
|
||||
func LocalIP() string {
|
||||
conn, err := net.Dial("udp", "8.8.8.8:80")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
localAddr := conn.LocalAddr().(*net.UDPAddr)
|
||||
|
||||
return localAddr.IP.String()
|
||||
}
|
||||
|
||||
func GenerateRandomPin() string {
|
||||
s := ""
|
||||
max := new(big.Int)
|
||||
max.SetInt64(9)
|
||||
for i := 0; i < NbPinNumbers; i++ {
|
||||
v, err := rand.Int(rand.Reader, max)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s += fmt.Sprintf("%d", v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// GetRandomName returns mnemonicoded random name
|
||||
func GetRandomName() string {
|
||||
var result []string
|
||||
bs := make([]byte, NbBytesWords)
|
||||
rand.Read(bs)
|
||||
result = mnemonicode.EncodeWordList(result, bs)
|
||||
return GenerateRandomPin() + "-" + strings.Join(result, "-")
|
||||
}
|
||||
|
||||
// ByteCountDecimal converts bytes to human readable byte string
|
||||
func ByteCountDecimal(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
// MissingChunks returns the positions of missing chunks.
|
||||
// If file doesn't exist, it returns an empty chunk list (all chunks).
|
||||
// If the file size is not the same as requested, it returns an empty chunk list (all chunks).
|
||||
func MissingChunks(fname string, fsize int64, chunkSize int) (chunkRanges []int64) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fstat, err := os.Stat(fname)
|
||||
if err != nil || fstat.Size() != fsize {
|
||||
return
|
||||
}
|
||||
|
||||
emptyBuffer := make([]byte, chunkSize)
|
||||
chunkNum := 0
|
||||
chunks := make([]int64, int64(math.Ceil(float64(fsize)/float64(chunkSize))))
|
||||
var currentLocation int64
|
||||
for {
|
||||
buffer := make([]byte, chunkSize)
|
||||
bytesread, err := f.Read(buffer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if bytes.Equal(buffer[:bytesread], emptyBuffer[:bytesread]) {
|
||||
chunks[chunkNum] = currentLocation
|
||||
chunkNum++
|
||||
}
|
||||
currentLocation += int64(bytesread)
|
||||
}
|
||||
if chunkNum == 0 {
|
||||
chunkRanges = []int64{}
|
||||
} else {
|
||||
chunks = chunks[:chunkNum]
|
||||
chunkRanges = []int64{int64(chunkSize), chunks[0]}
|
||||
curCount := 0
|
||||
for i, chunk := range chunks {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
curCount++
|
||||
if chunk-chunks[i-1] > int64(chunkSize) {
|
||||
chunkRanges = append(chunkRanges, int64(curCount))
|
||||
chunkRanges = append(chunkRanges, chunk)
|
||||
curCount = 0
|
||||
}
|
||||
}
|
||||
chunkRanges = append(chunkRanges, int64(curCount+1))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ChunkRangesToChunks converts chunk ranges to list
|
||||
func ChunkRangesToChunks(chunkRanges []int64) (chunks []int64) {
|
||||
if len(chunkRanges) == 0 {
|
||||
return
|
||||
}
|
||||
chunkSize := chunkRanges[0]
|
||||
chunks = []int64{}
|
||||
for i := 1; i < len(chunkRanges); i += 2 {
|
||||
for j := int64(0); j < (chunkRanges[i+1]); j++ {
|
||||
chunks = append(chunks, chunkRanges[i]+j*chunkSize)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetLocalIPs returns all local ips
|
||||
func GetLocalIPs() (ips []string, err error) {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ips = []string{}
|
||||
for _, address := range addrs {
|
||||
// check the address type and if it is not a loopback the display it
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
ips = append(ips, ipnet.IP.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func RandomFileName() (fname string, err error) {
|
||||
f, err := os.CreateTemp(".", "croc-stdin-")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fname = f.Name()
|
||||
_ = f.Close()
|
||||
return
|
||||
}
|
||||
|
||||
func FindOpenPorts(host string, portNumStart, numPorts int) (openPorts []int) {
|
||||
openPorts = []int{}
|
||||
for port := portNumStart; port-portNumStart < 200; port++ {
|
||||
timeout := 100 * time.Millisecond
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, fmt.Sprint(port)), timeout)
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
} else if err != nil {
|
||||
openPorts = append(openPorts, port)
|
||||
}
|
||||
if len(openPorts) >= numPorts {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// local ip determination
|
||||
// https://stackoverflow.com/questions/41240761/check-if-ip-address-is-in-private-network-space
|
||||
var privateIPBlocks []*net.IPNet
|
||||
|
||||
func init() {
|
||||
for _, cidr := range []string{
|
||||
"127.0.0.0/8", // IPv4 loopback
|
||||
"10.0.0.0/8", // RFC1918
|
||||
"172.16.0.0/12", // RFC1918
|
||||
"192.168.0.0/16", // RFC1918
|
||||
"169.254.0.0/16", // RFC3927 link-local
|
||||
"::1/128", // IPv6 loopback
|
||||
"fe80::/10", // IPv6 link-local
|
||||
"fc00::/7", // IPv6 unique local addr
|
||||
} {
|
||||
_, block, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("parse error on %q: %v", cidr, err))
|
||||
}
|
||||
privateIPBlocks = append(privateIPBlocks, block)
|
||||
}
|
||||
}
|
||||
|
||||
func IsLocalIP(ipaddress string) bool {
|
||||
if strings.Contains(ipaddress, "127.0.0.1") {
|
||||
return true
|
||||
}
|
||||
host, _, _ := net.SplitHostPort(ipaddress)
|
||||
ip := net.ParseIP(host)
|
||||
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
||||
return true
|
||||
}
|
||||
for _, block := range privateIPBlocks {
|
||||
if block.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ZipDirectory(destination string, source string) (err error) {
|
||||
if _, err = os.Stat(destination); err == nil {
|
||||
log.Fatalf("%s file already exists!\n", destination)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Zipping %s to %s\n", source, destination)
|
||||
file, err := os.Create(destination)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer file.Close()
|
||||
writer := zip.NewWriter(file)
|
||||
// no compression because croc does its compression on the fly
|
||||
writer.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
|
||||
return flate.NewWriter(out, flate.NoCompression)
|
||||
})
|
||||
defer writer.Close()
|
||||
err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
if info.Mode().IsRegular() {
|
||||
f1, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer f1.Close()
|
||||
zipPath := strings.ReplaceAll(path, source, strings.TrimSuffix(destination, ".zip"))
|
||||
zipPath = filepath.ToSlash(zipPath)
|
||||
w1, err := writer.Create(zipPath)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
if _, err := io.Copy(w1, f1); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\r\033[2K")
|
||||
fmt.Fprintf(os.Stderr, "\rAdding %s", zipPath)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnzipDirectory(destination string, source string) error {
|
||||
archive, err := zip.OpenReader(source)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer archive.Close()
|
||||
|
||||
for _, f := range archive.File {
|
||||
filePath := filepath.Join(destination, f.Name)
|
||||
fmt.Fprintf(os.Stderr, "\r\033[2K")
|
||||
fmt.Fprintf(os.Stderr, "\rUnzipping file %s", filePath)
|
||||
// Issue #593 conceal path traversal vulnerability
|
||||
// make sure the filepath does not have ".."
|
||||
filePath = filepath.Clean(filePath)
|
||||
if strings.Contains(filePath, "..") {
|
||||
log.Fatalf("Invalid file path %s\n", filePath)
|
||||
}
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(filePath, os.ModePerm)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// check if file exists
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
prompt := fmt.Sprintf("\nOverwrite '%s'? (y/N) ", filePath)
|
||||
choice := strings.ToLower(GetInput(prompt))
|
||||
if choice != "y" && choice != "yes" {
|
||||
fmt.Fprintf(os.Stderr, "Skipping '%s'\n", filePath)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
fileInArchive, err := f.Open()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(dstFile, fileInArchive); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
dstFile.Close()
|
||||
fileInArchive.Close()
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidFileName checks if a filename is valid
|
||||
// by making sure it has no invisible characters
|
||||
func ValidFileName(fname string) bool {
|
||||
clean1 := strings.Map(func(r rune) rune {
|
||||
if unicode.IsGraphic(r) {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, fname)
|
||||
|
||||
clean2 := strings.Map(func(r rune) rune {
|
||||
if unicode.IsPrint(r) {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, fname)
|
||||
|
||||
return (fname == clean1) && (fname == clean2)
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const TCP_BUFFER_SIZE = 1024 * 64
|
||||
|
||||
var bigFileSize = 75000000
|
||||
|
||||
func bigFile() {
|
||||
os.WriteFile("bigfile.test", bytes.Repeat([]byte("z"), bigFileSize), 0o666)
|
||||
}
|
||||
|
||||
func BenchmarkMD5(b *testing.B) {
|
||||
bigFile()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
MD5HashFile("bigfile.test", false)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkXXHash(b *testing.B) {
|
||||
bigFile()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
XXHashFile("bigfile.test", false)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkImoHash(b *testing.B) {
|
||||
bigFile()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
IMOHashFile("bigfile.test")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHighwayHash(b *testing.B) {
|
||||
bigFile()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
HighwayHashFile("bigfile.test", false)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkImoHashFull(b *testing.B) {
|
||||
bigFile()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
IMOHashFileFull("bigfile.test")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSha256(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
SHA256("hello,world")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMissingChunks(b *testing.B) {
|
||||
bigFile()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
MissingChunks("bigfile.test", int64(bigFileSize), TCP_BUFFER_SIZE/2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExists(t *testing.T) {
|
||||
bigFile()
|
||||
defer os.Remove("bigfile.test")
|
||||
fmt.Println(GetLocalIPs())
|
||||
assert.True(t, Exists("bigfile.test"))
|
||||
assert.False(t, Exists("doesnotexist"))
|
||||
}
|
||||
|
||||
func TestMD5HashFile(t *testing.T) {
|
||||
bigFile()
|
||||
defer os.Remove("bigfile.test")
|
||||
b, err := MD5HashFile("bigfile.test", false)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "8304ff018e02baad0e3555bade29a405", fmt.Sprintf("%x", b))
|
||||
_, err = MD5HashFile("bigfile.test.nofile", false)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestHighwayHashFile(t *testing.T) {
|
||||
bigFile()
|
||||
defer os.Remove("bigfile.test")
|
||||
b, err := HighwayHashFile("bigfile.test", false)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "3c32999529323ed66a67aeac5720c7bf1301dcc5dca87d8d46595e85ff990329", fmt.Sprintf("%x", b))
|
||||
_, err = HighwayHashFile("bigfile.test.nofile", false)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestIMOHashFile(t *testing.T) {
|
||||
bigFile()
|
||||
defer os.Remove("bigfile.test")
|
||||
b, err := IMOHashFile("bigfile.test")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "c0d1e123ca94148ffea146137684ebb9", fmt.Sprintf("%x", b))
|
||||
}
|
||||
|
||||
func TestXXHashFile(t *testing.T) {
|
||||
bigFile()
|
||||
defer os.Remove("bigfile.test")
|
||||
b, err := XXHashFile("bigfile.test", false)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "4918740eb5ccb6f7", fmt.Sprintf("%x", b))
|
||||
_, err = XXHashFile("nofile", false)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestSHA256(t *testing.T) {
|
||||
assert.Equal(t, "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b", SHA256("hello, world"))
|
||||
}
|
||||
|
||||
func TestByteCountDecimal(t *testing.T) {
|
||||
assert.Equal(t, "10.0 kB", ByteCountDecimal(10240))
|
||||
assert.Equal(t, "50 B", ByteCountDecimal(50))
|
||||
assert.Equal(t, "12.4 MB", ByteCountDecimal(13002343))
|
||||
}
|
||||
|
||||
func TestMissingChunks(t *testing.T) {
|
||||
fileSize := 100
|
||||
chunkSize := 10
|
||||
rand.Seed(1)
|
||||
bigBuff := make([]byte, fileSize)
|
||||
rand.Read(bigBuff)
|
||||
os.WriteFile("missing.test", bigBuff, 0o644)
|
||||
empty := make([]byte, chunkSize)
|
||||
f, err := os.OpenFile("missing.test", os.O_RDWR, 0o644)
|
||||
assert.Nil(t, err)
|
||||
for block := 0; block < fileSize/chunkSize; block++ {
|
||||
if block == 0 || block == 4 || block == 5 || block >= 7 {
|
||||
f.WriteAt(empty, int64(block*chunkSize))
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
|
||||
chunkRanges := MissingChunks("missing.test", int64(fileSize), chunkSize)
|
||||
assert.Equal(t, []int64{10, 0, 1, 40, 2, 70, 3}, chunkRanges)
|
||||
|
||||
chunks := ChunkRangesToChunks(chunkRanges)
|
||||
assert.Equal(t, []int64{0, 40, 50, 70, 80, 90}, chunks)
|
||||
|
||||
os.Remove("missing.test")
|
||||
|
||||
content := []byte("temporary file's content")
|
||||
tmpfile, err := os.CreateTemp("", "example")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
|
||||
if _, err := tmpfile.Write(content); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
chunkRanges = MissingChunks(tmpfile.Name(), int64(len(content)), chunkSize)
|
||||
assert.Empty(t, chunkRanges)
|
||||
chunkRanges = MissingChunks(tmpfile.Name(), int64(len(content)+10), chunkSize)
|
||||
assert.Empty(t, chunkRanges)
|
||||
chunkRanges = MissingChunks(tmpfile.Name()+"ok", int64(len(content)), chunkSize)
|
||||
assert.Empty(t, chunkRanges)
|
||||
chunks = ChunkRangesToChunks(chunkRanges)
|
||||
assert.Empty(t, chunks)
|
||||
}
|
||||
|
||||
// func Test1(t *testing.T) {
|
||||
// chunkRanges := MissingChunks("../../m/bigfile.test", int64(75000000), 1024*64/2)
|
||||
// fmt.Println(chunkRanges)
|
||||
// fmt.Println(ChunkRangesToChunks((chunkRanges)))
|
||||
// assert.Nil(t, nil)
|
||||
// }
|
||||
|
||||
func TestHashFile(t *testing.T) {
|
||||
content := []byte("temporary file's content")
|
||||
tmpfile, err := os.CreateTemp("", "example")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
|
||||
if _, err = tmpfile.Write(content); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = tmpfile.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
hashed, err := HashFile(tmpfile.Name(), "xxhash")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "e66c561610ad51e2", fmt.Sprintf("%x", hashed))
|
||||
}
|
||||
|
||||
func TestPublicIP(t *testing.T) {
|
||||
ip, err := PublicIP()
|
||||
fmt.Println(ip)
|
||||
assert.True(t, strings.Contains(ip, ".") || strings.Contains(ip, ":"))
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestLocalIP(t *testing.T) {
|
||||
ip := LocalIP()
|
||||
fmt.Println(ip)
|
||||
assert.True(t, strings.Contains(ip, ".") || strings.Contains(ip, ":"))
|
||||
}
|
||||
|
||||
func TestGetRandomName(t *testing.T) {
|
||||
name := GetRandomName()
|
||||
fmt.Println(name)
|
||||
assert.NotEmpty(t, name)
|
||||
}
|
||||
|
||||
func intSliceSame(a, b []int) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestFindOpenPorts(t *testing.T) {
|
||||
openPorts := FindOpenPorts("127.0.0.1", 9009, 4)
|
||||
if !intSliceSame(openPorts, []int{9009, 9010, 9011, 9012}) && !intSliceSame(openPorts, []int{9014, 9015, 9016, 9017}) {
|
||||
t.Errorf("openPorts: %v", openPorts)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLocalIP(t *testing.T) {
|
||||
assert.True(t, IsLocalIP("192.168.0.14:9009"))
|
||||
}
|
||||
|
||||
func TestValidFileName(t *testing.T) {
|
||||
// contains regular characters
|
||||
assert.True(t, ValidFileName("中文.csl"))
|
||||
// contains regular characters
|
||||
assert.True(t, ValidFileName("[something].csl"))
|
||||
// contains regular characters
|
||||
assert.True(t, ValidFileName("[(something)].csl"))
|
||||
// contains invisible character
|
||||
assert.False(t, ValidFileName("D中文.cslouglas"))
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
<p align="center">
|
||||
<img
|
||||
src="https://user-images.githubusercontent.com/6550035/31846899-2b8a7034-b5cf-11e7-9643-afe552226c59.png"
|
||||
width="100%" border="0" alt="croc">
|
||||
<br>
|
||||
<a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/badge/version-0.1.0-green.svg?style=flat-square" alt="Version"></a>
|
||||
<a href="https://gitter.im/schollz/croc?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge"><img src="https://img.shields.io/badge/chat-on%20gitter-green.svg?style=flat-square" alt="Version"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">Secure transfer of stuff from one side of the internet to the other.</p>
|
||||
|
||||
This is more or less (but mostly *less*) a Golang port of [@warner's](https://github.com/warner) [*magic-wormhole*](https://github.com/warner/magic-wormhole) which allows you to directly transfer files and folders between computers. I decided to make this because I wanted to send my friend Jessie a file using *magic-wormhole* and when I told Jessie how to install the dependencies she made this face: :sob:. So, nominally, *croc* does the same thing (encrypted file transfer directly between computers) without dependencies so you can just double-click on your computer, even if you use Windows.
|
||||
|
||||
**Don't we have enough open-source peer-to-peer file-transfer utilities?**
|
||||
|
||||
[There](https://github.com/cowbell/sharedrop) [are](https://github.com/webtorrent/instant.io) [great](https://github.com/kern/filepizza) [tools](https://github.com/warner/magic-wormhole) [that](https://github.com/zerotier/toss) [already](https://github.com/ipfs/go-ipfs) [do](https://github.com/zerotier/toss) [this](https://github.com/nils-werner/zget). But, no we don't, because after review, [I found it was useful to make a new one](https://schollz.github.io/sending-a-file/).
|
||||
|
||||
# Example
|
||||
|
||||
_These two gifs should run in sync if you force-reload (Ctl+F5)_
|
||||
|
||||
**Sender:**
|
||||
|
||||
![send](https://user-images.githubusercontent.com/6550035/31864532-ad80c6ae-b71b-11e7-91f9-bcba8143d3cf.gif)
|
||||
|
||||
**Receiver:**
|
||||
|
||||
![receive](https://user-images.githubusercontent.com/6550035/31864531-ad6e22c4-b71b-11e7-901a-02a210057cf1.gif)
|
||||
|
||||
|
||||
**Sender:**
|
||||
|
||||
```
|
||||
$ croc -send croc.exe
|
||||
Sending 4.4 MB file named 'croc.exe'
|
||||
Code is: 4-cement-galaxy-alpha
|
||||
|
||||
Sending (->24.65.41.43:50843)..
|
||||
0s [==========================================================] 100%
|
||||
File sent.
|
||||
```
|
||||
|
||||
**Receiver:**
|
||||
|
||||
```
|
||||
$ croc
|
||||
Enter receive code: 4-cement-galaxy-alpha
|
||||
Receiving file (4.4 MB) into: croc.exe
|
||||
ok? (y/n): y
|
||||
|
||||
Receiving (<-50.32.38.188:50843)..
|
||||
0s [==========================================================] 100%
|
||||
Received file written to croc.exe
|
||||
```
|
||||
|
||||
Note, by default, you don't need any arguments for receiving! This makes it possible for you to just double click the executable to run (nice for those of us that aren't computer wizards).
|
||||
|
||||
|
||||
# Install
|
||||
|
||||
[Download the latest release for your system](https://github.com/schollz/croc/releases/latest).
|
||||
|
||||
Or, you can [install Go](https://golang.org/dl/) and build from source with `go get github.com/schollz/croc`.
|
||||
|
||||
|
||||
|
||||
# How does it work?
|
||||
|
||||
*croc* is similar to [magic-wormhole](https://github.com/warner/magic-wormhole#design) in spirit and design. Like *magic-wormhole*, *croc* generates a code phrase for you to share with your friend which allows secure end-to-end transfering of files and folders through a intermediary relay that connects the TCP ports between the two computers.
|
||||
|
||||
In *croc*, code phrase is 16 random bits that are [menemonic encoded](http://web.archive.org/web/20101031205747/http://www.tothink.com/mnemonic/) plus a prepended integer to specify number of threads. This code phrase is hashed using sha256 and sent to a relay which maps that key to that connection. When the relay finds a matching key for both the receiver and the sender (i.e. they both have the same code phrase), then the sender transmits the encrypted metadata to the receiver through the relay. Then the receiver decrypts and reviews the metadata (file name, size), and chooses whether to consent to the transfer.
|
||||
|
||||
After the receiver consents to the transfer, the sender transmits encrypted data through the relay. The relay setups up [Go channels](https://golang.org/doc/effective_go.html?h=chan#channels) for each connection which pipes all the data incoming from that sender's connection out to the receiver's connection. After the transmission the channels are destroyed and all the connection and meta data information is wiped from the relay server. The encrypted file data never is stored on the relay.
|
||||
|
||||
**Encryption**
|
||||
|
||||
Encryption uses PBKDF2 (see [RFC2898](http://www.ietf.org/rfc/rfc2898.txt)) where the code phrase shared between the sender and receiver is used as the passphrase. For each of the two encrypted data blocks (metadata stored on relay server, and file data transmitted), a random 8-byte salt is used and a IV is generated according to [NIST Recommendation for Block ciphers, Section 8.2](http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf).
|
||||
|
||||
|
||||
**Decryption**
|
||||
|
||||
On the receiver's computer, each piece of received encrypted data is written to a separate file. These files are concatenated and then decrypted. The hash of the decrypted file is then checked against the hash transmitted from the sender (part of the meta data block).
|
||||
|
||||
## Run your own relay
|
||||
|
||||
*croc* relies on a TCP relay to staple the parallel incoming and outgoing connections. The relay temporarily stores connection information and the encrypted meta information. The default uses a public relay at, `cowyo.com`, which has no guarantees except that I guarantee to turn if off as soon as it gets abused ([click here to check the current status of the public relay](https://stats.uptimerobot.com/lOwJYIgRm)).
|
||||
|
||||
I recommend you run your own relay, it is very easy. On your server, `your-server.com`, just run
|
||||
|
||||
```
|
||||
$ croc -relay
|
||||
```
|
||||
|
||||
Now, when you use *croc* to send and receive you should add `-server your-server.com` to use your relay server.
|
||||
|
||||
_Note:_ If you are behind a firewall, make sure to open up TCP ports 27001-27009.
|
||||
|
||||
# Contribute
|
||||
|
||||
I am awed by all the [great contributions](#acknowledgements) made! If you feel like contributing, in any way, by all means you can send an Issue, a PR, ask a question, or tweet me ([@yakczar](http://ctt.ec/Rq054)).
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
|
||||
# Acknowledgements
|
||||
|
||||
Thanks...
|
||||
|
||||
- ...[@warner](https://github.com/warner) for the [idea](https://github.com/warner/magic-wormhole).
|
||||
- ...[@tscholl2](https://github.com/tscholl2) for the [encryption gists](https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28).
|
||||
- ...[@skorokithakis](https://github.com/skorokithakis) for [code on proxying two connections](https://www.stavros.io/posts/proxying-two-connections-go/).
|
||||
- ...for making pull requests [@Girbons](https://github.com/Girbons), [@techtide](https://github.com/techtide), [@heymatthew](https://github.com/heymatthew), [@Lunsford94](https://github.com/Lunsford94), [@lummie](https://github.com/lummie), [@jesuiscamille](https://github.com/jesuiscamille), [@threefjord](https://github.com/threefjord), [@marcossegovia](https://github.com/marcossegovia), [@csleong98](https://github.com/csleong98), [@afotescu](https://github.com/afotescu)!
|
|
@ -1 +0,0 @@
|
|||
Some simple text to see if it works
|
|
@ -1 +0,0 @@
|
|||
More data to see if it 100% works
|
172
utils.go
172
utils.go
|
@ -1,172 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// CatFiles copies data from n files to a single one and removes source files
|
||||
// if Debug mode is set to false
|
||||
func CatFiles(files []string, outfile string, remove bool) error {
|
||||
finished, err := os.Create(outfile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "CatFiles create: ")
|
||||
}
|
||||
defer finished.Close()
|
||||
for _, file := range files {
|
||||
fh, err := os.Open(file)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("CatFiles open %v: ", file))
|
||||
}
|
||||
_, err = io.Copy(finished, fh)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "CatFiles copy: ")
|
||||
}
|
||||
fh.Close()
|
||||
if remove {
|
||||
os.Remove(file)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SplitFile creates a bunch of smaller files with the data from source splited into them
|
||||
func SplitFile(fileName string, numPieces int) (err error) {
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bytesPerPiece := int(math.Ceil(float64(fi.Size()) / float64(numPieces)))
|
||||
bytesRead := 0
|
||||
i := 0
|
||||
out, err := os.Create(fileName + "." + strconv.Itoa(i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
if bytesPerPiece < 4096/numPieces {
|
||||
buf = make([]byte, bytesPerPiece)
|
||||
}
|
||||
for {
|
||||
n, err := file.Read(buf)
|
||||
out.Write(buf[:n])
|
||||
// If written bytes count is smaller than lenght of buffer
|
||||
// then we don't create one more empty file
|
||||
if err == io.EOF || n < len(buf) {
|
||||
break
|
||||
}
|
||||
bytesRead += n
|
||||
|
||||
if bytesRead >= bytesPerPiece {
|
||||
// Close file and open a new one
|
||||
out.Close()
|
||||
i++
|
||||
out, err = os.Create(fileName + "." + strconv.Itoa(i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bytesRead = 0
|
||||
}
|
||||
}
|
||||
out.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyFile copies a file from src to dst. If src and dst files exist, and are
|
||||
// the same, then return success. Otherise, attempt to create a hard link
|
||||
// between the two files. If that fail, copy the file contents from src to dst.
|
||||
func CopyFile(src, dst string) (err error) {
|
||||
sfi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !sfi.Mode().IsRegular() {
|
||||
// cannot copy non-regular files (e.g., directories,
|
||||
// symlinks, devices, etc.)
|
||||
return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
|
||||
}
|
||||
dfi, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !(dfi.Mode().IsRegular()) {
|
||||
return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
|
||||
}
|
||||
if os.SameFile(sfi, dfi) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = os.Link(src, dst); err == nil {
|
||||
return
|
||||
}
|
||||
err = copyFileContents(src, dst)
|
||||
return
|
||||
}
|
||||
|
||||
// copyFileContents copies the contents of the file named src to the file named
|
||||
// by dst. The file will be created if it does not already exist. If the
|
||||
// destination file exists, all it's contents will be replaced by the contents
|
||||
// of the source file.
|
||||
func copyFileContents(src, dst string) (err error) {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
cerr := out.Close()
|
||||
if err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
return
|
||||
}
|
||||
err = out.Sync()
|
||||
return
|
||||
}
|
||||
|
||||
// HashFile does a md5 hash on the file
|
||||
// from https://golang.org/pkg/crypto/md5/#example_New_file
|
||||
func HashFile(filename string) (hash string, err error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := md5.New()
|
||||
if _, err = io.Copy(h, f); err != nil {
|
||||
return
|
||||
}
|
||||
hash = fmt.Sprintf("%x", h.Sum(nil))
|
||||
return
|
||||
}
|
||||
|
||||
// FileSize returns the size of a file
|
||||
func FileSize(filename string) (int, error) {
|
||||
fi, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
size := int(fi.Size())
|
||||
return size, nil
|
||||
}
|
119
utils_test.go
119
utils_test.go
|
@ -1,119 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitFile(t *testing.T) {
|
||||
err := SplitFile("testing_data/README.md", 3)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
os.Remove("testing_data/README.md.0")
|
||||
os.Remove("testing_data/README.md.1")
|
||||
}
|
||||
|
||||
func TestFileSize(t *testing.T) {
|
||||
t.Run("File is ok ", func(t *testing.T) {
|
||||
_, err := FileSize("testing_data/README.md")
|
||||
if err != nil {
|
||||
t.Errorf("should pass with no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("File does not exist", func(t *testing.T) {
|
||||
s, err := FileSize("testing_data/someStrangeFile")
|
||||
if err == nil {
|
||||
t.Error("should return an error")
|
||||
}
|
||||
if s != -1 {
|
||||
t.Errorf("size should be -1, got: %d", s)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHashFile(t *testing.T) {
|
||||
t.Run("Hash created successfully", func(t *testing.T) {
|
||||
h, err := HashFile("testing_data/README.md")
|
||||
if err != nil {
|
||||
t.Errorf("should pass with no error, got: %v", err)
|
||||
}
|
||||
if len(h) != 32 {
|
||||
t.Errorf("invalid md5 hash, length should be 32 got: %d", len(h))
|
||||
}
|
||||
})
|
||||
t.Run("File does not exist", func(t *testing.T) {
|
||||
h, err := HashFile("testing_data/someStrangeFile")
|
||||
if err == nil {
|
||||
t.Error("should return an error")
|
||||
}
|
||||
if len(h) > 0 {
|
||||
t.Errorf("hash length should be 0, got: %d", len(h))
|
||||
}
|
||||
if h != "" {
|
||||
t.Errorf("hash should be empty string, got: %s", h)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCopyFileContents(t *testing.T) {
|
||||
t.Run("Content copied successfully", func(t *testing.T) {
|
||||
f1 := "testing_data/README.md"
|
||||
f2 := "testing_data/CopyOfREADME.md"
|
||||
err := copyFileContents(f1, f2)
|
||||
if err != nil {
|
||||
t.Errorf("should pass with no error, got: %v", err)
|
||||
}
|
||||
f1Length, err := FileSize(f1)
|
||||
if err != nil {
|
||||
t.Errorf("can't get file nr1 size: %v", err)
|
||||
}
|
||||
f2Length, err := FileSize(f2)
|
||||
if err != nil {
|
||||
t.Errorf("can't get file nr2 size: %v", err)
|
||||
}
|
||||
|
||||
if f1Length != f2Length {
|
||||
t.Errorf("size of both files should be same got: file1: %d file2: %d", f1Length, f2Length)
|
||||
}
|
||||
os.Remove(f2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCopyFile(t *testing.T) {
|
||||
t.Run("Files copied successfully", func(t *testing.T) {
|
||||
f1 := "testing_data/README.md"
|
||||
f2 := "testing_data/CopyOfREADME.md"
|
||||
err := CopyFile(f1, f2)
|
||||
if err != nil {
|
||||
t.Errorf("should pass with no error, got: %v", err)
|
||||
}
|
||||
f1Length, err := FileSize(f1)
|
||||
if err != nil {
|
||||
t.Errorf("can't get file nr1 size: %v", err)
|
||||
}
|
||||
f2Length, err := FileSize(f2)
|
||||
if err != nil {
|
||||
t.Errorf("can't get file nr2 size: %v", err)
|
||||
}
|
||||
|
||||
if f1Length != f2Length {
|
||||
t.Errorf("size of both files should be same got: file1: %d file2: %d", f1Length, f2Length)
|
||||
}
|
||||
os.Remove(f2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCatFiles(t *testing.T) {
|
||||
t.Run("CatFiles passing", func(t *testing.T) {
|
||||
files := []string{"testing_data/catFile1.txt", "testing_data/catFile2.txt"}
|
||||
err := CatFiles(files, "testing_data/CatFile.txt", false)
|
||||
if err != nil {
|
||||
t.Errorf("should pass with no error, got: %v", err)
|
||||
}
|
||||
if _, err := os.Stat("testing_data/CatFile.txt"); os.IsNotExist(err) {
|
||||
t.Errorf("file were not created: %v", err)
|
||||
}
|
||||
os.Remove("testing_data/CatFile.txt")
|
||||
})
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.3.x
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- master
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: master
|
||||
fast_finish: true
|
||||
install:
|
||||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
||||
- go tool vet .
|
||||
- go test -v -race ./...
|
|
@ -1,21 +0,0 @@
|
|||
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
<http://www.opensource.org/licenses/mit-license.php>
|
|
@ -1,92 +0,0 @@
|
|||
# Humane Units [![Build Status](https://travis-ci.org/dustin/go-humanize.svg?branch=master)](https://travis-ci.org/dustin/go-humanize) [![GoDoc](https://godoc.org/github.com/dustin/go-humanize?status.svg)](https://godoc.org/github.com/dustin/go-humanize)
|
||||
|
||||
Just a few functions for helping humanize times and sizes.
|
||||
|
||||
`go get` it as `github.com/dustin/go-humanize`, import it as
|
||||
`"github.com/dustin/go-humanize"`, use it as `humanize`.
|
||||
|
||||
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for
|
||||
complete documentation.
|
||||
|
||||
## Sizes
|
||||
|
||||
This lets you take numbers like `82854982` and convert them to useful
|
||||
strings like, `83 MB` or `79 MiB` (whichever you prefer).
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
|
||||
```
|
||||
|
||||
## Times
|
||||
|
||||
This lets you take a `time.Time` and spit it out in relative terms.
|
||||
For example, `12 seconds ago` or `3 days from now`.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.
|
||||
```
|
||||
|
||||
Thanks to Kyle Lemons for the time implementation from an IRC
|
||||
conversation one day. It's pretty neat.
|
||||
|
||||
## Ordinals
|
||||
|
||||
From a [mailing list discussion][odisc] where a user wanted to be able
|
||||
to label ordinals.
|
||||
|
||||
0 -> 0th
|
||||
1 -> 1st
|
||||
2 -> 2nd
|
||||
3 -> 3rd
|
||||
4 -> 4th
|
||||
[...]
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.
|
||||
```
|
||||
|
||||
## Commas
|
||||
|
||||
Want to shove commas into numbers? Be my guest.
|
||||
|
||||
0 -> 0
|
||||
100 -> 100
|
||||
1000 -> 1,000
|
||||
1000000000 -> 1,000,000,000
|
||||
-100000 -> -100,000
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
|
||||
```
|
||||
|
||||
## Ftoa
|
||||
|
||||
Nicer float64 formatter that removes trailing zeros.
|
||||
|
||||
```go
|
||||
fmt.Printf("%f", 2.24) // 2.240000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
|
||||
fmt.Printf("%f", 2.0) // 2.000000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
|
||||
```
|
||||
|
||||
## SI notation
|
||||
|
||||
Format numbers with [SI notation][sinotation].
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
humanize.SI(0.00000000223, "M") // 2.23 nM
|
||||
```
|
||||
|
||||
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
|
||||
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix
|
|
@ -1,31 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// order of magnitude (to a max order)
|
||||
func oomm(n, b *big.Int, maxmag int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
if mag == maxmag && maxmag >= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
|
||||
// total order of magnitude
|
||||
// (same as above, but with no upper limit)
|
||||
func oom(n, b *big.Int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
bigIECExp = big.NewInt(1024)
|
||||
|
||||
// BigByte is one byte in bit.Ints
|
||||
BigByte = big.NewInt(1)
|
||||
// BigKiByte is 1,024 bytes in bit.Ints
|
||||
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
|
||||
// BigMiByte is 1,024 k bytes in bit.Ints
|
||||
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
|
||||
// BigGiByte is 1,024 m bytes in bit.Ints
|
||||
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
|
||||
// BigTiByte is 1,024 g bytes in bit.Ints
|
||||
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
|
||||
// BigPiByte is 1,024 t bytes in bit.Ints
|
||||
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
|
||||
// BigEiByte is 1,024 p bytes in bit.Ints
|
||||
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
|
||||
// BigZiByte is 1,024 e bytes in bit.Ints
|
||||
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
|
||||
// BigYiByte is 1,024 z bytes in bit.Ints
|
||||
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
|
||||
)
|
||||
|
||||
var (
|
||||
bigSIExp = big.NewInt(1000)
|
||||
|
||||
// BigSIByte is one SI byte in big.Ints
|
||||
BigSIByte = big.NewInt(1)
|
||||
// BigKByte is 1,000 SI bytes in big.Ints
|
||||
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
|
||||
// BigMByte is 1,000 SI k bytes in big.Ints
|
||||
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
|
||||
// BigGByte is 1,000 SI m bytes in big.Ints
|
||||
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
|
||||
// BigTByte is 1,000 SI g bytes in big.Ints
|
||||
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
|
||||
// BigPByte is 1,000 SI t bytes in big.Ints
|
||||
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
|
||||
// BigEByte is 1,000 SI p bytes in big.Ints
|
||||
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
|
||||
// BigZByte is 1,000 SI e bytes in big.Ints
|
||||
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
|
||||
// BigYByte is 1,000 SI z bytes in big.Ints
|
||||
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
|
||||
)
|
||||
|
||||
var bigBytesSizeTable = map[string]*big.Int{
|
||||
"b": BigByte,
|
||||
"kib": BigKiByte,
|
||||
"kb": BigKByte,
|
||||
"mib": BigMiByte,
|
||||
"mb": BigMByte,
|
||||
"gib": BigGiByte,
|
||||
"gb": BigGByte,
|
||||
"tib": BigTiByte,
|
||||
"tb": BigTByte,
|
||||
"pib": BigPiByte,
|
||||
"pb": BigPByte,
|
||||
"eib": BigEiByte,
|
||||
"eb": BigEByte,
|
||||
"zib": BigZiByte,
|
||||
"zb": BigZByte,
|
||||
"yib": BigYiByte,
|
||||
"yb": BigYByte,
|
||||
// Without suffix
|
||||
"": BigByte,
|
||||
"ki": BigKiByte,
|
||||
"k": BigKByte,
|
||||
"mi": BigMiByte,
|
||||
"m": BigMByte,
|
||||
"gi": BigGiByte,
|
||||
"g": BigGByte,
|
||||
"ti": BigTiByte,
|
||||
"t": BigTByte,
|
||||
"pi": BigPiByte,
|
||||
"p": BigPByte,
|
||||
"ei": BigEiByte,
|
||||
"e": BigEByte,
|
||||
"z": BigZByte,
|
||||
"zi": BigZiByte,
|
||||
"y": BigYByte,
|
||||
"yi": BigYiByte,
|
||||
}
|
||||
|
||||
var ten = big.NewInt(10)
|
||||
|
||||
func humanateBigBytes(s, base *big.Int, sizes []string) string {
|
||||
if s.Cmp(ten) < 0 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
c := (&big.Int{}).Set(s)
|
||||
val, mag := oomm(c, base, len(sizes)-1)
|
||||
suffix := sizes[mag]
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
|
||||
}
|
||||
|
||||
// BigBytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigBytes(82854982) -> 83 MB
|
||||
func BigBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
return humanateBigBytes(s, bigSIExp, sizes)
|
||||
}
|
||||
|
||||
// BigIBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigIBytes(82854982) -> 79 MiB
|
||||
func BigIBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
|
||||
return humanateBigBytes(s, bigIECExp, sizes)
|
||||
}
|
||||
|
||||
// ParseBigBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See also: BigBytes, BigIBytes.
|
||||
//
|
||||
// ParseBigBytes("42 MB") -> 42000000, nil
|
||||
// ParseBigBytes("42 mib") -> 44040192, nil
|
||||
func ParseBigBytes(s string) (*big.Int, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
val := &big.Rat{}
|
||||
_, err := fmt.Sscanf(num, "%f", val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bigBytesSizeTable[extra]; ok {
|
||||
mv := (&big.Rat{}).SetInt(m)
|
||||
val.Mul(val, mv)
|
||||
rv := &big.Int{}
|
||||
rv.Div(val.Num(), val.Denom())
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBigByteParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
exp uint64
|
||||
}{
|
||||
{"42", 42},
|
||||
{"42MB", 42000000},
|
||||
{"42MiB", 44040192},
|
||||
{"42mb", 42000000},
|
||||
{"42mib", 44040192},
|
||||
{"42MIB", 44040192},
|
||||
{"42 MB", 42000000},
|
||||
{"42 MiB", 44040192},
|
||||
{"42 mb", 42000000},
|
||||
{"42 mib", 44040192},
|
||||
{"42 MIB", 44040192},
|
||||
{"42.5MB", 42500000},
|
||||
{"42.5MiB", 44564480},
|
||||
{"42.5 MB", 42500000},
|
||||
{"42.5 MiB", 44564480},
|
||||
// No need to say B
|
||||
{"42M", 42000000},
|
||||
{"42Mi", 44040192},
|
||||
{"42m", 42000000},
|
||||
{"42mi", 44040192},
|
||||
{"42MI", 44040192},
|
||||
{"42 M", 42000000},
|
||||
{"42 Mi", 44040192},
|
||||
{"42 m", 42000000},
|
||||
{"42 mi", 44040192},
|
||||
{"42 MI", 44040192},
|
||||
{"42.5M", 42500000},
|
||||
{"42.5Mi", 44564480},
|
||||
{"42.5 M", 42500000},
|
||||
{"42.5 Mi", 44564480},
|
||||
{"1,005.03 MB", 1005030000},
|
||||
// Large testing, breaks when too much larger than
|
||||
// this.
|
||||
{"12.5 EB", uint64(12.5 * float64(EByte))},
|
||||
{"12.5 E", uint64(12.5 * float64(EByte))},
|
||||
{"12.5 EiB", uint64(12.5 * float64(EiByte))},
|
||||
}
|
||||
|
||||
for _, p := range tests {
|
||||
got, err := ParseBigBytes(p.in)
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't parse %v: %v", p.in, err)
|
||||
} else {
|
||||
if got.Uint64() != p.exp {
|
||||
t.Errorf("Expected %v for %v, got %v",
|
||||
p.exp, p.in, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBigByteErrors(t *testing.T) {
|
||||
got, err := ParseBigBytes("84 JB")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %v", got)
|
||||
}
|
||||
got, err = ParseBigBytes("")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error parsing nothing")
|
||||
}
|
||||
}
|
||||
|
||||
func bbyte(in uint64) string {
|
||||
return BigBytes((&big.Int{}).SetUint64(in))
|
||||
}
|
||||
|
||||
func bibyte(in uint64) string {
|
||||
return BigIBytes((&big.Int{}).SetUint64(in))
|
||||
}
|
||||
|
||||
func TestBigBytes(t *testing.T) {
|
||||
testList{
|
||||
{"bytes(0)", bbyte(0), "0 B"},
|
||||
{"bytes(1)", bbyte(1), "1 B"},
|
||||
{"bytes(803)", bbyte(803), "803 B"},
|
||||
{"bytes(999)", bbyte(999), "999 B"},
|
||||
|
||||
{"bytes(1024)", bbyte(1024), "1.0 kB"},
|
||||
{"bytes(1MB - 1)", bbyte(MByte - Byte), "1000 kB"},
|
||||
|
||||
{"bytes(1MB)", bbyte(1024 * 1024), "1.0 MB"},
|
||||
{"bytes(1GB - 1K)", bbyte(GByte - KByte), "1000 MB"},
|
||||
|
||||
{"bytes(1GB)", bbyte(GByte), "1.0 GB"},
|
||||
{"bytes(1TB - 1M)", bbyte(TByte - MByte), "1000 GB"},
|
||||
|
||||
{"bytes(1TB)", bbyte(TByte), "1.0 TB"},
|
||||
{"bytes(1PB - 1T)", bbyte(PByte - TByte), "999 TB"},
|
||||
|
||||
{"bytes(1PB)", bbyte(PByte), "1.0 PB"},
|
||||
{"bytes(1PB - 1T)", bbyte(EByte - PByte), "999 PB"},
|
||||
|
||||
{"bytes(1EB)", bbyte(EByte), "1.0 EB"},
|
||||
// Overflows.
|
||||
// {"bytes(1EB - 1P)", Bytes((KByte*EByte)-PByte), "1023EB"},
|
||||
|
||||
{"bytes(0)", bibyte(0), "0 B"},
|
||||
{"bytes(1)", bibyte(1), "1 B"},
|
||||
{"bytes(803)", bibyte(803), "803 B"},
|
||||
{"bytes(1023)", bibyte(1023), "1023 B"},
|
||||
|
||||
{"bytes(1024)", bibyte(1024), "1.0 KiB"},
|
||||
{"bytes(1MB - 1)", bibyte(MiByte - IByte), "1024 KiB"},
|
||||
|
||||
{"bytes(1MB)", bibyte(1024 * 1024), "1.0 MiB"},
|
||||
{"bytes(1GB - 1K)", bibyte(GiByte - KiByte), "1024 MiB"},
|
||||
|
||||
{"bytes(1GB)", bibyte(GiByte), "1.0 GiB"},
|
||||
{"bytes(1TB - 1M)", bibyte(TiByte - MiByte), "1024 GiB"},
|
||||
|
||||
{"bytes(1TB)", bibyte(TiByte), "1.0 TiB"},
|
||||
{"bytes(1PB - 1T)", bibyte(PiByte - TiByte), "1023 TiB"},
|
||||
|
||||
{"bytes(1PB)", bibyte(PiByte), "1.0 PiB"},
|
||||
{"bytes(1PB - 1T)", bibyte(EiByte - PiByte), "1023 PiB"},
|
||||
|
||||
{"bytes(1EiB)", bibyte(EiByte), "1.0 EiB"},
|
||||
// Overflows.
|
||||
// {"bytes(1EB - 1P)", bibyte((KIByte*EIByte)-PiByte), "1023EB"},
|
||||
|
||||
{"bytes(5.5GiB)", bibyte(5.5 * GiByte), "5.5 GiB"},
|
||||
|
||||
{"bytes(5.5GB)", bbyte(5.5 * GByte), "5.5 GB"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func TestVeryBigBytes(t *testing.T) {
|
||||
b, _ := (&big.Int{}).SetString("15347691069326346944512", 10)
|
||||
s := BigBytes(b)
|
||||
if s != "15 ZB" {
|
||||
t.Errorf("Expected 15 ZB, got %v", s)
|
||||
}
|
||||
s = BigIBytes(b)
|
||||
if s != "13 ZiB" {
|
||||
t.Errorf("Expected 13 ZiB, got %v", s)
|
||||
}
|
||||
|
||||
b, _ = (&big.Int{}).SetString("15716035654990179271180288", 10)
|
||||
s = BigBytes(b)
|
||||
if s != "16 YB" {
|
||||
t.Errorf("Expected 16 YB, got %v", s)
|
||||
}
|
||||
s = BigIBytes(b)
|
||||
if s != "13 YiB" {
|
||||
t.Errorf("Expected 13 YiB, got %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVeryVeryBigBytes(t *testing.T) {
|
||||
b, _ := (&big.Int{}).SetString("16093220510709943573688614912", 10)
|
||||
s := BigBytes(b)
|
||||
if s != "16093 YB" {
|
||||
t.Errorf("Expected 16093 YB, got %v", s)
|
||||
}
|
||||
s = BigIBytes(b)
|
||||
if s != "13312 YiB" {
|
||||
t.Errorf("Expected 13312 YiB, got %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVeryBig(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"16 ZB", "16000000000000000000000"},
|
||||
{"16 ZiB", "18889465931478580854784"},
|
||||
{"16.5 ZB", "16500000000000000000000"},
|
||||
{"16.5 ZiB", "19479761741837286506496"},
|
||||
{"16 Z", "16000000000000000000000"},
|
||||
{"16 Zi", "18889465931478580854784"},
|
||||
{"16.5 Z", "16500000000000000000000"},
|
||||
{"16.5 Zi", "19479761741837286506496"},
|
||||
|
||||
{"16 YB", "16000000000000000000000000"},
|
||||
{"16 YiB", "19342813113834066795298816"},
|
||||
{"16.5 YB", "16500000000000000000000000"},
|
||||
{"16.5 YiB", "19947276023641381382651904"},
|
||||
{"16 Y", "16000000000000000000000000"},
|
||||
{"16 Yi", "19342813113834066795298816"},
|
||||
{"16.5 Y", "16500000000000000000000000"},
|
||||
{"16.5 Yi", "19947276023641381382651904"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
x, err := ParseBigBytes(test.in)
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing %q: %v", test.in, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if x.String() != test.out {
|
||||
t.Errorf("Expected %q for %q, got %v", test.out, test.in, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseBigBytes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseBigBytes("16.5 Z")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigBytes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
bibyte(16.5 * GByte)
|
||||
}
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// IEC Sizes.
|
||||
// kibis of bits
|
||||
const (
|
||||
Byte = 1 << (iota * 10)
|
||||
KiByte
|
||||
MiByte
|
||||
GiByte
|
||||
TiByte
|
||||
PiByte
|
||||
EiByte
|
||||
)
|
||||
|
||||
// SI Sizes.
|
||||
const (
|
||||
IByte = 1
|
||||
KByte = IByte * 1000
|
||||
MByte = KByte * 1000
|
||||
GByte = MByte * 1000
|
||||
TByte = GByte * 1000
|
||||
PByte = TByte * 1000
|
||||
EByte = PByte * 1000
|
||||
)
|
||||
|
||||
var bytesSizeTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"kib": KiByte,
|
||||
"kb": KByte,
|
||||
"mib": MiByte,
|
||||
"mb": MByte,
|
||||
"gib": GiByte,
|
||||
"gb": GByte,
|
||||
"tib": TiByte,
|
||||
"tb": TByte,
|
||||
"pib": PiByte,
|
||||
"pb": PByte,
|
||||
"eib": EiByte,
|
||||
"eb": EByte,
|
||||
// Without suffix
|
||||
"": Byte,
|
||||
"ki": KiByte,
|
||||
"k": KByte,
|
||||
"mi": MiByte,
|
||||
"m": MByte,
|
||||
"gi": GiByte,
|
||||
"g": GByte,
|
||||
"ti": TiByte,
|
||||
"t": TByte,
|
||||
"pi": PiByte,
|
||||
"p": PByte,
|
||||
"ei": EiByte,
|
||||
"e": EByte,
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
// Bytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// Bytes(82854982) -> 83 MB
|
||||
func Bytes(s uint64) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
return humanateBytes(s, 1000, sizes)
|
||||
}
|
||||
|
||||
// IBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// IBytes(82854982) -> 79 MiB
|
||||
func IBytes(s uint64) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
return humanateBytes(s, 1024, sizes)
|
||||
}
|
||||
|
||||
// ParseBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See Also: Bytes, IBytes.
|
||||
//
|
||||
// ParseBytes("42 MB") -> 42000000, nil
|
||||
// ParseBytes("42 mib") -> 44040192, nil
|
||||
func ParseBytes(s string) (uint64, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(num, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bytesSizeTable[extra]; ok {
|
||||
f *= float64(m)
|
||||
if f >= math.MaxUint64 {
|
||||
return 0, fmt.Errorf("too large: %v", s)
|
||||
}
|
||||
return uint64(f), nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestByteParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
exp uint64
|
||||
}{
|
||||
{"42", 42},
|
||||
{"42MB", 42000000},
|
||||
{"42MiB", 44040192},
|
||||
{"42mb", 42000000},
|
||||
{"42mib", 44040192},
|
||||
{"42MIB", 44040192},
|
||||
{"42 MB", 42000000},
|
||||
{"42 MiB", 44040192},
|
||||
{"42 mb", 42000000},
|
||||
{"42 mib", 44040192},
|
||||
{"42 MIB", 44040192},
|
||||
{"42.5MB", 42500000},
|
||||
{"42.5MiB", 44564480},
|
||||
{"42.5 MB", 42500000},
|
||||
{"42.5 MiB", 44564480},
|
||||
// No need to say B
|
||||
{"42M", 42000000},
|
||||
{"42Mi", 44040192},
|
||||
{"42m", 42000000},
|
||||
{"42mi", 44040192},
|
||||
{"42MI", 44040192},
|
||||
{"42 M", 42000000},
|
||||
{"42 Mi", 44040192},
|
||||
{"42 m", 42000000},
|
||||
{"42 mi", 44040192},
|
||||
{"42 MI", 44040192},
|
||||
{"42.5M", 42500000},
|
||||
{"42.5Mi", 44564480},
|
||||
{"42.5 M", 42500000},
|
||||
{"42.5 Mi", 44564480},
|
||||
// Bug #42
|
||||
{"1,005.03 MB", 1005030000},
|
||||
// Large testing, breaks when too much larger than
|
||||
// this.
|
||||
{"12.5 EB", uint64(12.5 * float64(EByte))},
|
||||
{"12.5 E", uint64(12.5 * float64(EByte))},
|
||||
{"12.5 EiB", uint64(12.5 * float64(EiByte))},
|
||||
}
|
||||
|
||||
for _, p := range tests {
|
||||
got, err := ParseBytes(p.in)
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't parse %v: %v", p.in, err)
|
||||
}
|
||||
if got != p.exp {
|
||||
t.Errorf("Expected %v for %v, got %v",
|
||||
p.exp, p.in, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestByteErrors(t *testing.T) {
|
||||
got, err := ParseBytes("84 JB")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %v", got)
|
||||
}
|
||||
got, err = ParseBytes("")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error parsing nothing")
|
||||
}
|
||||
got, err = ParseBytes("16 EiB")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytes(t *testing.T) {
|
||||
testList{
|
||||
{"bytes(0)", Bytes(0), "0 B"},
|
||||
{"bytes(1)", Bytes(1), "1 B"},
|
||||
{"bytes(803)", Bytes(803), "803 B"},
|
||||
{"bytes(999)", Bytes(999), "999 B"},
|
||||
|
||||
{"bytes(1024)", Bytes(1024), "1.0 kB"},
|
||||
{"bytes(9999)", Bytes(9999), "10 kB"},
|
||||
{"bytes(1MB - 1)", Bytes(MByte - Byte), "1000 kB"},
|
||||
|
||||
{"bytes(1MB)", Bytes(1024 * 1024), "1.0 MB"},
|
||||
{"bytes(1GB - 1K)", Bytes(GByte - KByte), "1000 MB"},
|
||||
|
||||
{"bytes(1GB)", Bytes(GByte), "1.0 GB"},
|
||||
{"bytes(1TB - 1M)", Bytes(TByte - MByte), "1000 GB"},
|
||||
{"bytes(10MB)", Bytes(9999 * 1000), "10 MB"},
|
||||
|
||||
{"bytes(1TB)", Bytes(TByte), "1.0 TB"},
|
||||
{"bytes(1PB - 1T)", Bytes(PByte - TByte), "999 TB"},
|
||||
|
||||
{"bytes(1PB)", Bytes(PByte), "1.0 PB"},
|
||||
{"bytes(1PB - 1T)", Bytes(EByte - PByte), "999 PB"},
|
||||
|
||||
{"bytes(1EB)", Bytes(EByte), "1.0 EB"},
|
||||
// Overflows.
|
||||
// {"bytes(1EB - 1P)", Bytes((KByte*EByte)-PByte), "1023EB"},
|
||||
|
||||
{"bytes(0)", IBytes(0), "0 B"},
|
||||
{"bytes(1)", IBytes(1), "1 B"},
|
||||
{"bytes(803)", IBytes(803), "803 B"},
|
||||
{"bytes(1023)", IBytes(1023), "1023 B"},
|
||||
|
||||
{"bytes(1024)", IBytes(1024), "1.0 KiB"},
|
||||
{"bytes(1MB - 1)", IBytes(MiByte - IByte), "1024 KiB"},
|
||||
|
||||
{"bytes(1MB)", IBytes(1024 * 1024), "1.0 MiB"},
|
||||
{"bytes(1GB - 1K)", IBytes(GiByte - KiByte), "1024 MiB"},
|
||||
|
||||
{"bytes(1GB)", IBytes(GiByte), "1.0 GiB"},
|
||||
{"bytes(1TB - 1M)", IBytes(TiByte - MiByte), "1024 GiB"},
|
||||
|
||||
{"bytes(1TB)", IBytes(TiByte), "1.0 TiB"},
|
||||
{"bytes(1PB - 1T)", IBytes(PiByte - TiByte), "1023 TiB"},
|
||||
|
||||
{"bytes(1PB)", IBytes(PiByte), "1.0 PiB"},
|
||||
{"bytes(1PB - 1T)", IBytes(EiByte - PiByte), "1023 PiB"},
|
||||
|
||||
{"bytes(1EiB)", IBytes(EiByte), "1.0 EiB"},
|
||||
// Overflows.
|
||||
// {"bytes(1EB - 1P)", IBytes((KIByte*EIByte)-PiByte), "1023EB"},
|
||||
|
||||
{"bytes(5.5GiB)", IBytes(5.5 * GiByte), "5.5 GiB"},
|
||||
|
||||
{"bytes(5.5GB)", Bytes(5.5 * GByte), "5.5 GB"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func BenchmarkParseBytes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseBytes("16.5 GB")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBytes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Bytes(16.5 * GByte)
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Comma produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Comma(834142) -> 834,142
|
||||
func Comma(v int64) string {
|
||||
sign := ""
|
||||
|
||||
// minin64 can't be negated to a usable value, so it has to be special cased.
|
||||
if v == math.MinInt64 {
|
||||
return "-9,223,372,036,854,775,808"
|
||||
}
|
||||
|
||||
if v < 0 {
|
||||
sign = "-"
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
parts := []string{"", "", "", "", "", "", ""}
|
||||
j := len(parts) - 1
|
||||
|
||||
for v > 999 {
|
||||
parts[j] = strconv.FormatInt(v%1000, 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
v = v / 1000
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(v))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
|
||||
// Commaf produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Commaf(834142.32) -> 834,142.32
|
||||
func Commaf(v float64) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// BigComma produces a string form of the given big.Int in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigComma(b *big.Int) string {
|
||||
sign := ""
|
||||
if b.Sign() < 0 {
|
||||
sign = "-"
|
||||
b.Abs(b)
|
||||
}
|
||||
|
||||
athousand := big.NewInt(1000)
|
||||
c := (&big.Int{}).Set(b)
|
||||
_, m := oom(c, athousand)
|
||||
parts := make([]string, m+1)
|
||||
j := len(parts) - 1
|
||||
|
||||
mod := &big.Int{}
|
||||
for b.Cmp(athousand) >= 0 {
|
||||
b.DivMod(b, athousand, mod)
|
||||
parts[j] = strconv.FormatInt(mod.Int64(), 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(b.Int64()))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommas(t *testing.T) {
|
||||
testList{
|
||||
{"0", Comma(0), "0"},
|
||||
{"10", Comma(10), "10"},
|
||||
{"100", Comma(100), "100"},
|
||||
{"1,000", Comma(1000), "1,000"},
|
||||
{"10,000", Comma(10000), "10,000"},
|
||||
{"100,000", Comma(100000), "100,000"},
|
||||
{"10,000,000", Comma(10000000), "10,000,000"},
|
||||
{"10,100,000", Comma(10100000), "10,100,000"},
|
||||
{"10,010,000", Comma(10010000), "10,010,000"},
|
||||
{"10,001,000", Comma(10001000), "10,001,000"},
|
||||
{"123,456,789", Comma(123456789), "123,456,789"},
|
||||
{"maxint", Comma(9.223372e+18), "9,223,372,000,000,000,000"},
|
||||
{"math.maxint", Comma(math.MaxInt64), "9,223,372,036,854,775,807"},
|
||||
{"math.minint", Comma(math.MinInt64), "-9,223,372,036,854,775,808"},
|
||||
{"minint", Comma(-9.223372e+18), "-9,223,372,000,000,000,000"},
|
||||
{"-123,456,789", Comma(-123456789), "-123,456,789"},
|
||||
{"-10,100,000", Comma(-10100000), "-10,100,000"},
|
||||
{"-10,010,000", Comma(-10010000), "-10,010,000"},
|
||||
{"-10,001,000", Comma(-10001000), "-10,001,000"},
|
||||
{"-10,000,000", Comma(-10000000), "-10,000,000"},
|
||||
{"-100,000", Comma(-100000), "-100,000"},
|
||||
{"-10,000", Comma(-10000), "-10,000"},
|
||||
{"-1,000", Comma(-1000), "-1,000"},
|
||||
{"-100", Comma(-100), "-100"},
|
||||
{"-10", Comma(-10), "-10"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func TestCommafs(t *testing.T) {
|
||||
testList{
|
||||
{"0", Commaf(0), "0"},
|
||||
{"10.11", Commaf(10.11), "10.11"},
|
||||
{"100", Commaf(100), "100"},
|
||||
{"1,000", Commaf(1000), "1,000"},
|
||||
{"10,000", Commaf(10000), "10,000"},
|
||||
{"100,000", Commaf(100000), "100,000"},
|
||||
{"834,142.32", Commaf(834142.32), "834,142.32"},
|
||||
{"10,000,000", Commaf(10000000), "10,000,000"},
|
||||
{"10,100,000", Commaf(10100000), "10,100,000"},
|
||||
{"10,010,000", Commaf(10010000), "10,010,000"},
|
||||
{"10,001,000", Commaf(10001000), "10,001,000"},
|
||||
{"123,456,789", Commaf(123456789), "123,456,789"},
|
||||
{"maxf64", Commaf(math.MaxFloat64), "179,769,313,486,231,570,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000"},
|
||||
{"minf64", Commaf(math.SmallestNonzeroFloat64), "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005"},
|
||||
{"-123,456,789", Commaf(-123456789), "-123,456,789"},
|
||||
{"-10,100,000", Commaf(-10100000), "-10,100,000"},
|
||||
{"-10,010,000", Commaf(-10010000), "-10,010,000"},
|
||||
{"-10,001,000", Commaf(-10001000), "-10,001,000"},
|
||||
{"-10,000,000", Commaf(-10000000), "-10,000,000"},
|
||||
{"-100,000", Commaf(-100000), "-100,000"},
|
||||
{"-10,000", Commaf(-10000), "-10,000"},
|
||||
{"-1,000", Commaf(-1000), "-1,000"},
|
||||
{"-100.11", Commaf(-100.11), "-100.11"},
|
||||
{"-10", Commaf(-10), "-10"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func BenchmarkCommas(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Comma(1234567890)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCommaf(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Commaf(1234567890.83584)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBigCommas(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
BigComma(big.NewInt(1234567890))
|
||||
}
|
||||
}
|
||||
|
||||
func bigComma(i int64) string {
|
||||
return BigComma(big.NewInt(i))
|
||||
}
|
||||
|
||||
func TestBigCommas(t *testing.T) {
|
||||
testList{
|
||||
{"0", bigComma(0), "0"},
|
||||
{"10", bigComma(10), "10"},
|
||||
{"100", bigComma(100), "100"},
|
||||
{"1,000", bigComma(1000), "1,000"},
|
||||
{"10,000", bigComma(10000), "10,000"},
|
||||
{"100,000", bigComma(100000), "100,000"},
|
||||
{"10,000,000", bigComma(10000000), "10,000,000"},
|
||||
{"10,100,000", bigComma(10100000), "10,100,000"},
|
||||
{"10,010,000", bigComma(10010000), "10,010,000"},
|
||||
{"10,001,000", bigComma(10001000), "10,001,000"},
|
||||
{"123,456,789", bigComma(123456789), "123,456,789"},
|
||||
{"maxint", bigComma(9.223372e+18), "9,223,372,000,000,000,000"},
|
||||
{"minint", bigComma(-9.223372e+18), "-9,223,372,000,000,000,000"},
|
||||
{"-123,456,789", bigComma(-123456789), "-123,456,789"},
|
||||
{"-10,100,000", bigComma(-10100000), "-10,100,000"},
|
||||
{"-10,010,000", bigComma(-10010000), "-10,010,000"},
|
||||
{"-10,001,000", bigComma(-10001000), "-10,001,000"},
|
||||
{"-10,000,000", bigComma(-10000000), "-10,000,000"},
|
||||
{"-100,000", bigComma(-100000), "-100,000"},
|
||||
{"-10,000", bigComma(-10000), "-10,000"},
|
||||
{"-1,000", bigComma(-1000), "-1,000"},
|
||||
{"-100", bigComma(-100), "-100"},
|
||||
{"-10", bigComma(-10), "-10"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func TestVeryBigCommas(t *testing.T) {
|
||||
tests := []struct{ in, exp string }{
|
||||
{
|
||||
"84889279597249724975972597249849757294578485",
|
||||
"84,889,279,597,249,724,975,972,597,249,849,757,294,578,485",
|
||||
},
|
||||
{
|
||||
"-84889279597249724975972597249849757294578485",
|
||||
"-84,889,279,597,249,724,975,972,597,249,849,757,294,578,485",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
n, _ := (&big.Int{}).SetString(test.in, 10)
|
||||
got := BigComma(n)
|
||||
if test.exp != got {
|
||||
t.Errorf("Expected %q, got %q", test.exp, got)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
// +build go1.6
|
||||
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BigCommaf produces a string form of the given big.Float in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigCommaf(v *big.Float) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v.Sign() < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v.Abs(v)
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(v.Text('f', -1), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
// +build go1.6
|
||||
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkBigCommaf(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Commaf(1234567890.83584)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBigCommafs(t *testing.T) {
|
||||
testList{
|
||||
{"0", BigCommaf(big.NewFloat(0)), "0"},
|
||||
{"10.11", BigCommaf(big.NewFloat(10.11)), "10.11"},
|
||||
{"100", BigCommaf(big.NewFloat(100)), "100"},
|
||||
{"1,000", BigCommaf(big.NewFloat(1000)), "1,000"},
|
||||
{"10,000", BigCommaf(big.NewFloat(10000)), "10,000"},
|
||||
{"100,000", BigCommaf(big.NewFloat(100000)), "100,000"},
|
||||
{"834,142.32", BigCommaf(big.NewFloat(834142.32)), "834,142.32"},
|
||||
{"10,000,000", BigCommaf(big.NewFloat(10000000)), "10,000,000"},
|
||||
{"10,100,000", BigCommaf(big.NewFloat(10100000)), "10,100,000"},
|
||||
{"10,010,000", BigCommaf(big.NewFloat(10010000)), "10,010,000"},
|
||||
{"10,001,000", BigCommaf(big.NewFloat(10001000)), "10,001,000"},
|
||||
{"123,456,789", BigCommaf(big.NewFloat(123456789)), "123,456,789"},
|
||||
{"maxf64", BigCommaf(big.NewFloat(math.MaxFloat64)), "179,769,313,486,231,570,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000"},
|
||||
{"minf64", BigCommaf(big.NewFloat(math.SmallestNonzeroFloat64)), "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004940656458412465"},
|
||||
{"-123,456,789", BigCommaf(big.NewFloat(-123456789)), "-123,456,789"},
|
||||
{"-10,100,000", BigCommaf(big.NewFloat(-10100000)), "-10,100,000"},
|
||||
{"-10,010,000", BigCommaf(big.NewFloat(-10010000)), "-10,010,000"},
|
||||
{"-10,001,000", BigCommaf(big.NewFloat(-10001000)), "-10,001,000"},
|
||||
{"-10,000,000", BigCommaf(big.NewFloat(-10000000)), "-10,000,000"},
|
||||
{"-100,000", BigCommaf(big.NewFloat(-100000)), "-100,000"},
|
||||
{"-10,000", BigCommaf(big.NewFloat(-10000)), "-10,000"},
|
||||
{"-1,000", BigCommaf(big.NewFloat(-1000)), "-1,000"},
|
||||
{"-100.11", BigCommaf(big.NewFloat(-100.11)), "-100.11"},
|
||||
{"-10", BigCommaf(big.NewFloat(-10)), "-10"},
|
||||
}.validate(t)
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testList []struct {
|
||||
name, got, exp string
|
||||
}
|
||||
|
||||
func (tl testList) validate(t *testing.T) {
|
||||
for _, test := range tl {
|
||||
if test.got != test.exp {
|
||||
t.Errorf("On %v, expected '%v', but got '%v'",
|
||||
test.name, test.exp, test.got)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import "strconv"
|
||||
|
||||
func stripTrailingZeros(s string) string {
|
||||
offset := len(s) - 1
|
||||
for offset > 0 {
|
||||
if s[offset] == '.' {
|
||||
offset--
|
||||
break
|
||||
}
|
||||
if s[offset] != '0' {
|
||||
break
|
||||
}
|
||||
offset--
|
||||
}
|
||||
return s[:offset+1]
|
||||
}
|
||||
|
||||
// Ftoa converts a float to a string with no trailing zeros.
|
||||
func Ftoa(num float64) string {
|
||||
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFtoa(t *testing.T) {
|
||||
testList{
|
||||
{"200", Ftoa(200), "200"},
|
||||
{"2", Ftoa(2), "2"},
|
||||
{"2.2", Ftoa(2.2), "2.2"},
|
||||
{"2.02", Ftoa(2.02), "2.02"},
|
||||
{"200.02", Ftoa(200.02), "200.02"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func BenchmarkFtoaRegexTrailing(b *testing.B) {
|
||||
trailingZerosRegex := regexp.MustCompile(`\.?0+$`)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
trailingZerosRegex.ReplaceAllString("2.00000", "")
|
||||
trailingZerosRegex.ReplaceAllString("2.0000", "")
|
||||
trailingZerosRegex.ReplaceAllString("2.000", "")
|
||||
trailingZerosRegex.ReplaceAllString("2.00", "")
|
||||
trailingZerosRegex.ReplaceAllString("2.0", "")
|
||||
trailingZerosRegex.ReplaceAllString("2", "")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFtoaFunc(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
stripTrailingZeros("2.00000")
|
||||
stripTrailingZeros("2.0000")
|
||||
stripTrailingZeros("2.000")
|
||||
stripTrailingZeros("2.00")
|
||||
stripTrailingZeros("2.0")
|
||||
stripTrailingZeros("2")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFmtF(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("%f", 2.03584)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStrconvF(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
strconv.FormatFloat(2.03584, 'f', 6, 64)
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
Package humanize converts boring ugly numbers to human-friendly strings and back.
|
||||
|
||||
Durations can be turned into strings such as "3 days ago", numbers
|
||||
representing sizes like 82854982 into useful strings like, "83 MB" or
|
||||
"79 MiB" (whichever you prefer).
|
||||
*/
|
||||
package humanize
|
|
@ -1,192 +0,0 @@
|
|||
package humanize
|
||||
|
||||
/*
|
||||
Slightly adapted from the source to fit go-humanize.
|
||||
|
||||
Author: https://github.com/gorhill
|
||||
Source: https://gist.github.com/gorhill/5285193
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
renderFloatPrecisionMultipliers = [...]float64{
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
100000,
|
||||
1000000,
|
||||
10000000,
|
||||
100000000,
|
||||
1000000000,
|
||||
}
|
||||
|
||||
renderFloatPrecisionRounders = [...]float64{
|
||||
0.5,
|
||||
0.05,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.00005,
|
||||
0.000005,
|
||||
0.0000005,
|
||||
0.00000005,
|
||||
0.000000005,
|
||||
0.0000000005,
|
||||
}
|
||||
)
|
||||
|
||||
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
|
||||
// * thousands separator
|
||||
// * decimal separator
|
||||
// * decimal precision
|
||||
//
|
||||
// Usage: s := RenderFloat(format, n)
|
||||
// The format parameter tells how to render the number n.
|
||||
//
|
||||
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
|
||||
//
|
||||
// Examples of format strings, given n = 12345.6789:
|
||||
// "#,###.##" => "12,345.67"
|
||||
// "#,###." => "12,345"
|
||||
// "#,###" => "12345,678"
|
||||
// "#\u202F###,##" => "12 345,68"
|
||||
// "#.###,###### => 12.345,678900
|
||||
// "" (aka default format) => 12,345.67
|
||||
//
|
||||
// The highest precision allowed is 9 digits after the decimal symbol.
|
||||
// There is also a version for integer number, FormatInteger(),
|
||||
// which is convenient for calls within template.
|
||||
func FormatFloat(format string, n float64) string {
|
||||
// Special cases:
|
||||
// NaN = "NaN"
|
||||
// +Inf = "+Infinity"
|
||||
// -Inf = "-Infinity"
|
||||
if math.IsNaN(n) {
|
||||
return "NaN"
|
||||
}
|
||||
if n > math.MaxFloat64 {
|
||||
return "Infinity"
|
||||
}
|
||||
if n < -math.MaxFloat64 {
|
||||
return "-Infinity"
|
||||
}
|
||||
|
||||
// default format
|
||||
precision := 2
|
||||
decimalStr := "."
|
||||
thousandStr := ","
|
||||
positiveStr := ""
|
||||
negativeStr := "-"
|
||||
|
||||
if len(format) > 0 {
|
||||
format := []rune(format)
|
||||
|
||||
// If there is an explicit format directive,
|
||||
// then default values are these:
|
||||
precision = 9
|
||||
thousandStr = ""
|
||||
|
||||
// collect indices of meaningful formatting directives
|
||||
formatIndx := []int{}
|
||||
for i, char := range format {
|
||||
if char != '#' && char != '0' {
|
||||
formatIndx = append(formatIndx, i)
|
||||
}
|
||||
}
|
||||
|
||||
if len(formatIndx) > 0 {
|
||||
// Directive at index 0:
|
||||
// Must be a '+'
|
||||
// Raise an error if not the case
|
||||
// index: 0123456789
|
||||
// +0.000,000
|
||||
// +000,000.0
|
||||
// +0000.00
|
||||
// +0000
|
||||
if formatIndx[0] == 0 {
|
||||
if format[formatIndx[0]] != '+' {
|
||||
panic("RenderFloat(): invalid positive sign directive")
|
||||
}
|
||||
positiveStr = "+"
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// Two directives:
|
||||
// First is thousands separator
|
||||
// Raise an error if not followed by 3-digit
|
||||
// 0123456789
|
||||
// 0.000,000
|
||||
// 000,000.00
|
||||
if len(formatIndx) == 2 {
|
||||
if (formatIndx[1] - formatIndx[0]) != 4 {
|
||||
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
|
||||
}
|
||||
thousandStr = string(format[formatIndx[0]])
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// One directive:
|
||||
// Directive is decimal separator
|
||||
// The number of digit-specifier following the separator indicates wanted precision
|
||||
// 0123456789
|
||||
// 0.00
|
||||
// 000,0000
|
||||
if len(formatIndx) == 1 {
|
||||
decimalStr = string(format[formatIndx[0]])
|
||||
precision = len(format) - formatIndx[0] - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate sign part
|
||||
var signStr string
|
||||
if n >= 0.000000001 {
|
||||
signStr = positiveStr
|
||||
} else if n <= -0.000000001 {
|
||||
signStr = negativeStr
|
||||
n = -n
|
||||
} else {
|
||||
signStr = ""
|
||||
n = 0.0
|
||||
}
|
||||
|
||||
// split number into integer and fractional parts
|
||||
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
|
||||
|
||||
// generate integer part string
|
||||
intStr := strconv.FormatInt(int64(intf), 10)
|
||||
|
||||
// add thousand separator if required
|
||||
if len(thousandStr) > 0 {
|
||||
for i := len(intStr); i > 3; {
|
||||
i -= 3
|
||||
intStr = intStr[:i] + thousandStr + intStr[i:]
|
||||
}
|
||||
}
|
||||
|
||||
// no fractional part, we can leave now
|
||||
if precision == 0 {
|
||||
return signStr + intStr
|
||||
}
|
||||
|
||||
// generate fractional part
|
||||
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
|
||||
// may need padding
|
||||
if len(fracStr) < precision {
|
||||
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
|
||||
}
|
||||
|
||||
return signStr + intStr + decimalStr + fracStr
|
||||
}
|
||||
|
||||
// FormatInteger produces a formatted number as string.
|
||||
// See FormatFloat.
|
||||
func FormatInteger(format string, n int) string {
|
||||
return FormatFloat(format, float64(n))
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TestStruct struct {
|
||||
name string
|
||||
format string
|
||||
num float64
|
||||
formatted string
|
||||
}
|
||||
|
||||
func TestFormatFloat(t *testing.T) {
|
||||
tests := []TestStruct{
|
||||
{"default", "", 12345.6789, "12,345.68"},
|
||||
{"#", "#", 12345.6789, "12345.678900000"},
|
||||
{"#.", "#.", 12345.6789, "12346"},
|
||||
{"#,#", "#,#", 12345.6789, "12345,7"},
|
||||
{"#,##", "#,##", 12345.6789, "12345,68"},
|
||||
{"#,###", "#,###", 12345.6789, "12345,679"},
|
||||
{"#,###.", "#,###.", 12345.6789, "12,346"},
|
||||
{"#,###.##", "#,###.##", 12345.6789, "12,345.68"},
|
||||
{"#,###.###", "#,###.###", 12345.6789, "12,345.679"},
|
||||
{"#,###.####", "#,###.####", 12345.6789, "12,345.6789"},
|
||||
{"#.###,######", "#.###,######", 12345.6789, "12.345,678900"},
|
||||
{"bug46", "#,###.##", 52746220055.92342, "52,746,220,055.92"},
|
||||
{"#\u202f###,##", "#\u202f###,##", 12345.6789, "12 345,68"},
|
||||
|
||||
// special cases
|
||||
{"NaN", "#", math.NaN(), "NaN"},
|
||||
{"+Inf", "#", math.Inf(1), "Infinity"},
|
||||
{"-Inf", "#", math.Inf(-1), "-Infinity"},
|
||||
{"signStr <= -0.000000001", "", -0.000000002, "-0.00"},
|
||||
{"signStr = 0", "", 0, "0.00"},
|
||||
{"Format directive must start with +", "+000", 12345.6789, "+12345.678900000"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got := FormatFloat(test.format, test.num)
|
||||
if got != test.formatted {
|
||||
t.Errorf("On %v (%v, %v), got %v, wanted %v",
|
||||
test.name, test.format, test.num, got, test.formatted)
|
||||
}
|
||||
}
|
||||
// Test a single integer
|
||||
got := FormatInteger("#", 12345)
|
||||
if got != "12345.000000000" {
|
||||
t.Errorf("On %v (%v, %v), got %v, wanted %v",
|
||||
"integerTest", "#", 12345, got, "12345.000000000")
|
||||
}
|
||||
// Test the things that could panic
|
||||
panictests := []TestStruct{
|
||||
{"RenderFloat(): invalid positive sign directive", "-", 12345.6789, "12,345.68"},
|
||||
{"RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers", "0.01", 12345.6789, "12,345.68"},
|
||||
}
|
||||
for _, test := range panictests {
|
||||
didPanic := false
|
||||
var message interface{}
|
||||
func() {
|
||||
|
||||
defer func() {
|
||||
if message = recover(); message != nil {
|
||||
didPanic = true
|
||||
}
|
||||
}()
|
||||
|
||||
// call the target function
|
||||
_ = FormatFloat(test.format, test.num)
|
||||
|
||||
}()
|
||||
if didPanic != true {
|
||||
t.Errorf("On %v, should have panic and did not.",
|
||||
test.name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import "strconv"
|
||||
|
||||
// Ordinal gives you the input number in a rank/ordinal format.
|
||||
//
|
||||
// Ordinal(3) -> 3rd
|
||||
func Ordinal(x int) string {
|
||||
suffix := "th"
|
||||
switch x % 10 {
|
||||
case 1:
|
||||
if x%100 != 11 {
|
||||
suffix = "st"
|
||||
}
|
||||
case 2:
|
||||
if x%100 != 12 {
|
||||
suffix = "nd"
|
||||
}
|
||||
case 3:
|
||||
if x%100 != 13 {
|
||||
suffix = "rd"
|
||||
}
|
||||
}
|
||||
return strconv.Itoa(x) + suffix
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOrdinals(t *testing.T) {
|
||||
testList{
|
||||
{"0", Ordinal(0), "0th"},
|
||||
{"1", Ordinal(1), "1st"},
|
||||
{"2", Ordinal(2), "2nd"},
|
||||
{"3", Ordinal(3), "3rd"},
|
||||
{"4", Ordinal(4), "4th"},
|
||||
{"10", Ordinal(10), "10th"},
|
||||
{"11", Ordinal(11), "11th"},
|
||||
{"12", Ordinal(12), "12th"},
|
||||
{"13", Ordinal(13), "13th"},
|
||||
{"101", Ordinal(101), "101st"},
|
||||
{"102", Ordinal(102), "102nd"},
|
||||
{"103", Ordinal(103), "103rd"},
|
||||
}.validate(t)
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var siPrefixTable = map[float64]string{
|
||||
-24: "y", // yocto
|
||||
-21: "z", // zepto
|
||||
-18: "a", // atto
|
||||
-15: "f", // femto
|
||||
-12: "p", // pico
|
||||
-9: "n", // nano
|
||||
-6: "µ", // micro
|
||||
-3: "m", // milli
|
||||
0: "",
|
||||
3: "k", // kilo
|
||||
6: "M", // mega
|
||||
9: "G", // giga
|
||||
12: "T", // tera
|
||||
15: "P", // peta
|
||||
18: "E", // exa
|
||||
21: "Z", // zetta
|
||||
24: "Y", // yotta
|
||||
}
|
||||
|
||||
var revSIPrefixTable = revfmap(siPrefixTable)
|
||||
|
||||
// revfmap reverses the map and precomputes the power multiplier
|
||||
func revfmap(in map[float64]string) map[string]float64 {
|
||||
rv := map[string]float64{}
|
||||
for k, v := range in {
|
||||
rv[v] = math.Pow(10, k)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
var riParseRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
ri := `^([\-0-9.]+)\s?([`
|
||||
for _, v := range siPrefixTable {
|
||||
ri += v
|
||||
}
|
||||
ri += `]?)(.*)`
|
||||
|
||||
riParseRegex = regexp.MustCompile(ri)
|
||||
}
|
||||
|
||||
// ComputeSI finds the most appropriate SI prefix for the given number
|
||||
// and returns the prefix along with the value adjusted to be within
|
||||
// that prefix.
|
||||
//
|
||||
// See also: SI, ParseSI.
|
||||
//
|
||||
// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p")
|
||||
func ComputeSI(input float64) (float64, string) {
|
||||
if input == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
mag := math.Abs(input)
|
||||
exponent := math.Floor(logn(mag, 10))
|
||||
exponent = math.Floor(exponent/3) * 3
|
||||
|
||||
value := mag / math.Pow(10, exponent)
|
||||
|
||||
// Handle special case where value is exactly 1000.0
|
||||
// Should return 1 M instead of 1000 k
|
||||
if value == 1000.0 {
|
||||
exponent += 3
|
||||
value = mag / math.Pow(10, exponent)
|
||||
}
|
||||
|
||||
value = math.Copysign(value, input)
|
||||
|
||||
prefix := siPrefixTable[exponent]
|
||||
return value, prefix
|
||||
}
|
||||
|
||||
// SI returns a string with default formatting.
|
||||
//
|
||||
// SI uses Ftoa to format float value, removing trailing zeros.
|
||||
//
|
||||
// See also: ComputeSI, ParseSI.
|
||||
//
|
||||
// e.g. SI(1000000, "B") -> 1 MB
|
||||
// e.g. SI(2.2345e-12, "F") -> 2.2345 pF
|
||||
func SI(input float64, unit string) string {
|
||||
value, prefix := ComputeSI(input)
|
||||
return Ftoa(value) + " " + prefix + unit
|
||||
}
|
||||
|
||||
var errInvalid = errors.New("invalid input")
|
||||
|
||||
// ParseSI parses an SI string back into the number and unit.
|
||||
//
|
||||
// See also: SI, ComputeSI.
|
||||
//
|
||||
// e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil)
|
||||
func ParseSI(input string) (float64, string, error) {
|
||||
found := riParseRegex.FindStringSubmatch(input)
|
||||
if len(found) != 4 {
|
||||
return 0, "", errInvalid
|
||||
}
|
||||
mag := revSIPrefixTable[found[2]]
|
||||
unit := found[3]
|
||||
|
||||
base, err := strconv.ParseFloat(found[1], 64)
|
||||
return base * mag, unit, err
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
num float64
|
||||
formatted string
|
||||
}{
|
||||
{"e-24", 1e-24, "1 yF"},
|
||||
{"e-21", 1e-21, "1 zF"},
|
||||
{"e-18", 1e-18, "1 aF"},
|
||||
{"e-15", 1e-15, "1 fF"},
|
||||
{"e-12", 1e-12, "1 pF"},
|
||||
{"e-12", 2.2345e-12, "2.2345 pF"},
|
||||
{"e-12", 2.23e-12, "2.23 pF"},
|
||||
{"e-11", 2.23e-11, "22.3 pF"},
|
||||
{"e-10", 2.2e-10, "220 pF"},
|
||||
{"e-9", 2.2e-9, "2.2 nF"},
|
||||
{"e-8", 2.2e-8, "22 nF"},
|
||||
{"e-7", 2.2e-7, "220 nF"},
|
||||
{"e-6", 2.2e-6, "2.2 µF"},
|
||||
{"e-6", 1e-6, "1 µF"},
|
||||
{"e-5", 2.2e-5, "22 µF"},
|
||||
{"e-4", 2.2e-4, "220 µF"},
|
||||
{"e-3", 2.2e-3, "2.2 mF"},
|
||||
{"e-2", 2.2e-2, "22 mF"},
|
||||
{"e-1", 2.2e-1, "220 mF"},
|
||||
{"e+0", 2.2e-0, "2.2 F"},
|
||||
{"e+0", 2.2, "2.2 F"},
|
||||
{"e+1", 2.2e+1, "22 F"},
|
||||
{"0", 0, "0 F"},
|
||||
{"e+1", 22, "22 F"},
|
||||
{"e+2", 2.2e+2, "220 F"},
|
||||
{"e+2", 220, "220 F"},
|
||||
{"e+3", 2.2e+3, "2.2 kF"},
|
||||
{"e+3", 2200, "2.2 kF"},
|
||||
{"e+4", 2.2e+4, "22 kF"},
|
||||
{"e+4", 22000, "22 kF"},
|
||||
{"e+5", 2.2e+5, "220 kF"},
|
||||
{"e+6", 2.2e+6, "2.2 MF"},
|
||||
{"e+6", 1e+6, "1 MF"},
|
||||
{"e+7", 2.2e+7, "22 MF"},
|
||||
{"e+8", 2.2e+8, "220 MF"},
|
||||
{"e+9", 2.2e+9, "2.2 GF"},
|
||||
{"e+10", 2.2e+10, "22 GF"},
|
||||
{"e+11", 2.2e+11, "220 GF"},
|
||||
{"e+12", 2.2e+12, "2.2 TF"},
|
||||
{"e+15", 2.2e+15, "2.2 PF"},
|
||||
{"e+18", 2.2e+18, "2.2 EF"},
|
||||
{"e+21", 2.2e+21, "2.2 ZF"},
|
||||
{"e+24", 2.2e+24, "2.2 YF"},
|
||||
|
||||
// special case
|
||||
{"1F", 1000 * 1000, "1 MF"},
|
||||
{"1F", 1e6, "1 MF"},
|
||||
|
||||
// negative number
|
||||
{"-100 F", -100, "-100 F"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got := SI(test.num, "F")
|
||||
if got != test.formatted {
|
||||
t.Errorf("On %v (%v), got %v, wanted %v",
|
||||
test.name, test.num, got, test.formatted)
|
||||
}
|
||||
|
||||
gotf, gotu, err := ParseSI(test.formatted)
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing %v (%v): %v", test.name, test.formatted, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if math.Abs(1-(gotf/test.num)) > 0.01 {
|
||||
t.Errorf("On %v (%v), got %v, wanted %v (±%v)",
|
||||
test.name, test.formatted, gotf, test.num,
|
||||
math.Abs(1-(gotf/test.num)))
|
||||
}
|
||||
if gotu != "F" {
|
||||
t.Errorf("On %v (%v), expected unit F, got %v",
|
||||
test.name, test.formatted, gotu)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse error
|
||||
gotf, gotu, err := ParseSI("x1.21JW") // 1.21 jigga whats
|
||||
if err == nil {
|
||||
t.Errorf("Expected error on x1.21JW, got %v %v", gotf, gotu)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseSI(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseSI("2.2346ZB")
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Seconds-based time units
|
||||
const (
|
||||
Day = 24 * time.Hour
|
||||
Week = 7 * Day
|
||||
Month = 30 * Day
|
||||
Year = 12 * Month
|
||||
LongTime = 37 * Year
|
||||
)
|
||||
|
||||
// Time formats a time into a relative string.
|
||||
//
|
||||
// Time(someT) -> "3 weeks ago"
|
||||
func Time(then time.Time) string {
|
||||
return RelTime(then, time.Now(), "ago", "from now")
|
||||
}
|
||||
|
||||
// A RelTimeMagnitude struct contains a relative time point at which
|
||||
// the relative format of time will switch to a new format string. A
|
||||
// slice of these in ascending order by their "D" field is passed to
|
||||
// CustomRelTime to format durations.
|
||||
//
|
||||
// The Format field is a string that may contain a "%s" which will be
|
||||
// replaced with the appropriate signed label (e.g. "ago" or "from
|
||||
// now") and a "%d" that will be replaced by the quantity.
|
||||
//
|
||||
// The DivBy field is the amount of time the time difference must be
|
||||
// divided by in order to display correctly.
|
||||
//
|
||||
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
|
||||
// DivBy should be time.Minute so whatever the duration is will be
|
||||
// expressed in minutes.
|
||||
type RelTimeMagnitude struct {
|
||||
D time.Duration
|
||||
Format string
|
||||
DivBy time.Duration
|
||||
}
|
||||
|
||||
var defaultMagnitudes = []RelTimeMagnitude{
|
||||
{time.Second, "now", time.Second},
|
||||
{2 * time.Second, "1 second %s", 1},
|
||||
{time.Minute, "%d seconds %s", time.Second},
|
||||
{2 * time.Minute, "1 minute %s", 1},
|
||||
{time.Hour, "%d minutes %s", time.Minute},
|
||||
{2 * time.Hour, "1 hour %s", 1},
|
||||
{Day, "%d hours %s", time.Hour},
|
||||
{2 * Day, "1 day %s", 1},
|
||||
{Week, "%d days %s", Day},
|
||||
{2 * Week, "1 week %s", 1},
|
||||
{Month, "%d weeks %s", Week},
|
||||
{2 * Month, "1 month %s", 1},
|
||||
{Year, "%d months %s", Month},
|
||||
{18 * Month, "1 year %s", 1},
|
||||
{2 * Year, "2 years %s", 1},
|
||||
{LongTime, "%d years %s", Year},
|
||||
{math.MaxInt64, "a long while %s", 1},
|
||||
}
|
||||
|
||||
// RelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times and two labels. In addition to the generic time
|
||||
// delta string (e.g. 5 minutes), the labels are used applied so that
|
||||
// the label corresponding to the smaller time is applied.
|
||||
//
|
||||
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
|
||||
func RelTime(a, b time.Time, albl, blbl string) string {
|
||||
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
|
||||
}
|
||||
|
||||
// CustomRelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times two labels and a table of relative time formats.
|
||||
// In addition to the generic time delta string (e.g. 5 minutes), the
|
||||
// labels are used applied so that the label corresponding to the
|
||||
// smaller time is applied.
|
||||
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
|
||||
lbl := albl
|
||||
diff := b.Sub(a)
|
||||
|
||||
if a.After(b) {
|
||||
lbl = blbl
|
||||
diff = a.Sub(b)
|
||||
}
|
||||
|
||||
n := sort.Search(len(magnitudes), func(i int) bool {
|
||||
return magnitudes[i].D >= diff
|
||||
})
|
||||
|
||||
if n >= len(magnitudes) {
|
||||
n = len(magnitudes) - 1
|
||||
}
|
||||
mag := magnitudes[n]
|
||||
args := []interface{}{}
|
||||
escaped := false
|
||||
for _, ch := range mag.Format {
|
||||
if escaped {
|
||||
switch ch {
|
||||
case 's':
|
||||
args = append(args, lbl)
|
||||
case 'd':
|
||||
args = append(args, diff/mag.DivBy)
|
||||
}
|
||||
escaped = false
|
||||
} else {
|
||||
escaped = ch == '%'
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(mag.Format, args...)
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPast(t *testing.T) {
|
||||
now := time.Now()
|
||||
testList{
|
||||
{"now", Time(now), "now"},
|
||||
{"1 second ago", Time(now.Add(-1 * time.Second)), "1 second ago"},
|
||||
{"12 seconds ago", Time(now.Add(-12 * time.Second)), "12 seconds ago"},
|
||||
{"30 seconds ago", Time(now.Add(-30 * time.Second)), "30 seconds ago"},
|
||||
{"45 seconds ago", Time(now.Add(-45 * time.Second)), "45 seconds ago"},
|
||||
{"1 minute ago", Time(now.Add(-63 * time.Second)), "1 minute ago"},
|
||||
{"15 minutes ago", Time(now.Add(-15 * time.Minute)), "15 minutes ago"},
|
||||
{"1 hour ago", Time(now.Add(-63 * time.Minute)), "1 hour ago"},
|
||||
{"2 hours ago", Time(now.Add(-2 * time.Hour)), "2 hours ago"},
|
||||
{"21 hours ago", Time(now.Add(-21 * time.Hour)), "21 hours ago"},
|
||||
{"1 day ago", Time(now.Add(-26 * time.Hour)), "1 day ago"},
|
||||
{"2 days ago", Time(now.Add(-49 * time.Hour)), "2 days ago"},
|
||||
{"3 days ago", Time(now.Add(-3 * Day)), "3 days ago"},
|
||||
{"1 week ago (1)", Time(now.Add(-7 * Day)), "1 week ago"},
|
||||
{"1 week ago (2)", Time(now.Add(-12 * Day)), "1 week ago"},
|
||||
{"2 weeks ago", Time(now.Add(-15 * Day)), "2 weeks ago"},
|
||||
{"1 month ago", Time(now.Add(-39 * Day)), "1 month ago"},
|
||||
{"3 months ago", Time(now.Add(-99 * Day)), "3 months ago"},
|
||||
{"1 year ago (1)", Time(now.Add(-365 * Day)), "1 year ago"},
|
||||
{"1 year ago (1)", Time(now.Add(-400 * Day)), "1 year ago"},
|
||||
{"2 years ago (1)", Time(now.Add(-548 * Day)), "2 years ago"},
|
||||
{"2 years ago (2)", Time(now.Add(-725 * Day)), "2 years ago"},
|
||||
{"2 years ago (3)", Time(now.Add(-800 * Day)), "2 years ago"},
|
||||
{"3 years ago", Time(now.Add(-3 * Year)), "3 years ago"},
|
||||
{"long ago", Time(now.Add(-LongTime)), "a long while ago"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func TestFuture(t *testing.T) {
|
||||
// Add a little time so that these things properly line up in
|
||||
// the future.
|
||||
now := time.Now().Add(time.Millisecond * 250)
|
||||
testList{
|
||||
{"now", Time(now), "now"},
|
||||
{"1 second from now", Time(now.Add(+1 * time.Second)), "1 second from now"},
|
||||
{"12 seconds from now", Time(now.Add(+12 * time.Second)), "12 seconds from now"},
|
||||
{"30 seconds from now", Time(now.Add(+30 * time.Second)), "30 seconds from now"},
|
||||
{"45 seconds from now", Time(now.Add(+45 * time.Second)), "45 seconds from now"},
|
||||
{"15 minutes from now", Time(now.Add(+15 * time.Minute)), "15 minutes from now"},
|
||||
{"2 hours from now", Time(now.Add(+2 * time.Hour)), "2 hours from now"},
|
||||
{"21 hours from now", Time(now.Add(+21 * time.Hour)), "21 hours from now"},
|
||||
{"1 day from now", Time(now.Add(+26 * time.Hour)), "1 day from now"},
|
||||
{"2 days from now", Time(now.Add(+49 * time.Hour)), "2 days from now"},
|
||||
{"3 days from now", Time(now.Add(+3 * Day)), "3 days from now"},
|
||||
{"1 week from now (1)", Time(now.Add(+7 * Day)), "1 week from now"},
|
||||
{"1 week from now (2)", Time(now.Add(+12 * Day)), "1 week from now"},
|
||||
{"2 weeks from now", Time(now.Add(+15 * Day)), "2 weeks from now"},
|
||||
{"1 month from now", Time(now.Add(+30 * Day)), "1 month from now"},
|
||||
{"1 year from now", Time(now.Add(+365 * Day)), "1 year from now"},
|
||||
{"2 years from now", Time(now.Add(+2 * Year)), "2 years from now"},
|
||||
{"a while from now", Time(now.Add(+LongTime)), "a long while from now"},
|
||||
}.validate(t)
|
||||
}
|
||||
|
||||
func TestRange(t *testing.T) {
|
||||
start := time.Time{}
|
||||
end := time.Unix(math.MaxInt64, math.MaxInt64)
|
||||
x := RelTime(start, end, "ago", "from now")
|
||||
if x != "a long while from now" {
|
||||
t.Errorf("Expected a long while from now, got %q", x)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomRelTime(t *testing.T) {
|
||||
now := time.Now().Add(time.Millisecond * 250)
|
||||
magnitudes := []RelTimeMagnitude{
|
||||
{time.Second, "now", time.Second},
|
||||
{2 * time.Second, "1 second %s", 1},
|
||||
{time.Minute, "%d seconds %s", time.Second},
|
||||
{Day - time.Second, "%d minutes %s", time.Minute},
|
||||
{Day, "%d hours %s", time.Hour},
|
||||
{2 * Day, "1 day %s", 1},
|
||||
{Week, "%d days %s", Day},
|
||||
{2 * Week, "1 week %s", 1},
|
||||
{6 * Month, "%d weeks %s", Week},
|
||||
{Year, "%d months %s", Month},
|
||||
}
|
||||
customRelTime := func(then time.Time) string {
|
||||
return CustomRelTime(then, time.Now(), "ago", "from now", magnitudes)
|
||||
}
|
||||
testList{
|
||||
{"now", customRelTime(now), "now"},
|
||||
{"1 second from now", customRelTime(now.Add(+1 * time.Second)), "1 second from now"},
|
||||
{"12 seconds from now", customRelTime(now.Add(+12 * time.Second)), "12 seconds from now"},
|
||||
{"30 seconds from now", customRelTime(now.Add(+30 * time.Second)), "30 seconds from now"},
|
||||
{"45 seconds from now", customRelTime(now.Add(+45 * time.Second)), "45 seconds from now"},
|
||||
{"15 minutes from now", customRelTime(now.Add(+15 * time.Minute)), "15 minutes from now"},
|
||||
{"2 hours from now", customRelTime(now.Add(+2 * time.Hour)), "120 minutes from now"},
|
||||
{"21 hours from now", customRelTime(now.Add(+21 * time.Hour)), "1260 minutes from now"},
|
||||
{"1 day from now", customRelTime(now.Add(+26 * time.Hour)), "1 day from now"},
|
||||
{"2 days from now", customRelTime(now.Add(+49 * time.Hour)), "2 days from now"},
|
||||
{"3 days from now", customRelTime(now.Add(+3 * Day)), "3 days from now"},
|
||||
{"1 week from now (1)", customRelTime(now.Add(+7 * Day)), "1 week from now"},
|
||||
{"1 week from now (2)", customRelTime(now.Add(+12 * Day)), "1 week from now"},
|
||||
{"2 weeks from now", customRelTime(now.Add(+15 * Day)), "2 weeks from now"},
|
||||
{"1 month from now", customRelTime(now.Add(+30 * Day)), "4 weeks from now"},
|
||||
{"6 months from now", customRelTime(now.Add(+6*Month - time.Second)), "25 weeks from now"},
|
||||
{"1 year from now", customRelTime(now.Add(+365 * Day)), "12 months from now"},
|
||||
{"2 years from now", customRelTime(now.Add(+2 * Year)), "24 months from now"},
|
||||
{"a while from now", customRelTime(now.Add(+LongTime)), "444 months from now"},
|
||||
}.validate(t)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
crypt/crypt
|
|
@ -1,13 +0,0 @@
|
|||
Copyright (c) 2013 Markus Sonderegger
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@ -1,4 +0,0 @@
|
|||
# crypt
|
||||
|
||||
Package crypt provides password-based encryption and decryption of
|
||||
data streams.
|
|
@ -1,165 +0,0 @@
|
|||
// Package crypt provides password-based encryption and decryption of
|
||||
// data streams.
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"hash"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
blockSize = aes.BlockSize // AES block size
|
||||
version = 1
|
||||
)
|
||||
|
||||
// Crypter encrypt/decrypts with AES (Rijndael) in cipher block counter
|
||||
// mode (CTR) and authenticate with HMAC-SHA.
|
||||
type Crypter struct {
|
||||
HashFunc func() hash.Hash
|
||||
HashSize int
|
||||
Key Key
|
||||
BufSize int
|
||||
}
|
||||
|
||||
func (c *Crypter) encHeader(salt, iv, hmacKey []byte) []byte {
|
||||
keySize := c.Key.Size()
|
||||
headerSize := 1 + keySize + blockSize + c.HashSize
|
||||
|
||||
b := make([]byte, headerSize)
|
||||
b[0] = version
|
||||
copy(b[1:1+keySize], salt)
|
||||
copy(b[1+keySize:1+keySize+blockSize], iv)
|
||||
|
||||
mac := hmac.New(c.HashFunc, hmacKey)
|
||||
mac.Write(b[:1+keySize+blockSize])
|
||||
copy(b[1+keySize+blockSize:], mac.Sum(nil))
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Crypter) bufSize() int {
|
||||
if c.BufSize == 0 {
|
||||
return 2 * 1024 * 1024
|
||||
}
|
||||
return c.BufSize
|
||||
}
|
||||
|
||||
func (c *Crypter) decHeader(b []byte) ([]byte, []byte, error) {
|
||||
if b[0] != version {
|
||||
return nil, nil, errors.New("malformed encrypted packet")
|
||||
}
|
||||
|
||||
keySize := c.Key.Size()
|
||||
salt := b[1 : 1+keySize]
|
||||
iv := b[1+keySize : 1+keySize+blockSize]
|
||||
return salt, iv, nil
|
||||
}
|
||||
|
||||
// Encrypt encrypts from src until either EOF is reached on src or an
|
||||
// error occurs. A successful Encrypt returns err == nil, not err == EOF.
|
||||
func (c *Crypter) Encrypt(dst io.Writer, src io.Reader) (err error) {
|
||||
salt := make([]byte, c.Key.Size())
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return err
|
||||
}
|
||||
iv := make([]byte, blockSize)
|
||||
if _, err := rand.Read(iv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aesKey, hmacKey := c.Key.Derive(salt)
|
||||
header := c.encHeader(salt, iv, hmacKey)
|
||||
if _, err := dst.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
mac := hmac.New(c.HashFunc, hmacKey)
|
||||
mac.Write(header)
|
||||
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stream := cipher.NewCTR(block, iv)
|
||||
|
||||
buf := make([]byte, c.bufSize())
|
||||
n := 0
|
||||
for {
|
||||
n, err = src.Read(buf)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
mac.Write(buf[:n])
|
||||
stream.XORKeyStream(buf[:n], buf[:n])
|
||||
if _, err = dst.Write(buf[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = dst.Write(mac.Sum(nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts from src until either EOF is reached on src or an
|
||||
// error occurs. A successful Decrypt returns err == nil, not err == EOF.
|
||||
func (c *Crypter) Decrypt(dst io.Writer, src io.Reader) (err error) {
|
||||
keySize := c.Key.Size()
|
||||
headerSize := 1 + keySize + blockSize + c.HashSize
|
||||
|
||||
header := make([]byte, headerSize)
|
||||
if _, err = src.Read(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
salt, iv, err := c.decHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
aesKey, hmacKey := c.Key.Derive(salt)
|
||||
|
||||
mac := hmac.New(c.HashFunc, hmacKey)
|
||||
mac.Write(header[:1+keySize+blockSize])
|
||||
|
||||
if !bytes.Equal(header[1+keySize+blockSize:], mac.Sum(nil)) {
|
||||
return errors.New("cannot authenticate header")
|
||||
}
|
||||
mac.Write(header[1+keySize+blockSize:])
|
||||
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stream := cipher.NewCTR(block, iv)
|
||||
buf := make([]byte, c.bufSize()+c.HashSize)
|
||||
n := 0
|
||||
for {
|
||||
n, err = src.Read(buf)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
stream.XORKeyStream(buf[:n-c.HashSize], buf[:n-c.HashSize])
|
||||
mac.Write(buf[:n-c.HashSize])
|
||||
if !bytes.Equal(buf[n-c.HashSize:n], mac.Sum(nil)) {
|
||||
return errors.New("cannot authenticate packet")
|
||||
}
|
||||
if _, err = dst.Write(buf[:n-c.HashSize]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
package crypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var plain = [][]byte{
|
||||
[]byte("Nö, ich trinke keinen Tee, ich bin Atheist. --- Helge Schneider"),
|
||||
[]byte("I wish these damn scientists would leave intelligence to the experts. --- Gen. Richard Stillwell (CIA)"),
|
||||
[]byte("I want to die peacefully in my sleep like my grandfather, not screaming in terror like his passengers. --- Charlie Hall"),
|
||||
[]byte("NOTE 3: Each bit has the value either ZERO or ONE. --- ECMA-035 spec"),
|
||||
[]byte("Writing about music is like dancing about architecture. --- Frank Zappa"),
|
||||
[]byte("If you want to go somewhere, goto is the best way to get there. --- K Thompson"),
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt(t *testing.T) {
|
||||
enc := bytes.NewBuffer(nil)
|
||||
dec := bytes.NewBuffer(nil)
|
||||
password := []byte("test password")
|
||||
c := &Crypter{
|
||||
HashFunc: sha1.New,
|
||||
HashSize: sha1.Size,
|
||||
Key: NewPbkdf2Key(password, 32),
|
||||
}
|
||||
defer c.Key.Reset()
|
||||
|
||||
for _, src := range plain {
|
||||
enc.Reset()
|
||||
dec.Reset()
|
||||
err := c.Encrypt(enc, bytes.NewReader(src))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = c.Decrypt(dec, enc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Compare(dec.Bytes(), src) != 0 {
|
||||
t.Errorf("encrypt/decrypt error: want %q, got %q", string(src), string(dec.Bytes()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt1(t *testing.T) {
|
||||
f, err := os.Open("crypt_test.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
encBuf := bytes.NewBuffer(nil)
|
||||
password := []byte("test password")
|
||||
c := &Crypter{
|
||||
HashFunc: sha1.New,
|
||||
HashSize: sha1.Size,
|
||||
Key: NewPbkdf2Key(password, 32),
|
||||
}
|
||||
defer c.Key.Reset()
|
||||
|
||||
err = c.Encrypt(encBuf, f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
decBuf := bytes.NewBuffer(nil)
|
||||
err = c.Decrypt(decBuf, encBuf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
src, err := ioutil.ReadFile("crypt_test.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Compare(decBuf.Bytes(), src) != 0 {
|
||||
t.Errorf("encrypt/decrypt file error: crypt_test.go")
|
||||
}
|
||||
}
|
|
@ -1,170 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/mars9/crypt"
|
||||
"github.com/mars9/keyring"
|
||||
"github.com/mars9/passwd"
|
||||
)
|
||||
|
||||
var (
|
||||
prompt = flag.Bool("p", false, "prompt to enter a passphrase")
|
||||
decrypt = flag.Bool("d", false, "decrypt infile to oufile")
|
||||
service = flag.String("s", "go-crypto", "keyring service name")
|
||||
username = flag.String("u", os.Getenv("USER"), "keyring username")
|
||||
initKeyring = flag.Bool("i", false, "intialize keyring")
|
||||
)
|
||||
|
||||
func passphrase() ([]byte, error) {
|
||||
if *prompt {
|
||||
password, err := passwd.Get("Enter passphrase: ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get passphrase: %v\n", err)
|
||||
}
|
||||
|
||||
if !*decrypt {
|
||||
confirm, err := passwd.Get("Confirm passphrase: ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get passphrase: %v\n", err)
|
||||
}
|
||||
if !bytes.Equal(password, confirm) {
|
||||
return nil, fmt.Errorf("Passphrase mismatch, try again.")
|
||||
}
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
ring, err := keyring.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ring.Get(*service, *username)
|
||||
}
|
||||
|
||||
func initialize() error {
|
||||
password, err := passwd.Get("Enter passphrase: ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get passphrase: %v\n", err)
|
||||
}
|
||||
|
||||
confirm, err := passwd.Get("Confirm passphrase: ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get passphrase: %v\n", err)
|
||||
}
|
||||
if !bytes.Equal(password, confirm) {
|
||||
return fmt.Errorf("Passphrase mismatch, try again.")
|
||||
}
|
||||
|
||||
ring, err := keyring.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ring.Set(*service, *username, password)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
narg := flag.NArg()
|
||||
if narg > 2 {
|
||||
usage()
|
||||
}
|
||||
if runtime.GOOS == "windows" && narg == 0 {
|
||||
usage()
|
||||
}
|
||||
|
||||
if *initKeyring {
|
||||
if err := initialize(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "initialize keyring: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
password, err := passphrase()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(3)
|
||||
}
|
||||
defer func() {
|
||||
for i := range password {
|
||||
password[i] = 0
|
||||
}
|
||||
}()
|
||||
|
||||
in := os.Stdin
|
||||
out := os.Stdout
|
||||
if narg > 0 {
|
||||
in, err = os.Open(flag.Arg(0))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "open %s: %v\n", flag.Arg(0), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
if narg == 2 {
|
||||
out, err = os.Create(flag.Arg(1))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "create %s: %v\n", flag.Arg(1), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
if err := out.Sync(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "sync %s: %v\n", flag.Arg(1), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := out.Close(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "sync %s: %v\n", flag.Arg(1), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
c := &crypt.Crypter{
|
||||
HashFunc: sha1.New,
|
||||
HashSize: sha1.Size,
|
||||
Key: crypt.NewPbkdf2Key(password, 32),
|
||||
}
|
||||
|
||||
if !*decrypt {
|
||||
if err := c.Encrypt(out, in); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "encrypt: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
if err := c.Decrypt(out, in); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "decrypt: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
if runtime.GOOS == "windows" {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [options] infile [outfile]\n", os.Args[0])
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [options] [infile] [[outfile]]\n", os.Args[0])
|
||||
}
|
||||
fmt.Fprint(os.Stderr, usageMsg)
|
||||
fmt.Fprintf(os.Stderr, "\nOptions:\n")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
const usageMsg = `
|
||||
Files are encrypted with AES (Rijndael) in cipher block counter mode
|
||||
(CTR) and authenticate with HMAC-SHA. Encryption and HMAC keys are
|
||||
derived from passphrase using PBKDF2.
|
||||
|
||||
If outfile is not specified, the de-/encrypted data is written to the
|
||||
standard output and if infile is not specified, the de-/encrypted data
|
||||
is read from standard input (reading standard input is not available
|
||||
on windows).
|
||||
`
|
|
@ -1,79 +0,0 @@
|
|||
package crypt
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
// Key defines the key derivation function interface.
|
||||
type Key interface {
|
||||
// Derive returns the AES key and HMAC-SHA key, for the given password,
|
||||
// salt combination.
|
||||
Derive(salt []byte) (aesKey, hmacKey []byte)
|
||||
|
||||
// Size returns the key-size. Key-size should either 16, 24, or 32 to
|
||||
// select AES-128, AES-192, or AES-256.
|
||||
Size() int
|
||||
|
||||
// Reset resets/flushes the key.
|
||||
Reset()
|
||||
}
|
||||
|
||||
type pbkdf2Key struct {
|
||||
password []byte
|
||||
size int
|
||||
}
|
||||
|
||||
// NewPbkdf2Key returns the key derivation function PBKDF2 as defined in
|
||||
// RFC 2898.
|
||||
func NewPbkdf2Key(password []byte, size int) Key {
|
||||
return pbkdf2Key{password: password, size: size}
|
||||
}
|
||||
|
||||
func (k pbkdf2Key) Derive(salt []byte) (aesKey, hmacKey []byte) {
|
||||
key := pbkdf2.Key(k.password, salt, 4096, 2*k.size, sha1.New)
|
||||
aesKey = key[:k.size]
|
||||
hmacKey = key[k.size:]
|
||||
return aesKey, hmacKey
|
||||
}
|
||||
|
||||
func (k pbkdf2Key) Size() int { return k.size }
|
||||
|
||||
func (k pbkdf2Key) Reset() {
|
||||
for i := range k.password {
|
||||
k.password[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
type scryptKey struct {
|
||||
password []byte
|
||||
size int
|
||||
}
|
||||
|
||||
// NewScryptKey returns the scrypt key derivation function as defined in
|
||||
// Colin Percival's paper "Stronger Key Derivation via Sequential
|
||||
// Memory-Hard Functions".
|
||||
func NewScryptKey(password []byte, size int) Key {
|
||||
return scryptKey{password: password, size: size}
|
||||
}
|
||||
|
||||
func (k scryptKey) Derive(salt []byte) (aesKey, hmacKey []byte) {
|
||||
key, err := scrypt.Key(k.password, salt, 16384, 8, 1, 2*k.size)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
aesKey = key[:k.size]
|
||||
hmacKey = key[k.size:]
|
||||
return aesKey, hmacKey
|
||||
}
|
||||
|
||||
func (k scryptKey) Size() int { return k.size }
|
||||
|
||||
func (k scryptKey) Reset() {
|
||||
for i := range k.password {
|
||||
k.password[i] = 0
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
|
@ -1,11 +0,0 @@
|
|||
language: go
|
||||
go_import_path: github.com/pkg/errors
|
||||
go:
|
||||
- 1.4.3
|
||||
- 1.5.4
|
||||
- 1.6.2
|
||||
- 1.7.1
|
||||
- tip
|
||||
|
||||
script:
|
||||
- go test -v ./...
|
|
@ -1,23 +0,0 @@
|
|||
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -1,52 +0,0 @@
|
|||
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors)
|
||||
|
||||
Package errors provides simple error handling primitives.
|
||||
|
||||
`go get github.com/pkg/errors`
|
||||
|
||||
The traditional error handling idiom in Go is roughly akin to
|
||||
```go
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
|
||||
|
||||
## Adding context to an error
|
||||
|
||||
The errors.Wrap function returns a new error that adds context to the original error. For example
|
||||
```go
|
||||
_, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read failed")
|
||||
}
|
||||
```
|
||||
## Retrieving the cause of an error
|
||||
|
||||
Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
|
||||
```go
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
```
|
||||
`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
|
||||
```go
|
||||
switch err := errors.Cause(err).(type) {
|
||||
case *MyError:
|
||||
// handle specifically
|
||||
default:
|
||||
// unknown error
|
||||
}
|
||||
```
|
||||
|
||||
[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high.
|
||||
|
||||
Before proposing a change, please discuss your change by raising an issue.
|
||||
|
||||
## Licence
|
||||
|
||||
BSD-2-Clause
|
|
@ -1,32 +0,0 @@
|
|||
version: build-{build}.{branch}
|
||||
|
||||
clone_folder: C:\gopath\src\github.com\pkg\errors
|
||||
shallow_clone: true # for startup speed
|
||||
|
||||
environment:
|
||||
GOPATH: C:\gopath
|
||||
|
||||
platform:
|
||||
- x64
|
||||
|
||||
# http://www.appveyor.com/docs/installed-software
|
||||
install:
|
||||
# some helpful output for debugging builds
|
||||
- go version
|
||||
- go env
|
||||
# pre-installed MinGW at C:\MinGW is 32bit only
|
||||
# but MSYS2 at C:\msys64 has mingw64
|
||||
- set PATH=C:\msys64\mingw64\bin;%PATH%
|
||||
- gcc --version
|
||||
- g++ --version
|
||||
|
||||
build_script:
|
||||
- go install -v ./...
|
||||
|
||||
test_script:
|
||||
- set PATH=C:\gopath\bin;%PATH%
|
||||
- go test -v ./...
|
||||
|
||||
#artifacts:
|
||||
# - path: '%GOPATH%\bin\*.exe'
|
||||
deploy: off
|
|
@ -1,59 +0,0 @@
|
|||
// +build go1.7
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
stderrors "errors"
|
||||
)
|
||||
|
||||
func noErrors(at, depth int) error {
|
||||
if at >= depth {
|
||||
return stderrors.New("no error")
|
||||
}
|
||||
return noErrors(at+1, depth)
|
||||
}
|
||||
func yesErrors(at, depth int) error {
|
||||
if at >= depth {
|
||||
return New("ye error")
|
||||
}
|
||||
return yesErrors(at+1, depth)
|
||||
}
|
||||
|
||||
func BenchmarkErrors(b *testing.B) {
|
||||
var toperr error
|
||||
type run struct {
|
||||
stack int
|
||||
std bool
|
||||
}
|
||||
runs := []run{
|
||||
{10, false},
|
||||
{10, true},
|
||||
{100, false},
|
||||
{100, true},
|
||||
{1000, false},
|
||||
{1000, true},
|
||||
}
|
||||
for _, r := range runs {
|
||||
part := "pkg/errors"
|
||||
if r.std {
|
||||
part = "errors"
|
||||
}
|
||||
name := fmt.Sprintf("%s-stack-%d", part, r.stack)
|
||||
b.Run(name, func(b *testing.B) {
|
||||
var err error
|
||||
f := yesErrors
|
||||
if r.std {
|
||||
f = noErrors
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = f(0, r.stack)
|
||||
}
|
||||
b.StopTimer()
|
||||
toperr = err
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,269 +0,0 @@
|
|||
// Package errors provides simple error handling primitives.
|
||||
//
|
||||
// The traditional error handling idiom in Go is roughly akin to
|
||||
//
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// which applied recursively up the call stack results in error reports
|
||||
// without context or debugging information. The errors package allows
|
||||
// programmers to add context to the failure path in their code in a way
|
||||
// that does not destroy the original value of the error.
|
||||
//
|
||||
// Adding context to an error
|
||||
//
|
||||
// The errors.Wrap function returns a new error that adds context to the
|
||||
// original error by recording a stack trace at the point Wrap is called,
|
||||
// and the supplied message. For example
|
||||
//
|
||||
// _, err := ioutil.ReadAll(r)
|
||||
// if err != nil {
|
||||
// return errors.Wrap(err, "read failed")
|
||||
// }
|
||||
//
|
||||
// If additional control is required the errors.WithStack and errors.WithMessage
|
||||
// functions destructure errors.Wrap into its component operations of annotating
|
||||
// an error with a stack trace and an a message, respectively.
|
||||
//
|
||||
// Retrieving the cause of an error
|
||||
//
|
||||
// Using errors.Wrap constructs a stack of errors, adding context to the
|
||||
// preceding error. Depending on the nature of the error it may be necessary
|
||||
// to reverse the operation of errors.Wrap to retrieve the original error
|
||||
// for inspection. Any error value which implements this interface
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
|
||||
// the topmost error which does not implement causer, which is assumed to be
|
||||
// the original cause. For example:
|
||||
//
|
||||
// switch err := errors.Cause(err).(type) {
|
||||
// case *MyError:
|
||||
// // handle specifically
|
||||
// default:
|
||||
// // unknown error
|
||||
// }
|
||||
//
|
||||
// causer interface is not exported by this package, but is considered a part
|
||||
// of stable public API.
|
||||
//
|
||||
// Formatted printing of errors
|
||||
//
|
||||
// All error values returned from this package implement fmt.Formatter and can
|
||||
// be formatted by the fmt package. The following verbs are supported
|
||||
//
|
||||
// %s print the error. If the error has a Cause it will be
|
||||
// printed recursively
|
||||
// %v see %s
|
||||
// %+v extended format. Each Frame of the error's StackTrace will
|
||||
// be printed in detail.
|
||||
//
|
||||
// Retrieving the stack trace of an error or wrapper
|
||||
//
|
||||
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
|
||||
// invoked. This information can be retrieved with the following interface.
|
||||
//
|
||||
// type stackTracer interface {
|
||||
// StackTrace() errors.StackTrace
|
||||
// }
|
||||
//
|
||||
// Where errors.StackTrace is defined as
|
||||
//
|
||||
// type StackTrace []Frame
|
||||
//
|
||||
// The Frame type represents a call site in the stack trace. Frame supports
|
||||
// the fmt.Formatter interface that can be used for printing information about
|
||||
// the stack trace of this error. For example:
|
||||
//
|
||||
// if err, ok := err.(stackTracer); ok {
|
||||
// for _, f := range err.StackTrace() {
|
||||
// fmt.Printf("%+s:%d", f)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// stackTracer interface is not exported by this package, but is considered a part
|
||||
// of stable public API.
|
||||
//
|
||||
// See the documentation for Frame.Format for more details.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// New returns an error with the supplied message.
|
||||
// New also records the stack trace at the point it was called.
|
||||
func New(message string) error {
|
||||
return &fundamental{
|
||||
msg: message,
|
||||
stack: callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf formats according to a format specifier and returns the string
|
||||
// as a value that satisfies error.
|
||||
// Errorf also records the stack trace at the point it was called.
|
||||
func Errorf(format string, args ...interface{}) error {
|
||||
return &fundamental{
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
stack: callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// fundamental is an error that has a message and a stack, but no caller.
|
||||
type fundamental struct {
|
||||
msg string
|
||||
*stack
|
||||
}
|
||||
|
||||
func (f *fundamental) Error() string { return f.msg }
|
||||
|
||||
func (f *fundamental) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
io.WriteString(s, f.msg)
|
||||
f.stack.Format(s, verb)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's':
|
||||
io.WriteString(s, f.msg)
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", f.msg)
|
||||
}
|
||||
}
|
||||
|
||||
// WithStack annotates err with a stack trace at the point WithStack was called.
|
||||
// If err is nil, WithStack returns nil.
|
||||
func WithStack(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
type withStack struct {
|
||||
error
|
||||
*stack
|
||||
}
|
||||
|
||||
func (w *withStack) Cause() error { return w.error }
|
||||
|
||||
func (w *withStack) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v", w.Cause())
|
||||
w.stack.Format(s, verb)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's':
|
||||
io.WriteString(s, w.Error())
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", w.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap returns an error annotating err with a stack trace
|
||||
// at the point Wrap is called, and the supplied message.
|
||||
// If err is nil, Wrap returns nil.
|
||||
func Wrap(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = &withMessage{
|
||||
cause: err,
|
||||
msg: message,
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapf returns an error annotating err with a stack trace
|
||||
// at the point Wrapf is call, and the format specifier.
|
||||
// If err is nil, Wrapf returns nil.
|
||||
func Wrapf(err error, format string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = &withMessage{
|
||||
cause: err,
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessage annotates err with a new message.
|
||||
// If err is nil, WithMessage returns nil.
|
||||
func WithMessage(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withMessage{
|
||||
cause: err,
|
||||
msg: message,
|
||||
}
|
||||
}
|
||||
|
||||
type withMessage struct {
|
||||
cause error
|
||||
msg string
|
||||
}
|
||||
|
||||
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
|
||||
func (w *withMessage) Cause() error { return w.cause }
|
||||
|
||||
func (w *withMessage) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v\n", w.Cause())
|
||||
io.WriteString(s, w.msg)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's', 'q':
|
||||
io.WriteString(s, w.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Cause returns the underlying cause of the error, if possible.
|
||||
// An error value has a cause if it implements the following
|
||||
// interface:
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// If the error does not implement Cause, the original error will
|
||||
// be returned. If the error is nil, nil will be returned without further
|
||||
// investigation.
|
||||
func Cause(err error) error {
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
for err != nil {
|
||||
cause, ok := err.(causer)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
err = cause.Cause()
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -1,226 +0,0 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
err string
|
||||
want error
|
||||
}{
|
||||
{"", fmt.Errorf("")},
|
||||
{"foo", fmt.Errorf("foo")},
|
||||
{"foo", New("foo")},
|
||||
{"string with format specifiers: %v", errors.New("string with format specifiers: %v")},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := New(tt.err)
|
||||
if got.Error() != tt.want.Error() {
|
||||
t.Errorf("New.Error(): got: %q, want %q", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapNil(t *testing.T) {
|
||||
got := Wrap(nil, "no error")
|
||||
if got != nil {
|
||||
t.Errorf("Wrap(nil, \"no error\"): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrap(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
message string
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "read error", "read error: EOF"},
|
||||
{Wrap(io.EOF, "read error"), "client error", "client error: read error: EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := Wrap(tt.err, tt.message).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("Wrap(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type nilError struct{}
|
||||
|
||||
func (nilError) Error() string { return "nil error" }
|
||||
|
||||
func TestCause(t *testing.T) {
|
||||
x := New("error")
|
||||
tests := []struct {
|
||||
err error
|
||||
want error
|
||||
}{{
|
||||
// nil error is nil
|
||||
err: nil,
|
||||
want: nil,
|
||||
}, {
|
||||
// explicit nil error is nil
|
||||
err: (error)(nil),
|
||||
want: nil,
|
||||
}, {
|
||||
// typed nil is nil
|
||||
err: (*nilError)(nil),
|
||||
want: (*nilError)(nil),
|
||||
}, {
|
||||
// uncaused error is unaffected
|
||||
err: io.EOF,
|
||||
want: io.EOF,
|
||||
}, {
|
||||
// caused error returns cause
|
||||
err: Wrap(io.EOF, "ignored"),
|
||||
want: io.EOF,
|
||||
}, {
|
||||
err: x, // return from errors.New
|
||||
want: x,
|
||||
}, {
|
||||
WithMessage(nil, "whoops"),
|
||||
nil,
|
||||
}, {
|
||||
WithMessage(io.EOF, "whoops"),
|
||||
io.EOF,
|
||||
}, {
|
||||
WithStack(nil),
|
||||
nil,
|
||||
}, {
|
||||
WithStack(io.EOF),
|
||||
io.EOF,
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
got := Cause(tt.err)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("test %d: got %#v, want %#v", i+1, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapfNil(t *testing.T) {
|
||||
got := Wrapf(nil, "no error")
|
||||
if got != nil {
|
||||
t.Errorf("Wrapf(nil, \"no error\"): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapf(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
message string
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "read error", "read error: EOF"},
|
||||
{Wrapf(io.EOF, "read error without format specifiers"), "client error", "client error: read error without format specifiers: EOF"},
|
||||
{Wrapf(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := Wrapf(tt.err, tt.message).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("Wrapf(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorf(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{Errorf("read error without format specifiers"), "read error without format specifiers"},
|
||||
{Errorf("read error with %d format specifier", 1), "read error with 1 format specifier"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := tt.err.Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("Errorf(%v): got: %q, want %q", tt.err, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithStackNil(t *testing.T) {
|
||||
got := WithStack(nil)
|
||||
if got != nil {
|
||||
t.Errorf("WithStack(nil): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithStack(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "EOF"},
|
||||
{WithStack(io.EOF), "EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := WithStack(tt.err).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("WithStack(%v): got: %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithMessageNil(t *testing.T) {
|
||||
got := WithMessage(nil, "no error")
|
||||
if got != nil {
|
||||
t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
message string
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "read error", "read error: EOF"},
|
||||
{WithMessage(io.EOF, "read error"), "client error", "client error: read error: EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := WithMessage(tt.err, tt.message).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// errors.New, etc values are not expected to be compared by value
|
||||
// but the change in errors#27 made them incomparable. Assert that
|
||||
// various kinds of errors have a functional equality operator, even
|
||||
// if the result of that equality is always false.
|
||||
func TestErrorEquality(t *testing.T) {
|
||||
vals := []error{
|
||||
nil,
|
||||
io.EOF,
|
||||
errors.New("EOF"),
|
||||
New("EOF"),
|
||||
Errorf("EOF"),
|
||||
Wrap(io.EOF, "EOF"),
|
||||
Wrapf(io.EOF, "EOF%d", 2),
|
||||
WithMessage(nil, "whoops"),
|
||||
WithMessage(io.EOF, "whoops"),
|
||||
WithStack(io.EOF),
|
||||
WithStack(nil),
|
||||
}
|
||||
|
||||
for i := range vals {
|
||||
for j := range vals {
|
||||
_ = vals[i] == vals[j] // mustn't panic
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,205 +0,0 @@
|
|||
package errors_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func ExampleNew() {
|
||||
err := errors.New("whoops")
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: whoops
|
||||
}
|
||||
|
||||
func ExampleNew_printf() {
|
||||
err := errors.New("whoops")
|
||||
fmt.Printf("%+v", err)
|
||||
|
||||
// Example output:
|
||||
// whoops
|
||||
// github.com/pkg/errors_test.ExampleNew_printf
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:17
|
||||
// testing.runExample
|
||||
// /home/dfc/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /home/dfc/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /home/dfc/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// /github.com/pkg/errors/_test/_testmain.go:106
|
||||
// runtime.main
|
||||
// /home/dfc/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /home/dfc/go/src/runtime/asm_amd64.s:2059
|
||||
}
|
||||
|
||||
func ExampleWithMessage() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.WithMessage(cause, "oh noes")
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: oh noes: whoops
|
||||
}
|
||||
|
||||
func ExampleWithStack() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.WithStack(cause)
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: whoops
|
||||
}
|
||||
|
||||
func ExampleWithStack_printf() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.WithStack(cause)
|
||||
fmt.Printf("%+v", err)
|
||||
|
||||
// Example Output:
|
||||
// whoops
|
||||
// github.com/pkg/errors_test.ExampleWithStack_printf
|
||||
// /home/fabstu/go/src/github.com/pkg/errors/example_test.go:55
|
||||
// testing.runExample
|
||||
// /usr/lib/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /usr/lib/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /usr/lib/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// github.com/pkg/errors/_test/_testmain.go:106
|
||||
// runtime.main
|
||||
// /usr/lib/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /usr/lib/go/src/runtime/asm_amd64.s:2086
|
||||
// github.com/pkg/errors_test.ExampleWithStack_printf
|
||||
// /home/fabstu/go/src/github.com/pkg/errors/example_test.go:56
|
||||
// testing.runExample
|
||||
// /usr/lib/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /usr/lib/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /usr/lib/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// github.com/pkg/errors/_test/_testmain.go:106
|
||||
// runtime.main
|
||||
// /usr/lib/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /usr/lib/go/src/runtime/asm_amd64.s:2086
|
||||
}
|
||||
|
||||
func ExampleWrap() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.Wrap(cause, "oh noes")
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: oh noes: whoops
|
||||
}
|
||||
|
||||
func fn() error {
|
||||
e1 := errors.New("error")
|
||||
e2 := errors.Wrap(e1, "inner")
|
||||
e3 := errors.Wrap(e2, "middle")
|
||||
return errors.Wrap(e3, "outer")
|
||||
}
|
||||
|
||||
func ExampleCause() {
|
||||
err := fn()
|
||||
fmt.Println(err)
|
||||
fmt.Println(errors.Cause(err))
|
||||
|
||||
// Output: outer: middle: inner: error
|
||||
// error
|
||||
}
|
||||
|
||||
func ExampleWrap_extended() {
|
||||
err := fn()
|
||||
fmt.Printf("%+v\n", err)
|
||||
|
||||
// Example output:
|
||||
// error
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:47
|
||||
// github.com/pkg/errors_test.ExampleCause_printf
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:63
|
||||
// testing.runExample
|
||||
// /home/dfc/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /home/dfc/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /home/dfc/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// /github.com/pkg/errors/_test/_testmain.go:104
|
||||
// runtime.main
|
||||
// /home/dfc/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /home/dfc/go/src/runtime/asm_amd64.s:2059
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:48: inner
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:49: middle
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:50: outer
|
||||
}
|
||||
|
||||
func ExampleWrapf() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.Wrapf(cause, "oh noes #%d", 2)
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: oh noes #2: whoops
|
||||
}
|
||||
|
||||
func ExampleErrorf_extended() {
|
||||
err := errors.Errorf("whoops: %s", "foo")
|
||||
fmt.Printf("%+v", err)
|
||||
|
||||
// Example output:
|
||||
// whoops: foo
|
||||
// github.com/pkg/errors_test.ExampleErrorf
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:101
|
||||
// testing.runExample
|
||||
// /home/dfc/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /home/dfc/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /home/dfc/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// /github.com/pkg/errors/_test/_testmain.go:102
|
||||
// runtime.main
|
||||
// /home/dfc/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /home/dfc/go/src/runtime/asm_amd64.s:2059
|
||||
}
|
||||
|
||||
func Example_stackTrace() {
|
||||
type stackTracer interface {
|
||||
StackTrace() errors.StackTrace
|
||||
}
|
||||
|
||||
err, ok := errors.Cause(fn()).(stackTracer)
|
||||
if !ok {
|
||||
panic("oops, err does not implement stackTracer")
|
||||
}
|
||||
|
||||
st := err.StackTrace()
|
||||
fmt.Printf("%+v", st[0:2]) // top two frames
|
||||
|
||||
// Example output:
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:47
|
||||
// github.com/pkg/errors_test.Example_stackTrace
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:127
|
||||
}
|
||||
|
||||
func ExampleCause_printf() {
|
||||
err := errors.Wrap(func() error {
|
||||
return func() error {
|
||||
return errors.Errorf("hello %s", fmt.Sprintf("world"))
|
||||
}()
|
||||
}(), "failed")
|
||||
|
||||
fmt.Printf("%v", err)
|
||||
|
||||
// Output: failed: hello world
|
||||
}
|
|
@ -1,535 +0,0 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
New("error"),
|
||||
"%s",
|
||||
"error",
|
||||
}, {
|
||||
New("error"),
|
||||
"%v",
|
||||
"error",
|
||||
}, {
|
||||
New("error"),
|
||||
"%+v",
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatNew\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:26",
|
||||
}, {
|
||||
New("error"),
|
||||
"%q",
|
||||
`"error"`,
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatErrorf(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
Errorf("%s", "error"),
|
||||
"%s",
|
||||
"error",
|
||||
}, {
|
||||
Errorf("%s", "error"),
|
||||
"%v",
|
||||
"error",
|
||||
}, {
|
||||
Errorf("%s", "error"),
|
||||
"%+v",
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatErrorf\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:56",
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWrap(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
Wrap(New("error"), "error2"),
|
||||
"%s",
|
||||
"error2: error",
|
||||
}, {
|
||||
Wrap(New("error"), "error2"),
|
||||
"%v",
|
||||
"error2: error",
|
||||
}, {
|
||||
Wrap(New("error"), "error2"),
|
||||
"%+v",
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatWrap\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:82",
|
||||
}, {
|
||||
Wrap(io.EOF, "error"),
|
||||
"%s",
|
||||
"error: EOF",
|
||||
}, {
|
||||
Wrap(io.EOF, "error"),
|
||||
"%v",
|
||||
"error: EOF",
|
||||
}, {
|
||||
Wrap(io.EOF, "error"),
|
||||
"%+v",
|
||||
"EOF\n" +
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatWrap\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:96",
|
||||
}, {
|
||||
Wrap(Wrap(io.EOF, "error1"), "error2"),
|
||||
"%+v",
|
||||
"EOF\n" +
|
||||
"error1\n" +
|
||||
"github.com/pkg/errors.TestFormatWrap\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:103\n",
|
||||
}, {
|
||||
Wrap(New("error with space"), "context"),
|
||||
"%q",
|
||||
`"context: error with space"`,
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWrapf(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
Wrapf(io.EOF, "error%d", 2),
|
||||
"%s",
|
||||
"error2: EOF",
|
||||
}, {
|
||||
Wrapf(io.EOF, "error%d", 2),
|
||||
"%v",
|
||||
"error2: EOF",
|
||||
}, {
|
||||
Wrapf(io.EOF, "error%d", 2),
|
||||
"%+v",
|
||||
"EOF\n" +
|
||||
"error2\n" +
|
||||
"github.com/pkg/errors.TestFormatWrapf\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:134",
|
||||
}, {
|
||||
Wrapf(New("error"), "error%d", 2),
|
||||
"%s",
|
||||
"error2: error",
|
||||
}, {
|
||||
Wrapf(New("error"), "error%d", 2),
|
||||
"%v",
|
||||
"error2: error",
|
||||
}, {
|
||||
Wrapf(New("error"), "error%d", 2),
|
||||
"%+v",
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatWrapf\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:149",
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWithStack(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want []string
|
||||
}{{
|
||||
WithStack(io.EOF),
|
||||
"%s",
|
||||
[]string{"EOF"},
|
||||
}, {
|
||||
WithStack(io.EOF),
|
||||
"%v",
|
||||
[]string{"EOF"},
|
||||
}, {
|
||||
WithStack(io.EOF),
|
||||
"%+v",
|
||||
[]string{"EOF",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:175"},
|
||||
}, {
|
||||
WithStack(New("error")),
|
||||
"%s",
|
||||
[]string{"error"},
|
||||
}, {
|
||||
WithStack(New("error")),
|
||||
"%v",
|
||||
[]string{"error"},
|
||||
}, {
|
||||
WithStack(New("error")),
|
||||
"%+v",
|
||||
[]string{"error",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:189",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:189"},
|
||||
}, {
|
||||
WithStack(WithStack(io.EOF)),
|
||||
"%+v",
|
||||
[]string{"EOF",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:197",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:197"},
|
||||
}, {
|
||||
WithStack(WithStack(Wrapf(io.EOF, "message"))),
|
||||
"%+v",
|
||||
[]string{"EOF",
|
||||
"message",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:205",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:205",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:205"},
|
||||
}, {
|
||||
WithStack(Errorf("error%d", 1)),
|
||||
"%+v",
|
||||
[]string{"error1",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:216",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:216"},
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWithMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want []string
|
||||
}{{
|
||||
WithMessage(New("error"), "error2"),
|
||||
"%s",
|
||||
[]string{"error2: error"},
|
||||
}, {
|
||||
WithMessage(New("error"), "error2"),
|
||||
"%v",
|
||||
[]string{"error2: error"},
|
||||
}, {
|
||||
WithMessage(New("error"), "error2"),
|
||||
"%+v",
|
||||
[]string{
|
||||
"error",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:244",
|
||||
"error2"},
|
||||
}, {
|
||||
WithMessage(io.EOF, "addition1"),
|
||||
"%s",
|
||||
[]string{"addition1: EOF"},
|
||||
}, {
|
||||
WithMessage(io.EOF, "addition1"),
|
||||
"%v",
|
||||
[]string{"addition1: EOF"},
|
||||
}, {
|
||||
WithMessage(io.EOF, "addition1"),
|
||||
"%+v",
|
||||
[]string{"EOF", "addition1"},
|
||||
}, {
|
||||
WithMessage(WithMessage(io.EOF, "addition1"), "addition2"),
|
||||
"%v",
|
||||
[]string{"addition2: addition1: EOF"},
|
||||
}, {
|
||||
WithMessage(WithMessage(io.EOF, "addition1"), "addition2"),
|
||||
"%+v",
|
||||
[]string{"EOF", "addition1", "addition2"},
|
||||
}, {
|
||||
Wrap(WithMessage(io.EOF, "error1"), "error2"),
|
||||
"%+v",
|
||||
[]string{"EOF", "error1", "error2",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:272"},
|
||||
}, {
|
||||
WithMessage(Errorf("error%d", 1), "error2"),
|
||||
"%+v",
|
||||
[]string{"error1",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:278",
|
||||
"error2"},
|
||||
}, {
|
||||
WithMessage(WithStack(io.EOF), "error"),
|
||||
"%+v",
|
||||
[]string{
|
||||
"EOF",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:285",
|
||||
"error"},
|
||||
}, {
|
||||
WithMessage(Wrap(WithStack(io.EOF), "inside-error"), "outside-error"),
|
||||
"%+v",
|
||||
[]string{
|
||||
"EOF",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:293",
|
||||
"inside-error",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:293",
|
||||
"outside-error"},
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatGeneric(t *testing.T) {
|
||||
starts := []struct {
|
||||
err error
|
||||
want []string
|
||||
}{
|
||||
{New("new-error"), []string{
|
||||
"new-error",
|
||||
"github.com/pkg/errors.TestFormatGeneric\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:315"},
|
||||
}, {Errorf("errorf-error"), []string{
|
||||
"errorf-error",
|
||||
"github.com/pkg/errors.TestFormatGeneric\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:319"},
|
||||
}, {errors.New("errors-new-error"), []string{
|
||||
"errors-new-error"},
|
||||
},
|
||||
}
|
||||
|
||||
wrappers := []wrapper{
|
||||
{
|
||||
func(err error) error { return WithMessage(err, "with-message") },
|
||||
[]string{"with-message"},
|
||||
}, {
|
||||
func(err error) error { return WithStack(err) },
|
||||
[]string{
|
||||
"github.com/pkg/errors.(func·002|TestFormatGeneric.func2)\n\t" +
|
||||
".+/github.com/pkg/errors/format_test.go:333",
|
||||
},
|
||||
}, {
|
||||
func(err error) error { return Wrap(err, "wrap-error") },
|
||||
[]string{
|
||||
"wrap-error",
|
||||
"github.com/pkg/errors.(func·003|TestFormatGeneric.func3)\n\t" +
|
||||
".+/github.com/pkg/errors/format_test.go:339",
|
||||
},
|
||||
}, {
|
||||
func(err error) error { return Wrapf(err, "wrapf-error%d", 1) },
|
||||
[]string{
|
||||
"wrapf-error1",
|
||||
"github.com/pkg/errors.(func·004|TestFormatGeneric.func4)\n\t" +
|
||||
".+/github.com/pkg/errors/format_test.go:346",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for s := range starts {
|
||||
err := starts[s].err
|
||||
want := starts[s].want
|
||||
testFormatCompleteCompare(t, s, err, "%+v", want, false)
|
||||
testGenericRecursive(t, err, want, wrappers, 3)
|
||||
}
|
||||
}
|
||||
|
||||
func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) {
|
||||
got := fmt.Sprintf(format, arg)
|
||||
gotLines := strings.SplitN(got, "\n", -1)
|
||||
wantLines := strings.SplitN(want, "\n", -1)
|
||||
|
||||
if len(wantLines) > len(gotLines) {
|
||||
t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want)
|
||||
return
|
||||
}
|
||||
|
||||
for i, w := range wantLines {
|
||||
match, err := regexp.MatchString(w, gotLines[i])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !match {
|
||||
t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stackLineR = regexp.MustCompile(`\.`)
|
||||
|
||||
// parseBlocks parses input into a slice, where:
|
||||
// - incase entry contains a newline, its a stacktrace
|
||||
// - incase entry contains no newline, its a solo line.
|
||||
//
|
||||
// Detecting stack boundaries only works incase the WithStack-calls are
|
||||
// to be found on the same line, thats why it is optionally here.
|
||||
//
|
||||
// Example use:
|
||||
//
|
||||
// for _, e := range blocks {
|
||||
// if strings.ContainsAny(e, "\n") {
|
||||
// // Match as stack
|
||||
// } else {
|
||||
// // Match as line
|
||||
// }
|
||||
// }
|
||||
//
|
||||
func parseBlocks(input string, detectStackboundaries bool) ([]string, error) {
|
||||
var blocks []string
|
||||
|
||||
stack := ""
|
||||
wasStack := false
|
||||
lines := map[string]bool{} // already found lines
|
||||
|
||||
for _, l := range strings.Split(input, "\n") {
|
||||
isStackLine := stackLineR.MatchString(l)
|
||||
|
||||
switch {
|
||||
case !isStackLine && wasStack:
|
||||
blocks = append(blocks, stack, l)
|
||||
stack = ""
|
||||
lines = map[string]bool{}
|
||||
case isStackLine:
|
||||
if wasStack {
|
||||
// Detecting two stacks after another, possible cause lines match in
|
||||
// our tests due to WithStack(WithStack(io.EOF)) on same line.
|
||||
if detectStackboundaries {
|
||||
if lines[l] {
|
||||
if len(stack) == 0 {
|
||||
return nil, errors.New("len of block must not be zero here")
|
||||
}
|
||||
|
||||
blocks = append(blocks, stack)
|
||||
stack = l
|
||||
lines = map[string]bool{l: true}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
stack = stack + "\n" + l
|
||||
} else {
|
||||
stack = l
|
||||
}
|
||||
lines[l] = true
|
||||
case !isStackLine && !wasStack:
|
||||
blocks = append(blocks, l)
|
||||
default:
|
||||
return nil, errors.New("must not happen")
|
||||
}
|
||||
|
||||
wasStack = isStackLine
|
||||
}
|
||||
|
||||
// Use up stack
|
||||
if stack != "" {
|
||||
blocks = append(blocks, stack)
|
||||
}
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) {
|
||||
gotStr := fmt.Sprintf(format, arg)
|
||||
|
||||
got, err := parseBlocks(gotStr, detectStackBoundaries)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("test %d: fmt.Sprintf(%s, err) -> wrong number of blocks: got(%d) want(%d)\n got: %s\nwant: %s\ngotStr: %q",
|
||||
n+1, format, len(got), len(want), prettyBlocks(got), prettyBlocks(want), gotStr)
|
||||
}
|
||||
|
||||
for i := range got {
|
||||
if strings.ContainsAny(want[i], "\n") {
|
||||
// Match as stack
|
||||
match, err := regexp.MatchString(want[i], got[i])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("test %d: block %d: fmt.Sprintf(%q, err):\ngot:\n%q\nwant:\n%q\nall-got:\n%s\nall-want:\n%s\n",
|
||||
n+1, i+1, format, got[i], want[i], prettyBlocks(got), prettyBlocks(want))
|
||||
}
|
||||
} else {
|
||||
// Match as message
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("test %d: fmt.Sprintf(%s, err) at block %d got != want:\n got: %q\nwant: %q", n+1, format, i+1, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
wrap func(err error) error
|
||||
want []string
|
||||
}
|
||||
|
||||
func prettyBlocks(blocks []string, prefix ...string) string {
|
||||
var out []string
|
||||
|
||||
for _, b := range blocks {
|
||||
out = append(out, fmt.Sprintf("%v", b))
|
||||
}
|
||||
|
||||
return " " + strings.Join(out, "\n ")
|
||||
}
|
||||
|
||||
func testGenericRecursive(t *testing.T, beforeErr error, beforeWant []string, list []wrapper, maxDepth int) {
|
||||
if len(beforeWant) == 0 {
|
||||
panic("beforeWant must not be empty")
|
||||
}
|
||||
for _, w := range list {
|
||||
if len(w.want) == 0 {
|
||||
panic("want must not be empty")
|
||||
}
|
||||
|
||||
err := w.wrap(beforeErr)
|
||||
|
||||
// Copy required cause append(beforeWant, ..) modified beforeWant subtly.
|
||||
beforeCopy := make([]string, len(beforeWant))
|
||||
copy(beforeCopy, beforeWant)
|
||||
|
||||
beforeWant := beforeCopy
|
||||
last := len(beforeWant) - 1
|
||||
var want []string
|
||||
|
||||
// Merge two stacks behind each other.
|
||||
if strings.ContainsAny(beforeWant[last], "\n") && strings.ContainsAny(w.want[0], "\n") {
|
||||
want = append(beforeWant[:last], append([]string{beforeWant[last] + "((?s).*)" + w.want[0]}, w.want[1:]...)...)
|
||||
} else {
|
||||
want = append(beforeWant, w.want...)
|
||||
}
|
||||
|
||||
testFormatCompleteCompare(t, maxDepth, err, "%+v", want, false)
|
||||
if maxDepth > 0 {
|
||||
testGenericRecursive(t, err, want, list, maxDepth-1)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Frame represents a program counter inside a stack frame.
|
||||
type Frame uintptr
|
||||
|
||||
// pc returns the program counter for this frame;
|
||||
// multiple frames may have the same PC value.
|
||||
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
|
||||
|
||||
// file returns the full path to the file that contains the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) file() string {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return "unknown"
|
||||
}
|
||||
file, _ := fn.FileLine(f.pc())
|
||||
return file
|
||||
}
|
||||
|
||||
// line returns the line number of source code of the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) line() int {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return 0
|
||||
}
|
||||
_, line := fn.FileLine(f.pc())
|
||||
return line
|
||||
}
|
||||
|
||||
// Format formats the frame according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s source file
|
||||
// %d source line
|
||||
// %n function name
|
||||
// %v equivalent to %s:%d
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+s path of source file relative to the compile time GOPATH
|
||||
// %+v equivalent to %+s:%d
|
||||
func (f Frame) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
pc := f.pc()
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
io.WriteString(s, "unknown")
|
||||
} else {
|
||||
file, _ := fn.FileLine(pc)
|
||||
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
|
||||
}
|
||||
default:
|
||||
io.WriteString(s, path.Base(f.file()))
|
||||
}
|
||||
case 'd':
|
||||
fmt.Fprintf(s, "%d", f.line())
|
||||
case 'n':
|
||||
name := runtime.FuncForPC(f.pc()).Name()
|
||||
io.WriteString(s, funcname(name))
|
||||
case 'v':
|
||||
f.Format(s, 's')
|
||||
io.WriteString(s, ":")
|
||||
f.Format(s, 'd')
|
||||
}
|
||||
}
|
||||
|
||||
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
|
||||
type StackTrace []Frame
|
||||
|
||||
func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
for _, f := range st {
|
||||
fmt.Fprintf(s, "\n%+v", f)
|
||||
}
|
||||
case s.Flag('#'):
|
||||
fmt.Fprintf(s, "%#v", []Frame(st))
|
||||
default:
|
||||
fmt.Fprintf(s, "%v", []Frame(st))
|
||||
}
|
||||
case 's':
|
||||
fmt.Fprintf(s, "%s", []Frame(st))
|
||||
}
|
||||
}
|
||||
|
||||
// stack represents a stack of program counters.
|
||||
type stack []uintptr
|
||||
|
||||
func (s *stack) Format(st fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case st.Flag('+'):
|
||||
for _, pc := range *s {
|
||||
f := Frame(pc)
|
||||
fmt.Fprintf(st, "\n%+v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stack) StackTrace() StackTrace {
|
||||
f := make([]Frame, len(*s))
|
||||
for i := 0; i < len(f); i++ {
|
||||
f[i] = Frame((*s)[i])
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func callers() *stack {
|
||||
const depth = 32
|
||||
var pcs [depth]uintptr
|
||||
n := runtime.Callers(3, pcs[:])
|
||||
var st stack = pcs[0:n]
|
||||
return &st
|
||||
}
|
||||
|
||||
// funcname removes the path prefix component of a function's name reported by func.Name().
|
||||
func funcname(name string) string {
|
||||
i := strings.LastIndex(name, "/")
|
||||
name = name[i+1:]
|
||||
i = strings.Index(name, ".")
|
||||
return name[i+1:]
|
||||
}
|
||||
|
||||
func trimGOPATH(name, file string) string {
|
||||
// Here we want to get the source file path relative to the compile time
|
||||
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
|
||||
// GOPATH at runtime, but we can infer the number of path segments in the
|
||||
// GOPATH. We note that fn.Name() returns the function name qualified by
|
||||
// the import path, which does not include the GOPATH. Thus we can trim
|
||||
// segments from the beginning of the file path until the number of path
|
||||
// separators remaining is one more than the number of path separators in
|
||||
// the function name. For example, given:
|
||||
//
|
||||
// GOPATH /home/user
|
||||
// file /home/user/src/pkg/sub/file.go
|
||||
// fn.Name() pkg/sub.Type.Method
|
||||
//
|
||||
// We want to produce:
|
||||
//
|
||||
// pkg/sub/file.go
|
||||
//
|
||||
// From this we can easily see that fn.Name() has one less path separator
|
||||
// than our desired output. We count separators from the end of the file
|
||||
// path until it finds two more than in the function name and then move
|
||||
// one character forward to preserve the initial path segment without a
|
||||
// leading separator.
|
||||
const sep = "/"
|
||||
goal := strings.Count(name, sep) + 2
|
||||
i := len(file)
|
||||
for n := 0; n < goal; n++ {
|
||||
i = strings.LastIndex(file[:i], sep)
|
||||
if i == -1 {
|
||||
// not enough separators found, set i so that the slice expression
|
||||
// below leaves file unmodified
|
||||
i = -len(sep)
|
||||
break
|
||||
}
|
||||
}
|
||||
// get back to 0 or trim the leading separator
|
||||
file = file[i+len(sep):]
|
||||
return file
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue