mirror of
https://github.com/buger/goreplay.git
synced 2026-10-19 16:15:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46b6064a5e | ||
|
|
48fdc02d2f | ||
|
|
f2c9aee4ac | ||
|
|
aea32d9395 | ||
|
|
0089892b26 | ||
|
|
5d2cc68fff | ||
|
|
b0fff3c402 | ||
|
|
da09b14c8b | ||
|
|
e50ff77ca6 | ||
|
|
51860e130c | ||
|
|
9e7351d28c | ||
|
|
2c49556b2e | ||
|
|
d0e1858eb3 | ||
|
|
95eecb6b1c | ||
|
|
c4271ffaf4 | ||
|
|
f4e2b12d0f | ||
|
|
d193e24910 | ||
|
|
d36afeeb39 | ||
|
|
06d129d20d | ||
|
|
a3aeca204c | ||
|
|
35696a49c2 | ||
|
|
0fb8cec69b | ||
|
|
cefa2b5cc2 | ||
|
|
ae092d6a02 | ||
|
|
9a6186ce9c | ||
|
|
ede0fb4c4c | ||
|
|
651bd82d3d | ||
|
|
a4e5918534 | ||
|
|
ecfd8549a6 | ||
|
|
0f5b509397 | ||
|
|
c8a234b3dc | ||
|
|
b6f7de00c5 | ||
|
|
ba98e6e18a | ||
|
|
b01748bbdf | ||
|
|
75d217b846 | ||
|
|
c1f354a01c | ||
|
|
b900e38035 | ||
|
|
2f45fc8ef3 | ||
|
|
62b92576a0 | ||
|
|
98fe7f19b2 | ||
|
|
1c3af3ef6d | ||
|
|
98868e81c5 | ||
|
|
0ccace8cad | ||
|
|
a181b144d7 | ||
|
|
bf13dc59c3 | ||
|
|
d944ac6789 | ||
|
|
a52f56df2b | ||
|
|
2836b555e3 | ||
|
|
af6307e6ef | ||
|
|
38535a2055 | ||
|
|
ae7a3b7fa5 | ||
|
|
3fa1b66fc5 | ||
|
|
7496692d76 | ||
|
|
cdbf3b910e | ||
|
|
fe90601bff | ||
|
|
d40d6c6116 | ||
|
|
37d657fc33 | ||
|
|
2f86d005ad | ||
|
|
9bed47e4d0 | ||
|
|
61605aefb2 | ||
|
|
eb9e073af8 | ||
|
|
163f4602e4 | ||
|
|
fee36a486d | ||
|
|
854ea15900 | ||
|
|
f8349e4488 | ||
|
|
aac1345d16 | ||
|
|
663bd8b95d | ||
|
|
19bfb8211c | ||
|
|
cbc871b83f | ||
|
|
768c5f9726 | ||
|
|
47bb312b73 | ||
|
|
fb9cf68ff9 | ||
|
|
759efce521 | ||
|
|
af831381aa | ||
|
|
85e84ce330 | ||
|
|
2bc441e0be | ||
|
|
7ee06e84f4 | ||
|
|
4109336a43 | ||
|
|
3ada9b63fd | ||
|
|
c41608e48c | ||
|
|
48995cc41b | ||
|
|
6a3b791f7b | ||
|
|
ddaa0e9d74 | ||
|
|
281219e6a0 | ||
|
|
ba227ae9ba | ||
|
|
5779ea1b45 | ||
|
|
bb0fab4d35 | ||
|
|
8cc377f22f | ||
|
|
5bea65c723 | ||
|
|
52a0ebfd57 | ||
|
|
a8731f290c | ||
|
|
2f0e865e82 | ||
|
|
afc8f7852e | ||
|
|
00feb08165 | ||
|
|
0754ed7d83 | ||
|
|
4baf3985dc | ||
|
|
32264eb427 | ||
|
|
f2cedaf89f | ||
|
|
e8f7d30551 | ||
|
|
0c7643f477 | ||
|
|
4c8a73a58b | ||
|
|
3d7455d6c1 | ||
|
|
3c40ac7ab6 | ||
|
|
e0e065ba8a | ||
|
|
fc1771ef70 | ||
|
|
5ec67ee88d | ||
|
|
500d80d261 | ||
|
|
eb395ff815 | ||
|
|
538c05ec62 | ||
|
|
d99117c92b | ||
|
|
e659f65d76 | ||
|
|
7a2f1aef4c | ||
|
|
6c410ab978 | ||
|
|
a3403a3295 | ||
|
|
cb4db10caf | ||
|
|
55405b6bfc | ||
|
|
fecfa7f15f | ||
|
|
eb6166724b | ||
|
|
dbd8a48561 | ||
|
|
d6f5ac1c3d | ||
|
|
334db15df9 | ||
|
|
d3e58ba374 | ||
|
|
7dee2dbefb | ||
|
|
1014abf878 | ||
|
|
fa11cc04c8 | ||
|
|
4cfa33f89c | ||
|
|
a62f269d3e | ||
|
|
7b2d7800b4 | ||
|
|
e5daaf067e | ||
|
|
9fc7cdc0d3 | ||
|
|
94f066c97a | ||
|
|
7f27d60f8a | ||
|
|
ad2bf0334c | ||
|
|
699eefe317 | ||
|
|
2a955dcdbf | ||
|
|
28f82b462e | ||
|
|
1fdcc54121 | ||
|
|
5378ecca71 | ||
|
|
9d861af3a2 | ||
|
|
5c272661e9 | ||
|
|
a4c40cd8b4 | ||
|
|
d23848167d | ||
|
|
44506785cb | ||
|
|
bb2adbce65 | ||
|
|
028bc6c34e | ||
|
|
16c5b6f15b | ||
|
|
5cf8f7dfd0 | ||
|
|
2b522285c6 | ||
|
|
5dcec259d6 | ||
|
|
766bbc6127 | ||
|
|
6d2a895db3 | ||
|
|
d81e39d25c | ||
|
|
ed94985346 | ||
|
|
f378def0b3 | ||
|
|
cd83e6f9ba | ||
|
|
60ddd535bc | ||
|
|
9ded255788 | ||
|
|
323f85bcfb | ||
|
|
07a3c191d7 | ||
|
|
4f3354507c | ||
|
|
661dd2026f | ||
|
|
67505d99d8 | ||
|
|
ac0438995c | ||
|
|
b5aa7d7c2d | ||
|
|
8daa7fdcab | ||
|
|
1fea397d54 | ||
|
|
abde6ecfcb | ||
|
|
43262f3336 | ||
|
|
fe91a20629 | ||
|
|
3c81723ac3 | ||
|
|
929e027907 | ||
|
|
f0b96af420 | ||
|
|
f3ea30c6df | ||
|
|
d5a1eb0b9d | ||
|
|
02bf7a86de | ||
|
|
cbc27bd662 | ||
|
|
16d7f23d98 | ||
|
|
6533b50ab0 | ||
|
|
9309fb86ef | ||
|
|
76d5e22ba1 | ||
|
|
81d3508653 | ||
|
|
3c45595a96 | ||
|
|
6974a6e278 | ||
|
|
33cdcd4d13 | ||
|
|
9550b31fde | ||
|
|
6cb4c265dd | ||
|
|
e54f77f229 | ||
|
|
5d2f6eceea | ||
|
|
a6e29c684c | ||
|
|
78456d6cd1 | ||
|
|
c4037e0c57 | ||
|
|
7cff78174a | ||
|
|
b53fba0415 | ||
|
|
d6c776fa0c | ||
|
|
ae35f88004 | ||
|
|
8da4e54cbb | ||
|
|
7790b33cea | ||
|
|
a971d8a843 | ||
|
|
3e763c43cf | ||
|
|
ea94dd0162 | ||
|
|
d96e3393ec | ||
|
|
69424bf670 | ||
|
|
211be0cddd | ||
|
|
d1091102ba | ||
|
|
0d445722b9 | ||
|
|
879ed48b80 | ||
|
|
ddd5190622 | ||
|
|
a18079fa84 | ||
|
|
1a36374c2d | ||
|
|
41c1421a1b | ||
|
|
4f2341b2d3 | ||
|
|
56f0a3fcc0 | ||
|
|
70e7d8dd29 | ||
|
|
bb319b5fd2 | ||
|
|
491541c2df | ||
|
|
2990fab635 | ||
|
|
8046468ea4 | ||
|
|
0009555d96 | ||
|
|
5098e388a2 | ||
|
|
5667cd35c6 | ||
|
|
9ab8b3c113 | ||
|
|
5e526af47b | ||
|
|
3a7badc508 | ||
|
|
9a76e354c5 | ||
|
|
d6b5c675cf | ||
|
|
de6c41c731 | ||
|
|
08933e148a | ||
|
|
259b70a7df | ||
|
|
35854196f0 | ||
|
|
a9be1811ea | ||
|
|
9185453453 | ||
|
|
87ad851ad8 | ||
|
|
ce193d0d4f | ||
|
|
f92a0b1242 | ||
|
|
f6864d05a0 | ||
|
|
3316447bf5 | ||
|
|
961d5a754c | ||
|
|
dd68d07c75 | ||
|
|
5c46db9220 | ||
|
|
ed7c5210be | ||
|
|
00ad6bd92b | ||
|
|
587b9ad2d5 | ||
|
|
083c97a9c4 | ||
|
|
8f2e9bd1dc | ||
|
|
a79e54c5f1 | ||
|
|
27f83a6c0c | ||
|
|
5158f75dc8 | ||
|
|
126957c599 | ||
|
|
d62eccb655 | ||
|
|
5cc4228dcd | ||
|
|
5e6f7e03a7 | ||
|
|
0a30582131 | ||
|
|
5c21600bf9 | ||
|
|
107982394e | ||
|
|
ac3e209787 | ||
|
|
e5d9f73937 | ||
|
|
df3b410e98 | ||
|
|
125d9db834 | ||
|
|
6147e14f88 | ||
|
|
fa003f5fec | ||
|
|
28540f43a1 | ||
|
|
6158ee3025 | ||
|
|
0859642b38 | ||
|
|
32081b2d17 | ||
|
|
6647481fe0 | ||
|
|
0551b19ddd | ||
|
|
a5f2d2fd6d | ||
|
|
f1c341e2ac | ||
|
|
becb4b958f | ||
|
|
db7f5c6a9c | ||
|
|
7af0eb2d65 | ||
|
|
32d0557a28 | ||
|
|
6ba0bfc614 | ||
|
|
34fe137d19 | ||
|
|
bb74704f9d | ||
|
|
d0873d1453 | ||
|
|
313c382adb | ||
|
|
6b9c9b91e3 | ||
|
|
531bd1da67 | ||
|
|
acaf935cae | ||
|
|
0fe72cc432 | ||
|
|
f812120dfc | ||
|
|
e64aac9364 | ||
|
|
8db14e334d | ||
|
|
b38cb458ff | ||
|
|
d380b71945 | ||
|
|
fd5388b6c9 | ||
|
|
48492dc8cb | ||
|
|
29035d4518 | ||
|
|
1ce10249be | ||
|
|
5a23573cfe | ||
|
|
413f50129f | ||
|
|
686d7436ae | ||
|
|
18c0473239 | ||
|
|
4d00ff8757 | ||
|
|
8f73233717 | ||
|
|
07c65117ad | ||
|
|
31fa612fef | ||
|
|
9ddb718525 | ||
|
|
4f114f067b | ||
|
|
9a0d9166c3 | ||
|
|
bc48e1ad49 | ||
|
|
53c27cf374 | ||
|
|
a859ae02ad | ||
|
|
2b17989fd9 | ||
|
|
96fd713453 | ||
|
|
43fd14b561 | ||
|
|
72b15db93f | ||
|
|
557cdbebb6 | ||
|
|
925ff151dc | ||
|
|
481e4f2e74 | ||
|
|
416586a365 | ||
|
|
a08ecc79fc | ||
|
|
decb9483a5 | ||
|
|
21c544ef67 | ||
|
|
122f06f6c2 | ||
|
|
3d3c6a34b1 | ||
|
|
04d727915d | ||
|
|
1e7b603ae0 | ||
|
|
41294f9470 | ||
|
|
3e86456e36 | ||
|
|
98f941b69a | ||
|
|
674ffc6202 | ||
|
|
4f03f81d04 | ||
|
|
dcb8a1dee4 | ||
|
|
451a1cd11a | ||
|
|
6fe7ba8e86 | ||
|
|
534e9b29fa | ||
|
|
390f75fd3c | ||
|
|
a059648008 | ||
|
|
b2e1eea6bf | ||
|
|
92ba321569 | ||
|
|
2b497e6177 | ||
|
|
09f9cac6fb | ||
|
|
d05bc0d5e9 | ||
|
|
1c0182188c | ||
|
|
cef8b75496 | ||
|
|
e9fd5eb273 | ||
|
|
85fbfff308 | ||
|
|
a7aaf7df65 | ||
|
|
c1c87ed4e2 | ||
|
|
072f332956 | ||
|
|
7c68920a3b | ||
|
|
b6351a8525 | ||
|
|
f557d412c4 | ||
|
|
ed5c2215f5 | ||
|
|
208faec7d2 | ||
|
|
ffe1646d8e | ||
|
|
ae293320ad | ||
|
|
eb7885cd56 | ||
|
|
b6977df90d | ||
|
|
15417894a9 | ||
|
|
7f49314ec7 | ||
|
|
80572f5f18 | ||
|
|
b7567bbd15 | ||
|
|
e4b7c8c763 | ||
|
|
62ced2e1ba | ||
|
|
79ed6a7548 | ||
|
|
3793a84e59 | ||
|
|
f2cf81c5bf | ||
|
|
aae57733a8 | ||
|
|
00df525b6a | ||
|
|
7864808e5e | ||
|
|
58c7e12d1f | ||
|
|
de143e4268 | ||
|
|
67cfca0ede | ||
|
|
43920a96c0 | ||
|
|
4cf3bf0ae4 | ||
|
|
095c717f6a | ||
|
|
abc44c2fa5 | ||
|
|
2d66ccc62d | ||
|
|
b257231d51 | ||
|
|
1cab1ecb0e | ||
|
|
983bc728fa | ||
|
|
4b592e0e2d | ||
|
|
1637dd491f | ||
|
|
9e63fb5b36 | ||
|
|
04a8cceb35 | ||
|
|
6ddf4df21a | ||
|
|
51f5a3e9ec | ||
|
|
4c0c9e97c5 | ||
|
|
4f64b71adb | ||
|
|
be7e27010d | ||
|
|
758995d8f5 | ||
|
|
5f883e08ce | ||
|
|
3a43e697d5 | ||
|
|
468d94610f | ||
|
|
d0266303d1 | ||
|
|
82ed628d45 | ||
|
|
97dd1520f1 | ||
|
|
972a80356d | ||
|
|
91a0282755 | ||
|
|
2c03d3ee28 | ||
|
|
b2f90789d2 | ||
|
|
e68332b897 | ||
|
|
cf0dfecac2 | ||
|
|
4f9c983bc7 | ||
|
|
988c6abdad | ||
|
|
864723fe47 | ||
|
|
2c2d5439bf | ||
|
|
c04b2528b8 | ||
|
|
58944fd42d | ||
|
|
6e125a7b7b | ||
|
|
1a7cf2bbab | ||
|
|
e022fdff98 | ||
|
|
c0dc1b3a53 | ||
|
|
674d6036d1 | ||
|
|
6f6c6aba95 | ||
|
|
2a6e9f7a92 | ||
|
|
007e0f3019 | ||
|
|
115a37e0e9 | ||
|
|
b2c87ff2a4 | ||
|
|
3a1efccbca | ||
|
|
a2873116a3 | ||
|
|
ccb309170f | ||
|
|
defc017ade | ||
|
|
9e2f7feec7 | ||
|
|
c56bbd98ce | ||
|
|
787a906bb8 | ||
|
|
d608d18057 | ||
|
|
cd27667246 | ||
|
|
bae1a84f40 | ||
|
|
057e642b87 | ||
|
|
c94cc88608 | ||
|
|
731a3745a4 | ||
|
|
c4307b750a | ||
|
|
9d341738de | ||
|
|
c8d623580a | ||
|
|
2e29e3ab9e | ||
|
|
bf786e5922 | ||
|
|
a9a08860b4 | ||
|
|
c6c021d65f | ||
|
|
49da801b33 | ||
|
|
a4a4c605a5 | ||
|
|
11d9531829 | ||
|
|
a6af811c50 | ||
|
|
7bf6ca4c87 | ||
|
|
31aef9ee1d | ||
|
|
41fe11c12f | ||
|
|
4720070996 | ||
|
|
dbfea7ceac | ||
|
|
c49c19428d | ||
|
|
7bf2015e90 | ||
|
|
db14154eb9 | ||
|
|
b52d562dad | ||
|
|
9461217a0e | ||
|
|
40d835e143 | ||
|
|
c97f02ff4c | ||
|
|
62254b6265 | ||
|
|
d5535ee7f3 | ||
|
|
959ac29661 | ||
|
|
b5c2d7747c | ||
|
|
610552be88 | ||
|
|
c250c3e30c | ||
|
|
ce5ee74d74 | ||
|
|
c4d0eb0ae1 | ||
|
|
840ca61de9 | ||
|
|
5cb3562796 | ||
|
|
384b3c5614 | ||
|
|
e97827d600 | ||
|
|
3eed7c2546 | ||
|
|
fa8f612a4f | ||
|
|
b00ea39d3e | ||
|
|
06b4892b33 | ||
|
|
06b08105bf | ||
|
|
8ccc0249ca | ||
|
|
e9bb6ef8f3 | ||
|
|
adf7ed6b84 | ||
|
|
0dc315b343 | ||
|
|
48ae0e6812 | ||
|
|
0d398dbf3d | ||
|
|
e373073742 | ||
|
|
296034ef00 | ||
|
|
a786c78d26 | ||
|
|
ed68b27153 | ||
|
|
e6fe38cfda | ||
|
|
441f0ade04 | ||
|
|
8d8c10f3ea | ||
|
|
fc52decebf | ||
|
|
f5d74e84f6 | ||
|
|
983eb7c6a8 | ||
|
|
8edbb7fb08 | ||
|
|
89f1b50b8d | ||
|
|
9b0153f45f | ||
|
|
832bff26d1 | ||
|
|
3d3cb1dbf5 | ||
|
|
ca952b7a8b | ||
|
|
872a4dcae9 | ||
|
|
b70290274e | ||
|
|
787d92d94a | ||
|
|
ae5f7e48b0 | ||
|
|
29deb996bd | ||
|
|
02fe5a976f | ||
|
|
c8e39b8f47 | ||
|
|
c5150cd911 | ||
|
|
7a657e273e | ||
|
|
cc2dd53de8 | ||
|
|
cad94ead29 | ||
|
|
28f7078dbb | ||
|
|
841bba116a | ||
|
|
3a3ae5b283 | ||
|
|
3299955888 | ||
|
|
3aa04783f9 | ||
|
|
5a7c817f0b | ||
|
|
b800896b04 | ||
|
|
e7a50ac5d4 | ||
|
|
0fb1d83284 | ||
|
|
1784516771 | ||
|
|
9fa5ef74fb | ||
|
|
d67bea5e10 | ||
|
|
263d1347a0 | ||
|
|
19008c2a2a | ||
|
|
49cdbbe41b | ||
|
|
59a2b7ca1d | ||
|
|
9a32742653 | ||
|
|
38b171d357 | ||
|
|
90261550c4 | ||
|
|
7814bd8bc5 | ||
|
|
40cd01630b | ||
|
|
19eadf49b9 | ||
|
|
227e95467a | ||
|
|
14d9ca6d54 | ||
|
|
b9c97eabaf | ||
|
|
d9c54d31ad | ||
|
|
796f192e71 | ||
|
|
eb6d0cddd9 | ||
|
|
f82dade127 | ||
|
|
5f35f41637 | ||
|
|
26ddfcd3cd | ||
|
|
ac3f255d38 | ||
|
|
68ba87c19e | ||
|
|
fd9f6cd056 | ||
|
|
9bba1296bd | ||
|
|
f7b3adc8cd | ||
|
|
dd806640f9 | ||
|
|
824c51248c | ||
|
|
f8a8444b27 | ||
|
|
c811dc017e | ||
|
|
c2424b2b8f | ||
|
|
ccf6723743 | ||
|
|
fa9c9c3387 | ||
|
|
8620e4a467 | ||
|
|
457e92ed11 | ||
|
|
9da6f908fd | ||
|
|
4406794a28 | ||
|
|
92c8dd19f4 | ||
|
|
860189b1c5 | ||
|
|
0f27a45fbb | ||
|
|
8ffdd10988 | ||
|
|
6a136b4686 | ||
|
|
af3a7a7541 | ||
|
|
602448accd | ||
|
|
8b9c72ef9e | ||
|
|
95d3676d83 | ||
|
|
72952fddd2 | ||
|
|
faa3cb7307 | ||
|
|
ce76453fa6 | ||
|
|
9dddab58d3 | ||
|
|
c42a42928b | ||
|
|
2a4efa91af | ||
|
|
2fd36f03f1 | ||
|
|
3b890c6d7f | ||
|
|
bc4fabd0da | ||
|
|
24a240c819 | ||
|
|
2f049dddbd | ||
|
|
e7c1d86bfa | ||
|
|
714307890e | ||
|
|
69df96e55d | ||
|
|
9c6802f5e8 | ||
|
|
d54da99667 | ||
|
|
f4af3a9bd4 | ||
|
|
1f9c9a6154 | ||
|
|
9e175434d6 | ||
|
|
9a126103fc | ||
|
|
9a978422b2 | ||
|
|
e02c145ac6 | ||
|
|
7d482d47de | ||
|
|
f7cf05c257 | ||
|
|
4af1230d83 | ||
|
|
18597d9e7a | ||
|
|
fe02bfa8c5 | ||
|
|
48d4406893 | ||
|
|
f07c7f5eaf | ||
|
|
db4b2c26b0 | ||
|
|
8ecda0fdb5 | ||
|
|
06befe3b4b | ||
|
|
8c41c01d4b | ||
|
|
4e41151963 | ||
|
|
18ec957e62 | ||
|
|
857a9b4835 | ||
|
|
12ae1455f8 | ||
|
|
da2478ca3c | ||
|
|
c32deec006 | ||
|
|
f5a8274fcd | ||
|
|
d1708ff1f3 | ||
|
|
fd642bded1 | ||
|
|
40203d2519 | ||
|
|
fd27e13e85 | ||
|
|
d46e587c08 | ||
|
|
fa2dedceb3 | ||
|
|
983e2a0e79 | ||
|
|
42173e4687 | ||
|
|
8757373a7a | ||
|
|
b6e7b18803 | ||
|
|
e21f9b024d | ||
|
|
20901ec6ae | ||
|
|
327ee32395 | ||
|
|
e32b985df2 | ||
|
|
737ea9fc68 | ||
|
|
509b27c476 | ||
|
|
94199fcb25 | ||
|
|
bba9637eed | ||
|
|
685c998e8e | ||
|
|
8adeda9a98 | ||
|
|
9077656bdf | ||
|
|
830ae237c9 | ||
|
|
88dd2602df | ||
|
|
27582841fe | ||
|
|
b53218e729 | ||
|
|
54e9de5088 | ||
|
|
5782d486f9 | ||
|
|
8b66a99665 | ||
|
|
30547f2a41 | ||
|
|
140267b6a6 | ||
|
|
e4807b7abb | ||
|
|
20ea55e6e7 | ||
|
|
47b498d4d9 | ||
|
|
4305b72608 | ||
|
|
578d785c27 | ||
|
|
6946fc6036 | ||
|
|
8535872b10 | ||
|
|
a1f9cc501b | ||
|
|
f07586a12e | ||
|
|
fa684d4645 | ||
|
|
c3330cb4be | ||
|
|
3cb98aaf94 | ||
|
|
cbc4204e50 | ||
|
|
207eea1ba7 | ||
|
|
de93abeab8 | ||
|
|
9908e8d4d9 | ||
|
|
e7b2866ca9 | ||
|
|
b562a6b62d | ||
|
|
0ac36b4fbf | ||
|
|
16ba3d1514 | ||
|
|
5b6b8a53c1 | ||
|
|
1a35a2c651 | ||
|
|
5a4c27dc9e | ||
|
|
e3d1fb4e72 | ||
|
|
0233ef7efb | ||
|
|
4a68bea49c | ||
|
|
fd47cc1144 | ||
|
|
26455e4e52 | ||
|
|
5aca51e5b1 | ||
|
|
af45f8246e | ||
|
|
5bae7c5de5 | ||
|
|
335c86c6e9 | ||
|
|
f1eedc78a1 | ||
|
|
cfb65c06ac | ||
|
|
02abcfdf74 | ||
|
|
1e08ec3953 | ||
|
|
db369cfc89 | ||
|
|
54a3d73a98 | ||
|
|
ae792afa5e | ||
|
|
700523e0c3 | ||
|
|
95841d597e | ||
|
|
5ccebd008d | ||
|
|
e5cb5d959d | ||
|
|
4f7b201ef1 | ||
|
|
6bb24697b2 | ||
|
|
719fd471f3 | ||
|
|
af2d47817b | ||
|
|
2c34f291fb | ||
|
|
041c20ef38 | ||
|
|
d425a7c9a8 | ||
|
|
528980c87e | ||
|
|
c899791d4c | ||
|
|
b6cce83b1b | ||
|
|
09175eade0 | ||
|
|
3c84a0c0d9 | ||
|
|
77751af316 | ||
|
|
00d3103451 | ||
|
|
e42c3a96e2 | ||
|
|
a9ca7ea7ff | ||
|
|
40a48863fb | ||
|
|
97f2bb7740 | ||
|
|
6ac803454b | ||
|
|
ec106e4b4e | ||
|
|
1f9a1ac3db | ||
|
|
4c9cf3fa6a | ||
|
|
a89f1c75c8 | ||
|
|
0bb7e9f7b8 | ||
|
|
3b68ae27a6 | ||
|
|
476da0f1e6 | ||
|
|
f6038b26b7 | ||
|
|
668e33c128 | ||
|
|
4066c24960 | ||
|
|
1686c701bf | ||
|
|
8fac086d02 | ||
|
|
1cc94bc7ac | ||
|
|
d231d64abb | ||
|
|
9a3c3f1e6d | ||
|
|
c6fa4b8332 | ||
|
|
85b54627ae | ||
|
|
97a5e99c6b | ||
|
|
9490c743c3 | ||
|
|
bc65507c8d | ||
|
|
203f282bad | ||
|
|
88cb352353 | ||
|
|
2ba6dacb64 | ||
|
|
8defc2062e | ||
|
|
76e5981f49 | ||
|
|
5910409379 | ||
|
|
83f16946c7 | ||
|
|
28702cd503 | ||
|
|
35f2f71199 | ||
|
|
15f23270df | ||
|
|
6ba587e82e | ||
|
|
cad13f4233 | ||
|
|
fcadc57696 | ||
|
|
28bbb9abf9 | ||
|
|
2709bba3c2 | ||
|
|
2e13a7930b | ||
|
|
c62ce5513f | ||
|
|
4bcb3e1ca6 |
@@ -0,0 +1,3 @@
|
||||
*.tar.gz
|
||||
gor
|
||||
gor.test
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
*.swp
|
||||
*.gor
|
||||
|
||||
*.out
|
||||
|
||||
*.bin
|
||||
|
||||
*.gz
|
||||
|
||||
*.class
|
||||
|
||||
*.test
|
||||
.idea
|
||||
*.iml
|
||||
gor
|
||||
|
||||
*.mprof
|
||||
@@ -0,0 +1,6 @@
|
||||
language: go
|
||||
go: 1.6
|
||||
script: sudo -E bash -c "source /etc/profile && eval '$(gimme 1.6)' && export GOPATH=$HOME/gopath:$GOPATH && go get && GORACE='halt_on_error=1' go test ./... -v -timeout 120s -race"
|
||||
|
||||
before_install:
|
||||
- sudo apt-get install libpcap-dev -y
|
||||
@@ -1,5 +0,0 @@
|
||||
v0.3 - 10 Jun 2013
|
||||
* Use RAW_SOCKETS instead of tcpdump
|
||||
* Own TCP stack
|
||||
* All HTTP request types support
|
||||
* Simplified request parsing
|
||||
@@ -0,0 +1,95 @@
|
||||
END-USER LICENSE AGREEMENT
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
IMPORTANT: THIS SOFTWARE END-USER LICENSE AGREEMENT ("EULA") IS A LEGAL AGREEMENT (“Agreement”) BETWEEN YOU (THE CUSTOMER, EITHER AS AN INDIVIDUAL OR, IF PURCHASED OR OTHERWISE ACQUIRED BY OR FOR AN ENTITY, AS AN ENTITY) AND REPLAY SOFTWARE. READ IT CAREFULLY BEFORE COMPLETING THE INSTALLATION PROCESS AND USING GOR PRO AND RELATED SOFTWARE COMPONENTS (“SOFTWARE”). IT PROVIDES A LICENSE TO USE THE SOFTWARE AND CONTAINS WARRANTY INFORMATION AND LIABILITY DISCLAIMERS. BY INSTALLING AND USING THE SOFTWARE, YOU ARE CONFIRMING YOUR ACCEPTANCE OF THE SOFTWARE AND AGREEING TO BECOME BOUND BY THE TERMS OF THIS AGREEMENT.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
In order to use the Software under this Agreement, you must receive a “Source URL” at the time of purchase, in accordance with the scope of use and other terms specified for each type of Software and as set forth in this Section 1 of this Agreement.
|
||||
|
||||
1. License Grant
|
||||
|
||||
1.1 General Use. This Agreement grants you a non-exclusive, non-transferable, limited license to the use rights for the Software, without the right to grant sublicenses, subject to the terms and conditions in this Agreement. The Software is licensed, not sold.
|
||||
|
||||
1.2 Unlimited Organization License. If you purchased an Organization License (included with the Gor Pro Software), you may install the Software on an unlimited number of Hosts. “Host” means any physical or virtual machine which is controlled by you. You may also run an unlimited number of Workers. “Worker” means a thread within a Gor server process which executes jobs. You may concurrently run the software on an unlimited number of Hosts, with each host running an unlimited number of Workers.
|
||||
|
||||
1.3 Limited Organization License. If you purchased an Organization License (included with the Gor Enterprise Software), you may install the Software on an unlimited number of Hosts. “Host” means any physical or virtual machine which is controlled by you. The aggregate number of Workers run by the hosts must not exceed the maximum number of Workers authorized at the time of purchase. “Worker” means a running Gor instance which intercept or replay traffic. In order to run additional Workers, you must purchase an additional allowance from Replay Software.
|
||||
|
||||
1.4 Appliance License. If you purchased an Appliance License, you may distribute the Software in any applications, frameworks, or elements (collectively referred to as an “Application” or “Applications”) that you develop using the Software in accordance with this EULA, provided that such distribution does not violate the restrictions set forth in section 3 of this EULA. You must not remove, obscure or interfere with any copyright, acknowledgment, attribution, trademark, warning or disclaimer statement affixed to, incorporated in or otherwise applied in connection with the Software. You are required to ensure that the Software is not reused by or with any applications other than those with which you distribute it as permitted herein. For example, if You install the Software on a customer’s server, that customer is not permitted to use the Software independently of your Application. You must inform Replay Software of your knowledge of any infringing use of the Software by any of your customers. You are liable for compliance by those third parties with the terms and conditions of this EULA. You will not owe Replay Software any royalties for your distribution of the Software in accordance with this EULA.
|
||||
|
||||
1.5 Archive Copies. You are entitled to make a reasonable amount of copies of the Software for archival purposes. Each copy must reproduce all copyright and other proprietary rights notices on or in the Software Product.
|
||||
|
||||
1.6 Electronic Delivery. All Software and license documentation shall be delivered by electronic means unless otherwise specified on the applicable invoice or at the time of purchase. Software shall be deemed delivered when it is made available for download by you (“Delivery”).
|
||||
|
||||
2. Modifications. Replay Software shall provide you with source code so that you can create Modifications of the original software. “Modification” means: (a) any addition to or deletion from the contents of a file included in the original Software or previous Modifications created by You, or (b) any new file that contains any part of the original Software or previous Modifications. While you retain all rights to any original work authored by you as part of the Modifications, We continue to own all copyright and other intellectual property rights in the Software.
|
||||
|
||||
3. Restricted Uses.
|
||||
|
||||
3.1 You shall not (and shall not allow any third party to): (a) decompile, disassemble, or otherwise reverse engineer the Software or attempt to reconstruct or discover any source code, underlying ideas, algorithms, file formats or programming interfaces of the Software by any means whatsoever (except and only to the extent that applicable law prohibits or restricts reverse engineering restrictions); (b) distribute, sell, sublicense, rent, lease or use the Software for time sharing, hosting, service provider or like purposes, except as expressly permitted under this Agreement; (c) redistribute the Software or Modifications other than by including the Software or a portion thereof within your own product, which must have substantially different functionality than the Software or Modifications and must not allow any third party to use the Software or Modifications, or any portions thereof, for software development or application development purposes; (d) redistribute the Software as part of a product, "appliance" or "virtual server"; (e) redistribute the Software on any server which is not directly under your control; (f) remove any product identification, proprietary, copyright or other notices contained in the Software; (g) modify any part of the Software, create a derivative work of any part of the Software (except as permitted in Section 4), or incorporate the Software, except to the extent expressly authorized in writing by Replay Software; (h) publicly disseminate performance information or analysis (including, without limitation, benchmarks) from any source relating to the Software; (i) utilize any equipment, device, software, or other means designed to circumvent or remove any form of Source URL or copy protection used by Replay Software in connection with the Software, or use the Software together with any authorization code, Source URL, serial number, or other copy protection device not supplied by Replay Software; (j) use the Software to develop a product which is competitive with any Replay Software product offerings; or (k) use unauthorized Source URLS or keycode(s) or distribute or publish Source URLs or keycode(s), except as may be expressly permitted by Replay Software in writing. If your unique Source URL is ever published, Replay Software reserves the right to terminate your access without notice.
|
||||
|
||||
3.2 UNDER NO CIRCUMSTANCES MAY YOU USE THE SOFTWARE AS PART OF A PRODUCT OR SERVICE THAT PROVIDES SIMILAR FUNCTIONALITY TO THE SOFTWARE ITSELF.
|
||||
|
||||
The Open Source version of the Software (“LGPL Version”) is licensed
|
||||
under the terms of the GNU Lesser General Public License versions 3.0
|
||||
(“LGPL”) and not under this EULA.
|
||||
|
||||
4. Ownership. Notwithstanding anything to the contrary contained herein, except for the limited license rights expressly provided herein, Replay Software and its suppliers have and will retain all rights, title and interest (including, without limitation, all patent, copyright, trademark, trade secret and other intellectual property rights) in and to the Software and all copies, modifications and derivative works thereof (including any changes which incorporate any of your ideas, feedback or suggestions). You acknowledge that you are obtaining only a limited license right to the Software, and that irrespective of any use of the words “purchase”, “sale” or like terms hereunder no ownership rights are being conveyed to you under this Agreement or otherwise.
|
||||
|
||||
5. Fees and Payment. The Software license fees will be due and payable in full as set forth in the applicable invoice or at the time of purchase. If the Software does not function properly within two weeks of purchase, please contact us within those two weeks for a refund. You shall be responsible for all taxes, withholdings, duties and levies arising from the order (excluding taxes based on the net income of Replay Software).
|
||||
|
||||
6. Support, Maintenance and Services. Subject to the terms and conditions of this Agreement, as set forth in your invoice, and as set forth on the Gor Pro support page (https://github.com/buger/gor/wiki/Pro-Support), support and maintenance services may be included with the purchase of your license subscription.
|
||||
|
||||
7. Term of Agreement.
|
||||
|
||||
7.1 Term. This Agreement is effective as of the Delivery of the Software and expires at such time as all license and service subscriptions hereunder have expired in accordance with their own terms (the “Term”). For clarification, the term of your license under this Agreement may be perpetual, limited for Evaluation Version, or designated as a fixed-term license in the Invoice, and shall be specified at your time of purchase. Either party may terminate this Agreement (including all related Invoices) if the other party: (a) fails to cure any material breach of this Agreement within thirty (30) days after written notice of such breach, provided that Replay Software may terminate this Agreement immediately upon any breach of Section 3 or if you exceed any other restrictions contained in Section 1, unless otherwise specified in this agreement; (b) ceases operation without a successor; or (c) seeks protection under any bankruptcy, receivership, trust deed, creditors arrangement, composition or comparable proceeding, or if any such proceeding is instituted against such party (and not dismissed within sixty (60) days)). Termination is not an exclusive remedy and the exercise by either party of any remedy under this Agreement will be without prejudice to any other remedies it may have under this Agreement, by law, or otherwise.
|
||||
|
||||
7.2 Termination. Upon any termination of this Agreement, you shall cease any and all use of any Software and destroy all copies thereof.
|
||||
|
||||
7.3 Expiration of License. Upon the expiration of any term under this Agreement, (a) all Software updates and services pursuant to the license shall cease, (b) you may only continue to run existing installations of the Software, (c) you may not install the Software on any additional Hosts, and (d) any new installation of the Software shall require the purchase of a new license subscription from Replay Software.
|
||||
|
||||
8. Disclaimer of Warranties. The Software is provided "as is," with all faults, defects and errors, and without warranty of any kind. Replay Software does not warrant that the Software will be free of bugs, errors, viruses or other defects, and Replay Software shall have no liability of any kind for the use of or inability to use the Software, the Software content or any associated service, and you acknowledge that it is not technically practicable for Replay Software to do so.
|
||||
To the maximum extent permitted by applicable law, Replay Software disclaims all warranties, express, implied, arising by law or otherwise, regarding the Software, the Software content and their respective performance or suitability for your intended use, including without limitation any implied warranty of merchantability, fitness for a particular purpose.
|
||||
|
||||
9. Limitation of Liability.
|
||||
|
||||
In no event will Replay Software be liable for any direct, indirect, consequential, incidental, special, exemplary, or punitive damages or liabilities whatsoever arising from or relating to the Software, the Software content or this Agreement, whether based on contract, tort (including negligence), strict liability or other theory, even if Replay Software has been advised of the possibility of such damages.
|
||||
|
||||
In no event will Replay Software' liability exceed the Software license price as indicated in the invoice. The existence of more than one claim will not enlarge or extend this limit.
|
||||
|
||||
10. Remedies. Your exclusive remedy and Replay Software’ entire liability for breach of this Agreement shall be limited, at Replay Software’ sole and exclusive discretion, to (a) replacement of any defective software or documentation; or (b) refund of the license fee paid to Replay Software, payable in accordance with Replay Software' refund policy.
|
||||
|
||||
11. Acknowledgements.
|
||||
|
||||
11.1 Consent to the Use of Data. You agree that Replay Software and its affiliates may collect and use technical information gathered as part of the product support services. Replay Software may use this information solely to improve products and services and will not disclose this information in a form that personally identifies you.
|
||||
|
||||
11.2 Verification. We or a certified auditor acting on our behalf, may, upon its reasonable request and at its expense, audit you with respect to the use of the Software. Such audit may be conducted by mail, electronic means or through an in-person visit to your place of business. Any such in-person audit shall be conducted during regular business hours at your facilities and shall not unreasonably interfere with your business activities. We shall not remove, copy, or redistribute any electronic material during the course of an audit. If an audit reveals that you are using the Software in a way that is in material violation of the terms of the EULA, then you shall pay our reasonable costs of conducting the audit. In the case of a material violation, you agree to pay Us any amounts owing that are attributable to the unauthorized use. In the alternative, We reserve the right, at our sole option, to terminate the licenses for the Software.
|
||||
|
||||
11.3 Government End Users. If the Software and related documentation are supplied to or purchased by or on behalf of the United States Government, then the Software is deemed to be "commercial software" as that term is used in the Federal Acquisition Regulation system. Rights of the United States shall not exceed the minimum rights set forth in FAR 52.227-19 for "restricted computer software". All other terms and conditions of this Agreement apply.
|
||||
|
||||
12. Third Party Software. Examples included in Software may provide links to third party libraries or code (collectively “Third Party Software”) to implement various functions. Third Party Software does not comprise part of the Software. In some cases, access to Third Party Software may be included along with the Software delivery as a convenience for demonstration purposes. Such source code and libraries may be included in the “…/examples” source tree delivered with the Software and do not comprise the Software. Licensee acknowledges (1) that some part of Third Party Software may require additional licensing of copyright and patents from the owners of such, and (2) that distribution of any of the Software referencing or including any portion of a Third Party Software may require appropriate licensing from such third parties.
|
||||
|
||||
|
||||
13. Miscellaneous
|
||||
|
||||
13.1 Entire Agreement. This Agreement sets forth our entire agreement with respect to the Software and the subject matter hereof and supersedes all prior and contemporaneous understandings and agreements whether written or oral.
|
||||
|
||||
13.2 Amendment. Replay Software reserves the right, in its sole discretion, to amend this Agreement from time. Amendments to this Agreement can be located at: https://github.com/buger/gor/blob/master/COMM-LICENSE.
|
||||
|
||||
13.3 Assignment. You may not assign this Agreement or any of its rights under this Agreement without the prior written consent of Replay Software and any attempted assignment without such consent shall be void.
|
||||
|
||||
13.4 Export Compliance. You agree to comply with all applicable laws and regulations, including laws, regulations, orders or other restrictions on export, re-export or redistribution of software.
|
||||
|
||||
13.5 Indemnification. You agree to defend, indemnify, and hold harmless Replay Software from and against any lawsuits, claims, losses, damages, fines and expenses (including attorneys' fees and costs) arising out of your use of the Software or breach of this Agreement.
|
||||
|
||||
13.6 Governing Law. This Agreement is governed by the laws of the State of Oregon and the United States without regard to conflicts of laws provisions thereof, and without regard to the United Nations Convention on the International Sale of Goods or the Uniform Computer Information Transactions Act, as currently enacted by any jurisdiction or as may be codified or amended from time to time by any jurisdiction. The jurisdiction and venue for actions related to the subject matter hereof shall be the state of Oregon and United States federal courts located in Portland, Oregon, and both parties hereby submit to the personal jurisdiction of such courts.
|
||||
|
||||
13.7 Attorneys’ Fees and Costs. The prevailing party in any action to enforce this Agreement will be entitled to recover its attorneys’ fees and costs in connection with such action.
|
||||
|
||||
13.8 Severability. If any provision of this Agreement is held by a court of competent jurisdiction to be invalid, illegal, or unenforceable, the remainder of this Agreement will remain in full force and effect.
|
||||
|
||||
13.9 Waiver. Failure or neglect by either party to enforce at any time any of the provisions of this licence Agreement shall not be construed or deemed to be a waiver of that party's rights under this Agreement.
|
||||
|
||||
13.10 Headings. The headings of sections and paragraphs of this Agreement are for convenience of reference only and are not intended to restrict, affect or be of any weight in the interpretation or construction of the provisions of such sections or paragraphs.
|
||||
|
||||
14. Contact Information. If you have any questions about this EULA, or if you want to contact Replay Software for any reason, please direct correspondence to info@gortool.com
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
FROM golang:latest
|
||||
|
||||
RUN apt-get update && apt-get install ruby vim-common -y
|
||||
|
||||
# Install Java for middleware testing
|
||||
RUN echo "deb http://ppa.launchpad.net/webupd8team/java/ubuntu trusty main" | tee /etc/apt/sources.list.d/webupd8team-java.list
|
||||
RUN echo "deb-src http://ppa.launchpad.net/webupd8team/java/ubuntu trusty main" | tee -a /etc/apt/sources.list.d/webupd8team-java.list
|
||||
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys EEA14886
|
||||
RUN apt-get update -y
|
||||
RUN echo oracle-java7-installer shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections
|
||||
RUN apt-get install oracle-java8-installer -y
|
||||
|
||||
RUN apt-get install flex bison -y
|
||||
RUN wget http://www.tcpdump.org/release/libpcap-1.7.4.tar.gz && tar xzf libpcap-1.7.4.tar.gz && cd libpcap-1.7.4 && ./configure && make install
|
||||
RUN go get github.com/google/gopacket
|
||||
RUN go get -u github.com/golang/lint/golint
|
||||
|
||||
WORKDIR /go/src/github.com/buger/gor/
|
||||
ADD . /go/src/github.com/buger/gor/
|
||||
|
||||
RUN javac -cp /tmp/commons-io-2.4/commons-io-2.4.jar ./examples/middleware/echo.java
|
||||
RUN go get
|
||||
@@ -0,0 +1,65 @@
|
||||
gor & elasticsearch
|
||||
===================
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
- elasticsearch
|
||||
- kibana (Get it here: http://www.elasticsearch.org/overview/kibana/)
|
||||
- gor
|
||||
|
||||
|
||||
elasticsearch
|
||||
-------------
|
||||
|
||||
The default elasticsearch configuration is just fine for most workloads. You won't need clustering, sharding or something like that.
|
||||
|
||||
In this example we're installing it on our gor replay server which gives us the elasticsearch listener on _http://localhost:9200_
|
||||
|
||||
|
||||
kibana
|
||||
------
|
||||
|
||||
Kibana (elasticsearch analytics web-ui) is just as simple.
|
||||
Download it, extract it and serve it via a simple webserver.
|
||||
(Could be nginx or apache)
|
||||
|
||||
You could also use a shell, ```cd``` into the kibana directory and start a little quick and dirty python webserver with:
|
||||
|
||||
```
|
||||
python -m SimpleHTTPServer 8000
|
||||
```
|
||||
|
||||
In this example we're also choosing the gor replay server as our kibana host. If you choose a different server you'll have to point kibana to your elasticsearch host.
|
||||
|
||||
|
||||
gor
|
||||
---
|
||||
|
||||
Start your gor replay server with elasticsearch option:
|
||||
|
||||
```
|
||||
./gor --input-raw :8000 --output-http http://staging.com --output-http-elasticsearch localhost:9200/gor
|
||||
```
|
||||
|
||||
|
||||
(You don't have to create the index upfront. That will be done for you automatically)
|
||||
|
||||
|
||||
Now visit your kibana url, load the predefined dashboard from the gist https://gist.github.com/gottwald/b2c875037f24719a9616 and watch the data rush in.
|
||||
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
The replay process may complain about __too many open files__.
|
||||
That's because your typical linux shell has a small open files soft limit at 1024.
|
||||
You can easily raise that when you do this before starting your _gor replay_ process:
|
||||
|
||||
```
|
||||
ulimit -n 64000
|
||||
```
|
||||
|
||||
Please be aware, this is not a permanent setting. It's just valid for the following jobs you start from that shell.
|
||||
|
||||
We reached the 1024 limit in our tests with a ubuntu box replaying about 9000 requests per minute. (We had very slow responses there, should be way more with fast responses)
|
||||
@@ -0,0 +1,7 @@
|
||||
Gor is an Open Source project licensed under the terms of
|
||||
the LGPLv3 license. Please see <http://www.gnu.org/licenses/lgpl-3.0.html>
|
||||
for license text.
|
||||
|
||||
Gor Pro has a commercial-friendly license allowing private forks
|
||||
and modifications of Gor. Please see http://gortool.com/#pro for
|
||||
more detail. You can find the commercial license terms in COMM-LICENSE.
|
||||
@@ -0,0 +1,86 @@
|
||||
SOURCE = emitter.go gor.go gor_stat.go input_dummy.go input_file.go input_raw.go input_tcp.go limiter.go output_dummy.go output_file.go input_http.go output_http.go output_tcp.go plugins.go settings.go test_input.go elasticsearch.go http_modifier.go http_modifier_settings.go http_client.go middleware.go protocol.go output_file_settings.go
|
||||
SOURCE_PATH = /go/src/github.com/buger/gor/
|
||||
PORT = 8000
|
||||
FADDR = :8000
|
||||
RUN = docker run -v `pwd`:$(SOURCE_PATH) -p 0.0.0.0:$(PORT):$(PORT) -t -i gor
|
||||
BENCHMARK = BenchmarkRAWInput
|
||||
TEST = TestRawListenerBench
|
||||
VERSION = DEV-$(shell date +%s)
|
||||
LDFLAGS = -ldflags "-X main.VERSION=$(VERSION) -extldflags \"-static\""
|
||||
MAC_LDFLAGS = -ldflags "-X main.VERSION=$(VERSION)"
|
||||
FADDR = ":8000"
|
||||
|
||||
release: release-x64 release-mac
|
||||
|
||||
release-x64:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i gor go build $(LDFLAGS) && tar -czf gor_$(VERSION)_x64.tar.gz gor && rm gor
|
||||
|
||||
release-x86:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=386 -i gor go build $(LDFLAGS) && tar -czf gor_$(VERSION)_x86.tar.gz gor && rm gor
|
||||
|
||||
release-mac:
|
||||
go build $(MAC_LDFLAGS) && tar -czf gor_$(VERSION)_mac.tar.gz gor && rm gor
|
||||
|
||||
build:
|
||||
docker build -t gor .
|
||||
|
||||
|
||||
profile:
|
||||
go build && ./gor --output-http="http://localhost:9000" --input-dummy 0 --input-raw :9000 --input-http :9000 --memprofile=./mem.out --cpuprofile=./cpu.out --stats --output-http-stats --output-http-timeout 100ms
|
||||
|
||||
lint:
|
||||
$(RUN) golint $(PKG)
|
||||
|
||||
race:
|
||||
$(RUN) go test ./... $(ARGS) -v -race -timeout 15s
|
||||
|
||||
test:
|
||||
$(RUN) go test ./. -timeout 60s $(LDFLAGS) $(ARGS) -v
|
||||
|
||||
test_all:
|
||||
$(RUN) go test ./... -timeout 60s $(LDFLAGS) $(ARGS) -v
|
||||
|
||||
testone:
|
||||
$(RUN) go test ./... -timeout 4s $(LDFLAGS) -run $(TEST) $(ARGS) -v
|
||||
|
||||
cover:
|
||||
$(RUN) go test $(ARGS) -race -v -timeout 15s -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
fmt:
|
||||
$(RUN) gofmt -w -s ./..
|
||||
|
||||
vet:
|
||||
$(RUN) go vet
|
||||
|
||||
bench:
|
||||
$(RUN) go test $(LDFLAGS) -v -run NOT_EXISTING -bench $(BENCHMARK) -benchtime 5s
|
||||
|
||||
profile_test:
|
||||
$(RUN) go test $(LDFLAGS) -run $(TEST) ./raw_socket_listener/. $(ARGS) -memprofile mem.mprof -cpuprofile cpu.out
|
||||
$(RUN) go test $(LDFLAGS) -run $(TEST) ./raw_socket_listener/. $(ARGS) -c
|
||||
|
||||
# Used mainly for debugging, because docker container do not have access to parent machine ports
|
||||
run:
|
||||
$(RUN) go run $(LDFLAGS) $(SOURCE) --input-dummy=0 --output-http="http://localhost:9000" --input-raw-track-response --input-raw 127.0.0.1:9000 --input-http 127.0.0.1:9000 --verbose --debug --middleware "./examples/middleware/echo.sh" --output-file requests.gor
|
||||
|
||||
run-2:
|
||||
sudo -E go run $(SOURCE) --input-dummy="" --output-tcp localhost:27001 --verbose --debug
|
||||
|
||||
run-3:
|
||||
sudo -E go run $(SOURCE) --input-tcp :27001 --output-stdout
|
||||
|
||||
run-arg:
|
||||
sudo -E go run $(SOURCE) $(ARGS)
|
||||
|
||||
file-server:
|
||||
go run $(SOURCE) file-server $(FADDR)
|
||||
|
||||
record:
|
||||
$(RUN) go run $(SOURCE) --input-dummy=0 --output-file=requests.gor --verbose --debug
|
||||
|
||||
replay:
|
||||
$(RUN) go run $(SOURCE) --input-file=requests.bin --output-tcp=:9000 --verbose -h
|
||||
|
||||
bash:
|
||||
$(RUN) /bin/bash
|
||||
@@ -0,0 +1,4 @@
|
||||
web: python -m SimpleHTTPServer 8000
|
||||
replayed_web: python -m SimpleHTTPServer 8001
|
||||
listener: sudo -E go run ./bin/gor.go --input-raw :8000 --output-tcp :8002 --verbose
|
||||
replay: go run ./bin/gor.go --input-tcp :8002 --output-http localhost:8001 --verbose
|
||||
@@ -1,88 +1,73 @@
|
||||
## About
|
||||
|
||||
Gor is a simple http traffic replication tool written in Go.
|
||||
Its main goal is to replay traffic from production servers to staging and dev environments.
|
||||
|
||||
|
||||
Now you can test your code on real user sessions in an automated and repeatable fashion.
|
||||
**No more falling down in production!**
|
||||
|
||||
Gor consists of 2 parts: listener and replay servers.
|
||||
|
||||
The listener server catches http traffic from a given port in real-time
|
||||
and sends it to the replay server via UDP.
|
||||
The replay server forwards traffic to a given address.
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
## Basic example
|
||||
|
||||
```bash
|
||||
# Run on servers where you want to catch traffic. You can run it on each `web` machine.
|
||||
sudo gor listen -p 80 -r replay.server.local:28020
|
||||
|
||||
# Replay server (replay.server.local).
|
||||
gor replay -f http://staging.server
|
||||
```
|
||||
|
||||
## Advanced use
|
||||
|
||||
### Rate limiting
|
||||
The replay server supports rate limiting. It can be useful if you want
|
||||
forward only part of production traffic and not overload your staging
|
||||
environment. You can specify your desired requests per second using the
|
||||
"|" operator after the server address:
|
||||
|
||||
```
|
||||
# staging.server will not get more than 10 requests per second
|
||||
gor replay -f "http://staging.server|10"
|
||||
```
|
||||
|
||||
### Forward to multiple addresses
|
||||
|
||||
You can forward traffic to multiple endpoints. Just separate the addresses by coma.
|
||||
```
|
||||
gor replay -f "http://staging.server|10,http://dev.server|5"
|
||||
```
|
||||
|
||||
## Additional help
|
||||
```
|
||||
$ gor listen -h
|
||||
Usage of ./bin/gor-linux:
|
||||
-i="any": By default it try to listen on all network interfaces.To get list of interfaces run `ifconfig`
|
||||
-p=80: Specify the http server port whose traffic you want to capture
|
||||
-r="localhost:28020": Address of replay server.
|
||||
```
|
||||
|
||||
```
|
||||
$ gor replay -h
|
||||
Usage of ./bin/gor-linux:
|
||||
-f="http://localhost:8080": http address to forward traffic.
|
||||
You can limit requests per second by adding `|#{num}` after address.
|
||||
If you have multiple addresses with different limits. For example: http://staging.example.com|100,http://dev.example.com|10
|
||||
-ip="0.0.0.0": ip addresses to listen on
|
||||
-p=28020: specify port number
|
||||
```
|
||||
|
||||
## Pre-build binaries
|
||||
|
||||
[Download binaries (linux 32/64, darwin)](https://drive.google.com/folderview?id=0B46uay48NwcfWFowc1E4a1BISVU&usp=sharing)
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why does the `gor listener` requires sudo or root access?
|
||||
Listener works by sniffing traffic from a given port. It's accessible
|
||||
only by using sudo or root access.
|
||||
|
||||
### Do you support all http request types?
|
||||
Yes. ~~Right now it supports only "GET" requests.~~
|
||||
|
||||
## TODO
|
||||
|
||||
Use buffering for request throttling instead of simple rate limiting.
|
||||
|
||||
Better stats
|
||||
|
||||
Optimize for load testing cases
|
||||
[](https://github.com/buger/gor/releases) [](https://codebeat.co/projects/github-com-buger-gor) [](https://goreportcard.com/report/github.com/buger/gor) [](https://gitter.im/buger/gor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||

|
||||
|
||||
## About
|
||||
|
||||
Gor is an open-source tool for capturing and replaying live HTTP traffic into a test environment in order to continuously test your system with real data. It can be used to increase confidence in code deployments, configuration changes and infrastructure changes.
|
||||
|
||||
Now you can test your code on real user sessions in an automated and repeatable fashion.
|
||||
**No more falling down in production!**
|
||||
|
||||
Here is basic workflow: The listener server catches http traffic and sends it to the replay server or saves to file. The replay server forwards traffic to a given address.
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
Download latest binary from https://github.com/buger/gor/releases or [compile by yourself](https://github.com/buger/gor/wiki/Compilation).
|
||||
|
||||
## Getting started
|
||||
|
||||
The most basic setup will be `sudo ./gor --input-raw :8000 --output-stdout` which acts like tcpdump.
|
||||
If you already have test environment you can start replaying: `sudo ./gor --input-raw :8000 --output-http http://staging.env`.
|
||||
|
||||
See the our wiki and especially [Getting started](https://github.com/buger/gor/wiki/Getting-Started) wiki page for more info.
|
||||
|
||||
|
||||
## Want to Upgrade?
|
||||
|
||||
I also sell Gor Pro, extensions to Gor which provide more features, a commercial-friendly license and allow you to support high quality open source development all at the same time. Please see the Gor [homepage](https://gortool.com/) for more detail.
|
||||
|
||||
Subscribe to the [quarterly newsletter](https://tinyletter.com/gor) to stay informed about the latest features and changes to Gor and its bigger siblings.
|
||||
|
||||
## Problems?
|
||||
If you have a problem, please review the [FAQ](https://github.com/buger/gor/wiki/FAQ) and [Troubleshooting](https://github.com/buger/gor/wiki/Troubleshooting) wiki pages. Searching the [issues](https://github.com/buger/gor/issues) for your problem is also a good idea.
|
||||
|
||||
All bug-reports and suggestions should go though Github Issues or our [Google Group](https://groups.google.com/forum/#!forum/gor-users) (you can just send email to gor-users@googlegroups.com).
|
||||
If you have a private question feel free to send email to support@gortool.com.
|
||||
|
||||
Useful resources:
|
||||
|
||||
* Product documentation is in the [wiki](http://github.com/buger/gor/wiki).
|
||||
* Release announcements are made to the [@buger](http://twitter.com/buger) Twitter account and our [newsleter](https://tinyletter.com/gor)
|
||||
|
||||
|
||||
If you need commercial support read more about Pro and Enterprise versions at our site [https://gortool.com/](https://gortool.com/)
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork it
|
||||
2. Create your feature branch (git checkout -b my-new-feature)
|
||||
3. Commit your changes (git commit -am 'Added some feature')
|
||||
4. Push to the branch (git push origin my-new-feature)
|
||||
5. Create new Pull Request
|
||||
|
||||
## Companies using Gor
|
||||
|
||||
* [GOV.UK](https://www.gov.uk) - UK Government Digital Service
|
||||
* [theguardian.com](http://theguardian.com) - Most popular online newspaper in the UK
|
||||
* [TomTom](http://www.tomtom.com/) - Global leader in navigation, traffic and map products, GPS Sport Watches and fleet management solutions.
|
||||
* [3SCALE](http://www.3scale.net/) - API infrastructure to manage your APIs for internal or external users
|
||||
* [Optionlab](http://www.opinionlab.com) - Optimize customer experience and drive engagement across multiple channels
|
||||
* [TubeMogul](http://tubemogul.com) - Software for Brand Advertising
|
||||
* [Videology](http://www.videologygroup.com/) - Video advertising platform
|
||||
* [ForeksMobile](http://foreksmobile.com/) - One of the leading financial application development company in Turkey
|
||||
* [Granify](http://granify.com) - AI backed SaaS solution that enables online retailers to maximise their sales
|
||||
* And many more!
|
||||
|
||||
If you are using Gor we are happy add you to the list and share your story, just write to: hello@gortool.com
|
||||
|
||||
## Author
|
||||
|
||||
Leonid Bugaev, [@buger](https://twitter.com/buger), https://leonsbox.com
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Package byteutils probvides helpers for working with byte slices
|
||||
package byteutils
|
||||
|
||||
// Cut elements from slice for a given range
|
||||
func Cut(a []byte, from, to int) []byte {
|
||||
copy(a[from:], a[to:])
|
||||
a = a[:len(a)-to+from]
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// Insert new slice at specified position
|
||||
func Insert(a []byte, i int, b []byte) []byte {
|
||||
a = append(a, make([]byte, len(b))...)
|
||||
copy(a[i+len(b):], a[i:])
|
||||
copy(a[i:i+len(b)], b)
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// Replace function unlike bytes.Replace allows you to specify range
|
||||
func Replace(a []byte, from, to int, new []byte) []byte {
|
||||
lenDiff := len(new) - (to - from)
|
||||
|
||||
if lenDiff > 0 {
|
||||
// Extend if new segment bigger
|
||||
a = append(a, make([]byte, lenDiff)...)
|
||||
copy(a[to+lenDiff:], a[to:])
|
||||
copy(a[from:from+len(new)], new)
|
||||
|
||||
return a
|
||||
} else if lenDiff < 0 {
|
||||
copy(a[from:], new)
|
||||
copy(a[from+len(new):], a[to:])
|
||||
return a[:len(a)+lenDiff]
|
||||
} else { // same size
|
||||
copy(a[from:], new)
|
||||
return a
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package byteutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCut(t *testing.T) {
|
||||
if !bytes.Equal(Cut([]byte("123456"), 2, 4), []byte("1256")) {
|
||||
t.Error("Should properly cut")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsert(t *testing.T) {
|
||||
if !bytes.Equal(Insert([]byte("123456"), 2, []byte("abcd")), []byte("12abcd3456")) {
|
||||
t.Error("Should insert into middle of slice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplace(t *testing.T) {
|
||||
if !bytes.Equal(Replace([]byte("123456"), 2, 4, []byte("ab")), []byte("12ab56")) {
|
||||
t.Error("Should replace when same length")
|
||||
}
|
||||
|
||||
if !bytes.Equal(Replace([]byte("123456"), 2, 4, []byte("abcd")), []byte("12abcd56")) {
|
||||
t.Error("Should replace when replacement length bigger")
|
||||
}
|
||||
|
||||
if !bytes.Equal(Replace([]byte("123456"), 2, 5, []byte("ab")), []byte("12ab6")) {
|
||||
t.Error("Should replace when replacement length bigger")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
dependencies:
|
||||
pre:
|
||||
- sudo apt-get install libpcap-dev -y
|
||||
|
||||
test:
|
||||
override:
|
||||
- sudo bash -l -c "export GOPATH='/home/ubuntu/.go_workspace:/usr/local/go_workspace:/home/ubuntu/.go_project' && GORACE='halt_on_error=1' /usr/local/go/bin/go test ./... -v -timeout 120s -race"
|
||||
@@ -0,0 +1,153 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/buger/elastigo/api"
|
||||
"github.com/buger/elastigo/core"
|
||||
"github.com/buger/gor/proto"
|
||||
"log"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ESUriErorr struct{}
|
||||
|
||||
func (e *ESUriErorr) Error() string {
|
||||
return "Wrong ElasticSearch URL format. Expected to be: host:port/index_name"
|
||||
}
|
||||
|
||||
type ESPlugin struct {
|
||||
Active bool
|
||||
ApiPort string
|
||||
Host string
|
||||
Index string
|
||||
indexor *core.BulkIndexer
|
||||
done chan bool
|
||||
}
|
||||
|
||||
type ESRequestResponse struct {
|
||||
ReqUrl []byte `json:"Req_URL"`
|
||||
ReqMethod []byte `json:"Req_Method"`
|
||||
ReqUserAgent []byte `json:"Req_User-Agent"`
|
||||
ReqAcceptLanguage []byte `json:"Req_Accept-Language,omitempty"`
|
||||
ReqAccept []byte `json:"Req_Accept,omitempty"`
|
||||
ReqAcceptEncoding []byte `json:"Req_Accept-Encoding,omitempty"`
|
||||
ReqIfModifiedSince []byte `json:"Req_If-Modified-Since,omitempty"`
|
||||
ReqConnection []byte `json:"Req_Connection,omitempty"`
|
||||
ReqCookies []byte `json:"Req_Cookies,omitempty"`
|
||||
RespStatus []byte `json:"Resp_Status"`
|
||||
RespStatusCode []byte `json:"Resp_Status-Code"`
|
||||
RespProto []byte `json:"Resp_Proto,omitempty"`
|
||||
RespContentLength []byte `json:"Resp_Content-Length,omitempty"`
|
||||
RespContentType []byte `json:"Resp_Content-Type,omitempty"`
|
||||
RespTransferEncoding []byte `json:"Resp_Transfer-Encoding,omitempty"`
|
||||
RespContentEncoding []byte `json:"Resp_Content-Encoding,omitempty"`
|
||||
RespExpires []byte `json:"Resp_Expires,omitempty"`
|
||||
RespCacheControl []byte `json:"Resp_Cache-Control,omitempty"`
|
||||
RespVary []byte `json:"Resp_Vary,omitempty"`
|
||||
RespSetCookie []byte `json:"Resp_Set-Cookie,omitempty"`
|
||||
Rtt int64 `json:"RTT"`
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// Parse ElasticSearch URI
|
||||
//
|
||||
// Proper format is: host:port/index_name
|
||||
func parseURI(URI string) (err error, host string, port string, index string) {
|
||||
rURI := regexp.MustCompile("(.+):([0-9]+)/(.+)")
|
||||
match := rURI.FindAllStringSubmatch(URI, -1)
|
||||
|
||||
if len(match) == 0 {
|
||||
err = new(ESUriErorr)
|
||||
} else {
|
||||
host = match[0][1]
|
||||
port = match[0][2]
|
||||
index = match[0][3]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ESPlugin) Init(URI string) {
|
||||
var err error
|
||||
|
||||
err, p.Host, p.ApiPort, p.Index = parseURI(URI)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Can't initialize ElasticSearch plugin.", err)
|
||||
}
|
||||
|
||||
api.Domain = p.Host
|
||||
api.Port = p.ApiPort
|
||||
|
||||
p.indexor = core.NewBulkIndexerErrors(50, 60)
|
||||
p.done = make(chan bool)
|
||||
p.indexor.Run(p.done)
|
||||
|
||||
// Only start the ErrorHandler goroutine when in verbose mode
|
||||
// no need to burn ressources otherwise
|
||||
// go p.ErrorHandler()
|
||||
|
||||
log.Println("Initialized Elasticsearch Plugin")
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ESPlugin) IndexerShutdown() {
|
||||
p.done <- true
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ESPlugin) ErrorHandler() {
|
||||
for {
|
||||
errBuf := <-p.indexor.ErrorChannel
|
||||
log.Println(errBuf.Err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ESPlugin) RttDurationToMs(d time.Duration) int64 {
|
||||
sec := d / time.Second
|
||||
nsec := d % time.Second
|
||||
fl := float64(sec) + float64(nsec)*1e-6
|
||||
return int64(fl)
|
||||
}
|
||||
|
||||
func (p *ESPlugin) ResponseAnalyze(req, resp []byte, start, stop time.Time) {
|
||||
if len(resp) == 0 {
|
||||
// nil http response - skipped elasticsearch export for this request
|
||||
return
|
||||
}
|
||||
t := time.Now()
|
||||
rtt := p.RttDurationToMs(stop.Sub(start))
|
||||
|
||||
esResp := ESRequestResponse{
|
||||
ReqUrl: proto.Path(req),
|
||||
ReqMethod: proto.Method(req),
|
||||
ReqUserAgent: proto.Header(req, []byte("User-Agent")),
|
||||
ReqAcceptLanguage: proto.Header(req, []byte("Accept-Language")),
|
||||
ReqAccept: proto.Header(req, []byte("Accept")),
|
||||
ReqAcceptEncoding: proto.Header(req, []byte("Accept-Encoding")),
|
||||
ReqIfModifiedSince: proto.Header(req, []byte("If-Modified-Since")),
|
||||
ReqConnection: proto.Header(req, []byte("Connection")),
|
||||
ReqCookies: proto.Header(req, []byte("Cookie")),
|
||||
RespStatus: proto.Status(resp),
|
||||
RespStatusCode: proto.Status(resp),
|
||||
RespProto: proto.Method(resp),
|
||||
RespContentLength: proto.Header(resp, []byte("Content-Length")),
|
||||
RespContentType: proto.Header(resp, []byte("Content-Type")),
|
||||
RespTransferEncoding: proto.Header(resp, []byte("Transfer-Encoding")),
|
||||
RespContentEncoding: proto.Header(resp, []byte("Content-Encoding")),
|
||||
RespExpires: proto.Header(resp, []byte("Expires")),
|
||||
RespCacheControl: proto.Header(resp, []byte("Cache-Control")),
|
||||
RespVary: proto.Header(resp, []byte("Vary")),
|
||||
RespSetCookie: proto.Header(resp, []byte("Set-Cookie")),
|
||||
Rtt: rtt,
|
||||
Timestamp: t,
|
||||
}
|
||||
j, err := json.Marshal(&esResp)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
p.indexor.Index(p.Index, "RequestResponse", "", "", &t, j, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Start initialize loop for sending data from inputs to outputs
|
||||
func Start(stop chan int) {
|
||||
if Settings.middleware != "" {
|
||||
middleware := NewMiddleware(Settings.middleware)
|
||||
|
||||
for _, in := range Plugins.Inputs {
|
||||
middleware.ReadFrom(in)
|
||||
}
|
||||
|
||||
// We going only to read responses, so using same ReadFrom method
|
||||
for _, out := range Plugins.Outputs {
|
||||
if r, ok := out.(io.Reader); ok {
|
||||
middleware.ReadFrom(r)
|
||||
}
|
||||
}
|
||||
|
||||
go CopyMulty(middleware, Plugins.Outputs...)
|
||||
} else {
|
||||
for _, in := range Plugins.Inputs {
|
||||
go CopyMulty(in, Plugins.Outputs...)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
pluginMu.Lock()
|
||||
for _, p := range Plugins.All {
|
||||
if cp, ok := p.(io.Closer); ok {
|
||||
cp.Close()
|
||||
}
|
||||
}
|
||||
pluginMu.Unlock()
|
||||
return
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CopyMulty copies from 1 reader to multiple writers
|
||||
func CopyMulty(src io.Reader, writers ...io.Writer) (err error) {
|
||||
buf := make([]byte, 5*1024*1024)
|
||||
wIndex := 0
|
||||
modifier := NewHTTPModifier(&Settings.modifierConfig)
|
||||
|
||||
for {
|
||||
nr, er := src.Read(buf)
|
||||
|
||||
if nr > 0 && len(buf) > nr {
|
||||
payload := buf[:nr]
|
||||
|
||||
_maxN := nr
|
||||
if nr > 500 {
|
||||
_maxN = 500
|
||||
}
|
||||
|
||||
if Settings.debug {
|
||||
Debug("[EMITTER] input:", string(payload[0:_maxN]), nr, "from:", src)
|
||||
}
|
||||
|
||||
if modifier != nil && isRequestPayload(payload) {
|
||||
headSize := bytes.IndexByte(payload, '\n') + 1
|
||||
body := payload[headSize:]
|
||||
originalBodyLen := len(body)
|
||||
body = modifier.Rewrite(body)
|
||||
|
||||
// If modifier tells to skip request
|
||||
if len(body) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if originalBodyLen != len(body) {
|
||||
payload = append(payload[:headSize], body...)
|
||||
}
|
||||
|
||||
if Settings.debug {
|
||||
Debug("[EMITTER] Rewrittern input:", len(payload), "First 500 bytes:", string(payload[0:_maxN]))
|
||||
}
|
||||
}
|
||||
|
||||
if Settings.splitOutput {
|
||||
// Simple round robin
|
||||
writers[wIndex].Write(payload)
|
||||
|
||||
wIndex++
|
||||
|
||||
if wIndex >= len(writers) {
|
||||
wIndex = 0
|
||||
}
|
||||
} else {
|
||||
for _, dst := range writers {
|
||||
dst.Write(payload)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if er == io.EOF {
|
||||
break
|
||||
}
|
||||
if er != nil {
|
||||
err = er
|
||||
break
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEmitter(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestEmitterRoundRobin(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
|
||||
var counter1, counter2 int32
|
||||
|
||||
output1 := NewTestOutput(func(data []byte) {
|
||||
atomic.AddInt32(&counter1, 1)
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
output2 := NewTestOutput(func(data []byte) {
|
||||
atomic.AddInt32(&counter2, 1)
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output1, output2}
|
||||
|
||||
Settings.splitOutput = true
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
|
||||
if counter1 == 0 || counter2 == 0 {
|
||||
t.Errorf("Round robin should split traffic equally: %d vs %d", counter1, counter2)
|
||||
}
|
||||
|
||||
Settings.splitOutput = false
|
||||
}
|
||||
|
||||
func BenchmarkEmitter(b *testing.B) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class echo {
|
||||
public static void main(String[] args) {
|
||||
if(args != null){
|
||||
for(String arg : args){
|
||||
System.out.println(arg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BufferedReader stdin = new BufferedReader(new InputStreamReader(
|
||||
System.in));
|
||||
String line = null;
|
||||
|
||||
try {
|
||||
while ((line = stdin.readLine()) != null) {
|
||||
|
||||
System.out.println(line);
|
||||
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env ruby
|
||||
# encoding: utf-8
|
||||
while data = STDIN.gets # continiously read line from STDIN
|
||||
next unless data
|
||||
data = data.chomp # remove end of line symbol
|
||||
|
||||
decoded = [data].pack("H*") # decode base64 encoded request
|
||||
|
||||
# dedoded value is raw HTTP payload, example:
|
||||
#
|
||||
# POST /post HTTP/1.1
|
||||
# Content-Length: 7
|
||||
# Host: www.w3.org
|
||||
#
|
||||
# a=1&b=2"
|
||||
|
||||
encoded = decoded.unpack("H*").first # encoding back to base64
|
||||
|
||||
# Emit request back
|
||||
# You can skip this if want to filter out request
|
||||
STDOUT.puts encoded
|
||||
end
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# `xxd` utility included into vim-common package
|
||||
# It allow hex decoding/encoding
|
||||
#
|
||||
# This example may broke if you request contains `null` string, you may consider using pipes instead.
|
||||
# See: https://github.com/buger/gor/issues/309
|
||||
#
|
||||
|
||||
function log {
|
||||
# Logging to stderr, because stdout/stdin used for data transfer
|
||||
>&2 echo "[DEBUG][ECHO] $1"
|
||||
}
|
||||
|
||||
while read line; do
|
||||
decoded=$(echo -e "$line" | xxd -r -p)
|
||||
|
||||
header=$(echo -e "$decoded" | head -n +1)
|
||||
payload=$(echo -e "$decoded" | tail -n +2)
|
||||
|
||||
encoded=$(echo -e "$header\n$payload" | xxd -p | tr -d "\\n")
|
||||
|
||||
log ""
|
||||
log "==================================="
|
||||
|
||||
case ${header:0:1} in
|
||||
"1")
|
||||
log "Request type: Request"
|
||||
;;
|
||||
"2")
|
||||
log "Request type: Original Response"
|
||||
;;
|
||||
"3")
|
||||
log "Request type: Replayed Response"
|
||||
;;
|
||||
*)
|
||||
log "Unknown request type $header"
|
||||
esac
|
||||
echo "$encoded"
|
||||
|
||||
log "==================================="
|
||||
|
||||
log "Original data: $line"
|
||||
log "Decoded request: $decoded"
|
||||
log "Encoded data: $encoded"
|
||||
done;
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
This middleware made for auth system that randomly generate access tokens, which used later for accessing secure content. Since there is no pre-defined token value, naive approach without middleware (or if middleware use only request payloads) will fail, because replayed server have own tokens, not synced with origin. To fix this, our middleware should take in account responses of replayed and origin server, store `originalToken -> replayedToken` aliases and rewrite all requests using this token to use replayed alias. See `middleware_test.go#TestTokenMiddleware` test for examples of using this middleware.
|
||||
|
||||
How middleware works:
|
||||
|
||||
Original request +--------------+
|
||||
+-------------+----------STDIN---------->+ |
|
||||
| Gor input | | Middleware |
|
||||
+-------------+----------STDIN---------->+ |
|
||||
Original response +------+---+---+
|
||||
| ^
|
||||
+-------------+ Modified request v |
|
||||
| Gor output +<---------STDOUT-----------------+ |
|
||||
+-----+-------+ |
|
||||
| |
|
||||
| Replayed response |
|
||||
+------------------STDIN----------------->----+
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/buger/gor/proto"
|
||||
"os"
|
||||
)
|
||||
|
||||
// requestID -> originalToken
|
||||
var originalTokens map[string][]byte
|
||||
|
||||
// originalToken -> replayedToken
|
||||
var tokenAliases map[string][]byte
|
||||
|
||||
func main() {
|
||||
originalTokens = make(map[string][]byte)
|
||||
tokenAliases = make(map[string][]byte)
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
||||
for scanner.Scan() {
|
||||
encoded := scanner.Bytes()
|
||||
buf := make([]byte, len(encoded)/2)
|
||||
hex.Decode(buf, encoded)
|
||||
|
||||
process(buf)
|
||||
}
|
||||
}
|
||||
|
||||
func process(buf []byte) {
|
||||
// First byte indicate payload type, possible values:
|
||||
// 1 - Request
|
||||
// 2 - Response
|
||||
// 3 - ReplayedResponse
|
||||
payloadType := buf[0]
|
||||
headerSize := bytes.IndexByte(buf, '\n') + 1
|
||||
header := buf[:headerSize-1]
|
||||
|
||||
// Header contains space separated values of: request type, request id, and request start time (or round-trip time for responses)
|
||||
meta := bytes.Split(header, []byte(" "))
|
||||
// For each request you should receive 3 payloads (request, response, replayed response) with same request id
|
||||
reqID := string(meta[1])
|
||||
payload := buf[headerSize:]
|
||||
|
||||
Debug("Received payload:", string(buf))
|
||||
|
||||
switch payloadType {
|
||||
case '1': // Request
|
||||
if bytes.Equal(proto.Path(payload), []byte("/token")) {
|
||||
originalTokens[reqID] = []byte{}
|
||||
Debug("Found token request:", reqID)
|
||||
} else {
|
||||
token, vs, _ := proto.PathParam(payload, []byte("token"))
|
||||
|
||||
if vs != -1 { // If there is GET token param
|
||||
if alias, ok := tokenAliases[string(token)]; ok {
|
||||
// Rewrite original token to alias
|
||||
payload = proto.SetPathParam(payload, []byte("token"), alias)
|
||||
|
||||
// Copy modified payload to our buffer
|
||||
buf = append(buf[:headerSize], payload...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emitting data back
|
||||
os.Stdout.Write(encode(buf))
|
||||
case '2': // Original response
|
||||
if _, ok := originalTokens[reqID]; ok {
|
||||
// Token is inside response body
|
||||
secureToken := proto.Body(payload)
|
||||
originalTokens[reqID] = secureToken
|
||||
Debug("Remember origial token:", string(secureToken))
|
||||
}
|
||||
case '3': // Replayed response
|
||||
if originalToken, ok := originalTokens[reqID]; ok {
|
||||
delete(originalTokens, reqID)
|
||||
secureToken := proto.Body(payload)
|
||||
tokenAliases[string(originalToken)] = secureToken
|
||||
|
||||
Debug("Create alias for new token token, was:", string(originalToken), "now:", string(secureToken))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func encode(buf []byte) []byte {
|
||||
dst := make([]byte, len(buf)*2+1)
|
||||
hex.Encode(dst, buf)
|
||||
dst[len(dst)-1] = '\n'
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
func Debug(args ...interface{}) {
|
||||
fmt.Fprint(os.Stderr, "[DEBUG][TOKEN-MOD] ")
|
||||
fmt.Fprintln(os.Stderr, args...)
|
||||
}
|
||||
@@ -1,46 +1,121 @@
|
||||
// Gor is simple http traffic replication tool written in Go. Its main goal to replay traffic from production servers to staging and dev environments.
|
||||
// Now you can test your code on real user sessions in an automated and repeatable fashion.
|
||||
//
|
||||
// Gor consists of 2 parts: listener and replay servers.
|
||||
// Listener catch http traffic from given port in real-time and send it to replay server via UDP. Replay server forwards traffic to given address.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/buger/gor/listener"
|
||||
"github.com/buger/gor/replay"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
_ "runtime/debug"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
VERSION = "0.3"
|
||||
var (
|
||||
mode string
|
||||
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
|
||||
memprofile = flag.String("memprofile", "", "write memory profile to this file")
|
||||
)
|
||||
|
||||
func loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rb, _ := httputil.DumpRequest(r, false)
|
||||
log.Println(string(rb))
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
// // Don't exit on panic
|
||||
// defer func() {
|
||||
// if r := recover(); r != nil {
|
||||
// fmt.Printf("PANIC: pkg: %v %s \n", r, debug.Stack())
|
||||
// }
|
||||
// }()
|
||||
|
||||
// If not set via env cariable
|
||||
if len(os.Getenv("GOMAXPROCS")) == 0 {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU() * 2)
|
||||
}
|
||||
|
||||
args := os.Args[1:]
|
||||
if len(args) > 0 && args[0] == "file-server" {
|
||||
if len(args) != 2 {
|
||||
log.Fatal("You should specify port and IP (optional) for the file server. Example: `gor file-server :80`")
|
||||
}
|
||||
dir, _ := os.Getwd()
|
||||
|
||||
log.Println("Started example file server for current dirrectory on address ", args[1])
|
||||
|
||||
log.Fatal(http.ListenAndServe(args[1], loggingMiddleware(http.FileServer(http.Dir(dir)))))
|
||||
} else {
|
||||
flag.Parse()
|
||||
InitPlugins()
|
||||
}
|
||||
|
||||
fmt.Println("Version:", VERSION)
|
||||
|
||||
mode := "unknown"
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
mode = os.Args[1]
|
||||
if len(Plugins.Inputs) == 0 || len(Plugins.Outputs) == 0 {
|
||||
log.Fatal("Required at least 1 input and 1 output")
|
||||
}
|
||||
|
||||
if mode != "listen" && mode != "replay" {
|
||||
fmt.Println("Usage: \n\tgor listen -h\n\tgor replay -h")
|
||||
return
|
||||
if *memprofile != "" {
|
||||
profileMEM(*memprofile)
|
||||
}
|
||||
|
||||
// Remove mode attr
|
||||
os.Args = append(os.Args[:1], os.Args[2:]...)
|
||||
|
||||
flag.Parse()
|
||||
|
||||
switch mode {
|
||||
case "listen":
|
||||
listener.Run()
|
||||
case "replay":
|
||||
replay.Run()
|
||||
if *cpuprofile != "" {
|
||||
profileCPU(*cpuprofile)
|
||||
}
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
|
||||
for _, p := range Plugins.All {
|
||||
if cp, ok := p.(io.Closer); ok {
|
||||
cp.Close()
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
Start(nil)
|
||||
}
|
||||
|
||||
func profileCPU(cpuprofile string) {
|
||||
if cpuprofile != "" {
|
||||
f, err := os.Create(cpuprofile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
pprof.StartCPUProfile(f)
|
||||
|
||||
time.AfterFunc(30*time.Second, func() {
|
||||
pprof.StopCPUProfile()
|
||||
f.Close()
|
||||
log.Println("Stop profiling after 30 seconds")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func profileMEM(memprofile string) {
|
||||
if memprofile != "" {
|
||||
f, err := os.Create(memprofile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
time.AfterFunc(30*time.Second, func() {
|
||||
pprof.WriteHeapProfile(f)
|
||||
f.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
rate = 5
|
||||
)
|
||||
|
||||
type GorStat struct {
|
||||
statName string
|
||||
latest int
|
||||
mean int
|
||||
max int
|
||||
count int
|
||||
}
|
||||
|
||||
func NewGorStat(statName string) (s *GorStat) {
|
||||
s = new(GorStat)
|
||||
s.statName = statName
|
||||
s.latest = 0
|
||||
s.mean = 0
|
||||
s.max = 0
|
||||
s.count = 0
|
||||
|
||||
if Settings.stats {
|
||||
log.Println(s.statName + ":latest,mean,max,count,count/second,gcount")
|
||||
go s.reportStats()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *GorStat) Write(latest int) {
|
||||
if Settings.stats {
|
||||
if latest > s.max {
|
||||
s.max = latest
|
||||
}
|
||||
if latest != 0 {
|
||||
s.mean = (s.mean + latest) / 2
|
||||
}
|
||||
s.latest = latest
|
||||
s.count = s.count + 1
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GorStat) Reset() {
|
||||
s.latest = 0
|
||||
s.max = 0
|
||||
s.mean = 0
|
||||
s.count = 0
|
||||
}
|
||||
|
||||
func (s *GorStat) String() string {
|
||||
return s.statName + ":" + strconv.Itoa(s.latest) + "," + strconv.Itoa(s.mean) + "," + strconv.Itoa(s.max) + "," + strconv.Itoa(s.count) + "," + strconv.Itoa(s.count/rate) + "," + strconv.Itoa(runtime.NumGoroutine())
|
||||
}
|
||||
|
||||
func (s *GorStat) reportStats() {
|
||||
for {
|
||||
log.Println(s)
|
||||
s.Reset()
|
||||
time.Sleep(rate * time.Second)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
require "language/go"
|
||||
|
||||
class Gor < Formula
|
||||
desc "Real-time HTTP traffic replay tool written in Go"
|
||||
homepage "https://gortool.com"
|
||||
url "https://github.com/buger/gor/archive/v0.14.0.tar.gz"
|
||||
sha256 "62260a6f5cabde571b91d5762fba9c47691643df0a58565cbe808854cd064dc8"
|
||||
head "https://github.com/buger/gor.git"
|
||||
|
||||
bottle do
|
||||
cellar :any_skip_relocation
|
||||
sha256 "c382403de70a41b7445920a02051f5e82030704aaaae70cfcd4e8f401cc87f6a" => :el_capitan
|
||||
sha256 "4b76b3785584897800e87967f1af9510208faefe46f57d7bd6f8b40a7133c19b" => :yosemite
|
||||
sha256 "d186cb1566d33ab8f78215e69934f49dd96becb1c236905b4502d94399ae1974" => :mavericks
|
||||
end
|
||||
|
||||
depends_on "go" => :build
|
||||
|
||||
go_resource "github.com/bitly/go-hostpool" do
|
||||
url "https://github.com/bitly/go-hostpool.git",
|
||||
:revision => "d0e59c22a56e8dadfed24f74f452cea5a52722d2"
|
||||
end
|
||||
|
||||
go_resource "github.com/buger/elastigo" do
|
||||
url "https://github.com/buger/elastigo.git",
|
||||
:revision => "23fcfd9db0d8be2189a98fdab77a4c90fcc3a1e9"
|
||||
end
|
||||
|
||||
go_resource "github.com/google/gopacket" do
|
||||
url "https://github.com/google/gopacket.git",
|
||||
:revision => "aa09ced736460d76535444c825932a0742975f7d"
|
||||
end
|
||||
|
||||
def install
|
||||
ENV["GOPATH"] = buildpath
|
||||
mkdir_p buildpath/"src/github.com/buger/"
|
||||
ln_sf buildpath, buildpath/"src/github.com/buger/gor"
|
||||
Language::Go.stage_deps resources, buildpath/"src"
|
||||
|
||||
system "go", "build", "-o", "#{bin}/gor", "-ldflags", "-X main.VERSION \"#{version}\""
|
||||
end
|
||||
|
||||
test do
|
||||
assert_match version.to_s, shell_output("#{bin}/gor", 1)
|
||||
end
|
||||
end
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/buger/gor/proto"
|
||||
)
|
||||
|
||||
var httpMu sync.Mutex
|
||||
|
||||
const (
|
||||
readChunkSize = 64 * 1024
|
||||
maxResponseSize = 1073741824
|
||||
)
|
||||
|
||||
var chunkedSuffix = []byte("0\r\n\r\n")
|
||||
|
||||
var defaultPorts = map[string]string{
|
||||
"http": "80",
|
||||
"https": "443",
|
||||
}
|
||||
|
||||
type HTTPClientConfig struct {
|
||||
FollowRedirects int
|
||||
Debug bool
|
||||
OriginalHost bool
|
||||
ConnectionTimeout time.Duration
|
||||
Timeout time.Duration
|
||||
ResponseBufferSize int
|
||||
}
|
||||
|
||||
type HTTPClient struct {
|
||||
baseURL string
|
||||
scheme string
|
||||
host string
|
||||
conn net.Conn
|
||||
respBuf []byte
|
||||
config *HTTPClientConfig
|
||||
redirectsCount int
|
||||
}
|
||||
|
||||
func NewHTTPClient(baseURL string, config *HTTPClientConfig) *HTTPClient {
|
||||
if !strings.HasPrefix(baseURL, "http") {
|
||||
baseURL = "http://" + baseURL
|
||||
}
|
||||
|
||||
u, _ := url.Parse(baseURL)
|
||||
if !strings.Contains(u.Host, ":") {
|
||||
if u.Scheme != "http" {
|
||||
u.Host += ":" + defaultPorts[u.Scheme]
|
||||
}
|
||||
}
|
||||
|
||||
if config.Timeout.Nanoseconds() == 0 {
|
||||
config.Timeout = 5 * time.Second
|
||||
}
|
||||
|
||||
config.ConnectionTimeout = config.Timeout
|
||||
|
||||
if config.ResponseBufferSize == 0 {
|
||||
config.ResponseBufferSize = 100 * 1024 // 100kb
|
||||
}
|
||||
|
||||
client := new(HTTPClient)
|
||||
client.baseURL = u.String()
|
||||
client.host = u.Host
|
||||
client.scheme = u.Scheme
|
||||
client.respBuf = make([]byte, config.ResponseBufferSize)
|
||||
client.config = config
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Connect() (err error) {
|
||||
c.Disconnect()
|
||||
|
||||
if !strings.Contains(c.host, ":") {
|
||||
c.conn, err = net.DialTimeout("tcp", c.host+":80", c.config.ConnectionTimeout)
|
||||
} else {
|
||||
c.conn, err = net.DialTimeout("tcp", c.host, c.config.ConnectionTimeout)
|
||||
}
|
||||
|
||||
if c.scheme == "https" {
|
||||
tlsConn := tls.Client(c.conn, &tls.Config{InsecureSkipVerify: true})
|
||||
|
||||
if err = tlsConn.Handshake(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.conn = tlsConn
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Disconnect() {
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
Debug("[HTTP] Disconnected: ", c.baseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPClient) isAlive() bool {
|
||||
one := make([]byte, 1)
|
||||
|
||||
// Ready 1 byte from socket without timeout to check if it not closed
|
||||
c.conn.SetReadDeadline(time.Now().Add(time.Millisecond))
|
||||
_, err := c.conn.Read(one)
|
||||
|
||||
if err == nil {
|
||||
return true
|
||||
} else if err == io.EOF {
|
||||
if c.config.Debug {
|
||||
Debug("[HTTPClient] connection closed, reconnecting")
|
||||
}
|
||||
return false
|
||||
} else if err == syscall.EPIPE {
|
||||
Debug("Detected broken pipe.", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Send(data []byte) (response []byte, err error) {
|
||||
// Don't exit on panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
Debug("[HTTPClient]", r, string(data))
|
||||
|
||||
if _, ok := r.(error); !ok {
|
||||
log.Println("[HTTPClient] Failed to send request: ", string(data))
|
||||
log.Println("PANIC: pkg:", r, debug.Stack())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if c.conn == nil || !c.isAlive() {
|
||||
Debug("[HTTPClient] Connecting:", c.baseURL)
|
||||
if err = c.Connect(); err != nil {
|
||||
log.Println("[HTTPClient] Connection error:", err)
|
||||
response = errorPayload(HTTP_CONNECTION_ERROR)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
timeout := time.Now().Add(c.config.Timeout)
|
||||
|
||||
c.conn.SetWriteDeadline(timeout)
|
||||
|
||||
if !c.config.OriginalHost {
|
||||
data = proto.SetHost(data, []byte(c.baseURL), []byte(c.host))
|
||||
}
|
||||
|
||||
if c.config.Debug {
|
||||
Debug("[HTTPClient] Sending:", string(data))
|
||||
}
|
||||
|
||||
if _, err = c.conn.Write(data); err != nil {
|
||||
Debug("[HTTPClient] Write error:", err, c.baseURL)
|
||||
response = errorPayload(HTTP_TIMEOUT)
|
||||
return
|
||||
}
|
||||
|
||||
var readBytes, n int
|
||||
var currentChunk []byte
|
||||
timeout = time.Now().Add(c.config.Timeout)
|
||||
chunked := false
|
||||
contentLength := -1
|
||||
currentContentLength := 0
|
||||
chunks := 0
|
||||
|
||||
for {
|
||||
c.conn.SetReadDeadline(timeout)
|
||||
|
||||
if readBytes < len(c.respBuf) {
|
||||
n, err = c.conn.Read(c.respBuf[readBytes:])
|
||||
readBytes += n
|
||||
chunks++
|
||||
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// First chunk
|
||||
if chunked || contentLength != -1 {
|
||||
currentContentLength += n
|
||||
} else {
|
||||
// If headers are finished
|
||||
if bytes.Contains(c.respBuf[:readBytes], proto.EmptyLine) {
|
||||
if bytes.Equal(proto.Header(c.respBuf, []byte("Transfer-Encoding")), []byte("chunked")) {
|
||||
chunked = true
|
||||
} else {
|
||||
l := proto.Header(c.respBuf, []byte("Content-Length"))
|
||||
if len(l) > 0 {
|
||||
contentLength, _ = strconv.Atoi(string(l))
|
||||
}
|
||||
}
|
||||
|
||||
currentContentLength += len(proto.Body(c.respBuf[:readBytes]))
|
||||
}
|
||||
}
|
||||
|
||||
if chunked {
|
||||
// Check if chunked message finished
|
||||
if bytes.HasSuffix(c.respBuf[:readBytes], chunkedSuffix) {
|
||||
break
|
||||
}
|
||||
} else if contentLength != -1 {
|
||||
if currentContentLength > contentLength {
|
||||
Debug("[HTTPClient] disconnected, wrong length", currentContentLength, contentLength)
|
||||
c.Disconnect()
|
||||
break
|
||||
} else if currentContentLength == contentLength {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if currentChunk == nil {
|
||||
currentChunk = make([]byte, readChunkSize)
|
||||
}
|
||||
|
||||
n, err = c.conn.Read(currentChunk)
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
Debug("[HTTPClient] Read the whole body error:", err, c.baseURL)
|
||||
break
|
||||
}
|
||||
|
||||
readBytes += int(n)
|
||||
chunks++
|
||||
currentContentLength += n
|
||||
|
||||
if chunked {
|
||||
// Check if chunked message finished
|
||||
if bytes.HasSuffix(currentChunk[:n], chunkedSuffix) {
|
||||
break
|
||||
}
|
||||
} else if contentLength != -1 {
|
||||
if currentContentLength > contentLength {
|
||||
Debug("[HTTPClient] disconnected, wrong length", currentContentLength, contentLength)
|
||||
c.Disconnect()
|
||||
break
|
||||
} else if currentContentLength == contentLength {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
Debug("[HTTPClient] disconnected, can't find Content-Length or Chunked")
|
||||
c.Disconnect()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if readBytes >= maxResponseSize {
|
||||
Debug("[HTTPClient] Body is more than the max size", maxResponseSize,
|
||||
c.baseURL)
|
||||
break
|
||||
}
|
||||
|
||||
// For following chunks expect less timeout
|
||||
timeout = time.Now().Add(c.config.Timeout / 5)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Debug("[HTTPClient] Response read error", err, c.conn, readBytes)
|
||||
response = errorPayload(HTTP_TIMEOUT)
|
||||
return
|
||||
}
|
||||
|
||||
if readBytes > len(c.respBuf) {
|
||||
readBytes = len(c.respBuf)
|
||||
}
|
||||
payload := make([]byte, readBytes)
|
||||
copy(payload, c.respBuf[:readBytes])
|
||||
|
||||
if c.config.Debug {
|
||||
Debug("[HTTPClient] Received:", string(payload))
|
||||
}
|
||||
|
||||
if c.config.FollowRedirects > 0 && c.redirectsCount < c.config.FollowRedirects {
|
||||
status := payload[9:12]
|
||||
|
||||
// 3xx requests
|
||||
if status[0] == '3' {
|
||||
c.redirectsCount++
|
||||
|
||||
location := proto.Header(payload, []byte("Location"))
|
||||
redirectPayload := []byte("GET " + string(location) + " HTTP/1.1\r\n\r\n")
|
||||
|
||||
if c.config.Debug {
|
||||
Debug("[HTTPClient] Redirecting to: " + string(location))
|
||||
}
|
||||
|
||||
return c.Send(redirectPayload)
|
||||
}
|
||||
}
|
||||
|
||||
c.redirectsCount = 0
|
||||
|
||||
return payload, err
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Get(path string) (response []byte, err error) {
|
||||
payload := "GET " + path + " HTTP/1.1\r\n\r\n"
|
||||
|
||||
return c.Send([]byte(payload))
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Post(path string, body []byte) (response []byte, err error) {
|
||||
payload := "POST " + path + " HTTP/1.1\r\n"
|
||||
payload += "Content-Length: " + strconv.Itoa(len(body)) + "\r\n\r\n"
|
||||
payload += string(body)
|
||||
|
||||
return c.Send([]byte(payload))
|
||||
}
|
||||
|
||||
const (
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200171936-Error-520-Web-server-is-returning-an-unknown-error
|
||||
HTTP_UNKNOWN_ERROR = "520"
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200171916-Error-521-Web-server-is-down
|
||||
HTTP_CONNECTION_ERROR = "521"
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200171906-Error-522-Connection-timed-out
|
||||
HTTP_CONNECTION_TIMEOUT = "522"
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200171946-Error-523-Origin-is-unreachable
|
||||
HTTP_UNREACHABLE = "523"
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200171926-Error-524-A-timeout-occurred
|
||||
HTTP_TIMEOUT = "524"
|
||||
)
|
||||
|
||||
var errorPayloadTemplate = "HTTP/1.1 202 Accepted\r\nDate: Mon, 17 Aug 2015 14:10:11 GMT\r\nContent-Length: 0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n"
|
||||
|
||||
func errorPayload(errorCode string) []byte {
|
||||
payload := make([]byte, len(errorPayloadTemplate))
|
||||
copy(payload, errorPayloadTemplate)
|
||||
|
||||
copy(payload[29:58], []byte(time.Now().Format(time.RFC1123)))
|
||||
copy(payload[9:12], errorCode)
|
||||
|
||||
return payload
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"github.com/buger/gor/proto"
|
||||
"io/ioutil"
|
||||
_ "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
_ "reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestHTTPClientURLPort(t *testing.T) {
|
||||
c1 := NewHTTPClient("http://example.com", &HTTPClientConfig{})
|
||||
if c1.baseURL != "http://example.com" {
|
||||
t.Error("Sould not add 80 port for http:", c1.baseURL)
|
||||
}
|
||||
|
||||
c2 := NewHTTPClient("https://example.com", &HTTPClientConfig{})
|
||||
if c2.baseURL != "https://example.com:443" {
|
||||
t.Error("Sould add 443 port for https:", c2.baseURL)
|
||||
}
|
||||
|
||||
c3 := NewHTTPClient("https://example.com:1", &HTTPClientConfig{})
|
||||
if c3.baseURL != "https://example.com:1" {
|
||||
t.Error("Sould use specified port:", c3.baseURL)
|
||||
}
|
||||
|
||||
c4 := NewHTTPClient("example.com", &HTTPClientConfig{})
|
||||
if c4.baseURL != "http://example.com" {
|
||||
t.Error("Sould not add default protocol:", c4.baseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientSend(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
payload := func(reqType string) []byte {
|
||||
switch reqType {
|
||||
case "GET":
|
||||
return []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
case "POST":
|
||||
return []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
case "POST_CHUNKED":
|
||||
return []byte("POST / HTTP/1.1\r\nHost: www.w3.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
|
||||
}
|
||||
|
||||
return []byte("")
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method == "POST" {
|
||||
defer r.Body.Close()
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
|
||||
if len(r.TransferEncoding) > 0 && r.TransferEncoding[0] == "chunked" {
|
||||
if string(body) != "Wikipedia in\r\n\r\nchunks." {
|
||||
t.Error("Wrong POST body:", body, string(body))
|
||||
}
|
||||
} else {
|
||||
if string(body) != "a=1&b=2" {
|
||||
buf, _ := httputil.DumpRequest(r, true)
|
||||
t.Error("Wrong POST body:", string(body), string(buf))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true})
|
||||
|
||||
wg.Add(4)
|
||||
client.Send(payload("POST"))
|
||||
client.Send(payload("GET"))
|
||||
client.Send(payload("POST_CHUNKED"))
|
||||
client.Send(payload("POST"))
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// https://github.com/buger/gor/issues/184
|
||||
func TestHTTPClientResponseBuffer(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
responseSize int
|
||||
buffserSize int
|
||||
expectedSize int
|
||||
timeout time.Duration
|
||||
}{
|
||||
{"Chunked, buffer overflow", 10 * 1024, 1024, 1024, 50 * time.Millisecond},
|
||||
|
||||
{"Chunked, fits buffer", 10 * 1024, 64 * 1024, 10*1024 + 145 /* headers length + chunked meta */, 50 * time.Millisecond},
|
||||
|
||||
{"Content-Length, buffer overflow", 1024, 1000, 1000, 50 * time.Millisecond},
|
||||
|
||||
{"Content-Length, fits buffer", 1024, 64 * 1024, 1024 + 118, 50 * time.Millisecond},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
payload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
size := tc.responseSize // 1kb
|
||||
rb := make([]byte, size)
|
||||
rand.Read(rb)
|
||||
|
||||
w.Write(rb[:size/2])
|
||||
w.Write(rb[size/2:])
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true, ResponseBufferSize: tc.buffserSize, Timeout: 100 * time.Millisecond})
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
start := time.Now()
|
||||
client.Send(payload)
|
||||
resp, err := client.Send(payload)
|
||||
stop := time.Now()
|
||||
|
||||
if err != nil {
|
||||
t.Error("Request error", err)
|
||||
}
|
||||
|
||||
if stop.Sub(start) > tc.timeout {
|
||||
t.Error("Request took too long", stop.Sub(start), tc.timeout)
|
||||
}
|
||||
|
||||
if len(resp) != tc.expectedSize {
|
||||
t.Error(tc.name, " - Wrong response size:", tc.expectedSize, len(resp))
|
||||
} else {
|
||||
if !bytes.Equal(resp[0:8], []byte("HTTP/1.1")) {
|
||||
t.Error(tc.name, " - Response buffer contains data from previous request", string(resp), len(resp))
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
server.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientHTTPSSend(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
payload := func(reqType string) []byte {
|
||||
switch reqType {
|
||||
case "GET":
|
||||
return []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
case "POST":
|
||||
return []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
case "POST_CHUNKED":
|
||||
return []byte("POST / HTTP/1.1\r\nHost: www.w3.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
|
||||
}
|
||||
|
||||
return []byte("")
|
||||
}
|
||||
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method == "POST" {
|
||||
defer r.Body.Close()
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
|
||||
if len(r.TransferEncoding) > 0 && r.TransferEncoding[0] == "chunked" {
|
||||
if string(body) != "Wikipedia in\r\n\r\nchunks." {
|
||||
t.Error("Wrong POST body:", body, string(body))
|
||||
}
|
||||
} else {
|
||||
if string(body) != "a=1&b=2" {
|
||||
buf, _ := httputil.DumpRequest(r, true)
|
||||
t.Error("Wrong POST body:", string(body), string(buf))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{})
|
||||
|
||||
wg.Add(4)
|
||||
client.Send(payload("POST"))
|
||||
client.Send(payload("GET"))
|
||||
client.Send(payload("POST_CHUNKED"))
|
||||
client.Send(payload("POST"))
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientServerInstantDisconnect(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
ln, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
defer ln.Close()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
conn.Close()
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
client := NewHTTPClient(ln.Addr().String(), &HTTPClientConfig{})
|
||||
|
||||
wg.Add(2)
|
||||
client.Send(GETPayload)
|
||||
client.Send(GETPayload)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientServerNoKeepAlive(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
ln, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
defer ln.Close()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
reqLen, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
t.Error("Error reading:", err.Error())
|
||||
}
|
||||
Debug("Received: ", string(buf[0:reqLen]))
|
||||
conn.Write([]byte("OK"))
|
||||
|
||||
// No keep-alive connections
|
||||
conn.Close()
|
||||
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
client := NewHTTPClient(ln.Addr().String(), &HTTPClientConfig{})
|
||||
|
||||
wg.Add(2)
|
||||
client.Send(GETPayload)
|
||||
client.Send(GETPayload)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientRedirect(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.URL.Path == "/" {
|
||||
http.Redirect(w, r, "/new", 301)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{FollowRedirects: 1, Debug: false})
|
||||
|
||||
// Should do 2 queries
|
||||
wg.Add(2)
|
||||
client.Send(GETPayload)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientRedirectLimit(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.URL.Path == "/" {
|
||||
http.Redirect(w, r, "/r1", 301)
|
||||
}
|
||||
|
||||
if r.URL.Path == "/r1" {
|
||||
http.Redirect(w, r, "/r2", 301)
|
||||
}
|
||||
|
||||
if r.URL.Path == "/r2" {
|
||||
http.Redirect(w, r, "/new", 301)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{FollowRedirects: 2, Debug: false})
|
||||
|
||||
// Have 3 redirects + 1 GET, but should do only 2 redirects + GET
|
||||
wg.Add(3)
|
||||
client.Send(GETPayload)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientHandleHTTP10(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
GETPayload := []byte("GET http://foobar.com/path HTTP/1.0\r\n\r\n")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.URL.Path != "/path" {
|
||||
t.Error("Path not match:", r.URL.Path)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true})
|
||||
|
||||
wg.Add(1)
|
||||
client.Send(GETPayload)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientErrors(t *testing.T) {
|
||||
req := []byte("GET http://foobar.com/path HTTP/1.0\r\n\r\n")
|
||||
|
||||
// Port not exists
|
||||
client := NewHTTPClient("http://127.0.0.1:1", &HTTPClientConfig{Debug: true})
|
||||
if resp, err := client.Send(req); err != nil {
|
||||
if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) {
|
||||
t.Error("Should return status 521 for connection refused, instead:", string(s))
|
||||
}
|
||||
} else {
|
||||
t.Error("Should throw error")
|
||||
}
|
||||
|
||||
client = NewHTTPClient("http://not.existing", &HTTPClientConfig{Debug: true})
|
||||
if resp, err := client.Send(req); err != nil {
|
||||
if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) {
|
||||
t.Error("Should return status 521 for no such host, instead:", string(s))
|
||||
}
|
||||
} else {
|
||||
t.Error("Should throw error")
|
||||
}
|
||||
|
||||
// Non routable IP address to simulate connection timeout
|
||||
client = NewHTTPClient("http://10.255.255.1", &HTTPClientConfig{Debug: true, ConnectionTimeout: 100 * time.Millisecond})
|
||||
|
||||
if resp, err := client.Send(req); err != nil {
|
||||
if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) {
|
||||
t.Error("Should return status 521 for io/timeout:", string(s))
|
||||
}
|
||||
} else {
|
||||
t.Error("Should throw error")
|
||||
}
|
||||
|
||||
// Connecting but io timeout on read
|
||||
ln, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
client = NewHTTPClient("http://"+ln.Addr().String(), &HTTPClientConfig{Debug: true, Timeout: 10 * time.Millisecond})
|
||||
defer ln.Close()
|
||||
|
||||
if resp, err := client.Send(req); err != nil {
|
||||
if s := proto.Status(resp); !bytes.Equal(s, []byte("524")) {
|
||||
t.Error("Should return status 524 for io read, instead:", string(s))
|
||||
}
|
||||
} else {
|
||||
t.Error("Should throw error")
|
||||
}
|
||||
|
||||
// Response read error read tcp [::1]:51128: connection reset by peer &{{0xc20802a000}}
|
||||
ln1, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
go func() {
|
||||
ln1.Accept()
|
||||
}()
|
||||
defer ln1.Close()
|
||||
|
||||
client = NewHTTPClient("http://"+ln1.Addr().String(), &HTTPClientConfig{Debug: true, Timeout: 10 * time.Millisecond})
|
||||
|
||||
if resp, err := client.Send(req); err != nil {
|
||||
if s := proto.Status(resp); !bytes.Equal(s, []byte("524")) {
|
||||
t.Error("Should return status 524 for connection reset by peer, instead:", string(s))
|
||||
}
|
||||
} else {
|
||||
t.Error("Should throw error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/buger/gor/proto"
|
||||
)
|
||||
|
||||
type HTTPModifier struct {
|
||||
config *HTTPModifierConfig
|
||||
}
|
||||
|
||||
func NewHTTPModifier(config *HTTPModifierConfig) *HTTPModifier {
|
||||
// Optimization to skip modifier completely if we do not need it
|
||||
if len(config.urlRegexp) == 0 &&
|
||||
len(config.urlNegativeRegexp) == 0 &&
|
||||
len(config.urlRewrite) == 0 &&
|
||||
len(config.headerFilters) == 0 &&
|
||||
len(config.headerNegativeFilters) == 0 &&
|
||||
len(config.headerHashFilters) == 0 &&
|
||||
len(config.paramHashFilters) == 0 &&
|
||||
len(config.params) == 0 &&
|
||||
len(config.headers) == 0 &&
|
||||
len(config.methods) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &HTTPModifier{config: config}
|
||||
}
|
||||
|
||||
func (m *HTTPModifier) Rewrite(payload []byte) (response []byte) {
|
||||
if !proto.IsHTTPPayload(payload) {
|
||||
return payload
|
||||
}
|
||||
|
||||
if len(m.config.methods) > 0 {
|
||||
method := proto.Method(payload)
|
||||
|
||||
matched := false
|
||||
|
||||
for _, m := range m.config.methods {
|
||||
if bytes.Equal(method, m) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !matched {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.headers) > 0 {
|
||||
for _, header := range m.config.headers {
|
||||
payload = proto.SetHeader(payload, []byte(header.Name), []byte(header.Value))
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.params) > 0 {
|
||||
for _, param := range m.config.params {
|
||||
payload = proto.SetPathParam(payload, param.Name, param.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.urlRegexp) > 0 {
|
||||
path := proto.Path(payload)
|
||||
|
||||
matched := false
|
||||
|
||||
for _, f := range m.config.urlRegexp {
|
||||
if f.regexp.Match(path) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !matched {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.urlNegativeRegexp) > 0 {
|
||||
path := proto.Path(payload)
|
||||
|
||||
for _, f := range m.config.urlNegativeRegexp {
|
||||
if f.regexp.Match(path) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.headerFilters) > 0 {
|
||||
for _, f := range m.config.headerFilters {
|
||||
value := proto.Header(payload, f.name)
|
||||
|
||||
if len(value) > 0 && !f.regexp.Match(value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.headerNegativeFilters) > 0 {
|
||||
for _, f := range m.config.headerNegativeFilters {
|
||||
value := proto.Header(payload, f.name)
|
||||
|
||||
if len(value) > 0 && f.regexp.Match(value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.headerHashFilters) > 0 {
|
||||
for _, f := range m.config.headerHashFilters {
|
||||
value := proto.Header(payload, f.name)
|
||||
|
||||
if len(value) > 0 {
|
||||
hasher := fnv.New32a()
|
||||
hasher.Write(value)
|
||||
|
||||
if (hasher.Sum32() % 100) >= f.percent {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.paramHashFilters) > 0 {
|
||||
for _, f := range m.config.paramHashFilters {
|
||||
value, s, _ := proto.PathParam(payload, f.name)
|
||||
|
||||
if s != -1 {
|
||||
hasher := fnv.New32a()
|
||||
hasher.Write(value)
|
||||
|
||||
if (hasher.Sum32() % 100) >= f.percent {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.urlRewrite) > 0 {
|
||||
path := proto.Path(payload)
|
||||
|
||||
for _, f := range m.config.urlRewrite {
|
||||
if f.src.Match(path) {
|
||||
path = f.src.ReplaceAll(path, f.target)
|
||||
payload = proto.SetPath(payload, path)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HTTPModifierConfig holds configuration options for built-in traffic modifier
|
||||
type HTTPModifierConfig struct {
|
||||
urlNegativeRegexp HTTPUrlRegexp
|
||||
urlRegexp HTTPUrlRegexp
|
||||
urlRewrite UrlRewriteMap
|
||||
headerFilters HTTPHeaderFilters
|
||||
headerNegativeFilters HTTPHeaderFilters
|
||||
headerHashFilters HTTPHashFilters
|
||||
paramHashFilters HTTPHashFilters
|
||||
|
||||
params HTTPParams
|
||||
headers HTTPHeaders
|
||||
methods HTTPMethods
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-header, --http-disallow-header options
|
||||
//
|
||||
type headerFilter struct {
|
||||
name []byte
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
// HTTPHeaderFilters holds list of headers and their regexps
|
||||
type HTTPHeaderFilters []headerFilter
|
||||
|
||||
func (h *HTTPHeaderFilters) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
func (h *HTTPHeaderFilters) Set(value string) error {
|
||||
valArr := strings.SplitN(value, ":", 2)
|
||||
if len(valArr) < 2 {
|
||||
return errors.New("need both header and value, colon-delimited (ex. user_id:^169$).")
|
||||
}
|
||||
r, err := regexp.Compile(valArr[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*h = append(*h, headerFilter{name: []byte(valArr[0]), regexp: r})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-header-hash and --http-allow-param-hash options
|
||||
//
|
||||
type hashFilter struct {
|
||||
name []byte
|
||||
percent uint32
|
||||
}
|
||||
|
||||
type HTTPHashFilters []hashFilter
|
||||
|
||||
func (h *HTTPHashFilters) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
func (h *HTTPHashFilters) Set(value string) error {
|
||||
valArr := strings.SplitN(value, ":", 2)
|
||||
if len(valArr) < 2 {
|
||||
return errors.New("need both header and value, colon-delimited (ex. user_id:50%)")
|
||||
}
|
||||
|
||||
f := hashFilter{name: []byte(valArr[0])}
|
||||
|
||||
if strings.Contains(valArr[1], "%") {
|
||||
p, _ := strconv.ParseInt(valArr[1][:len(valArr[1])-1], 0, 0)
|
||||
f.percent = uint32(p)
|
||||
} else if strings.Contains(valArr[1], "/") {
|
||||
// DEPRECATED format
|
||||
var num, den uint64
|
||||
|
||||
fracArr := strings.Split(valArr[1], "/")
|
||||
num, _ = strconv.ParseUint(fracArr[0], 10, 64)
|
||||
den, _ = strconv.ParseUint(fracArr[1], 10, 64)
|
||||
|
||||
f.percent = uint32((float64(num) / float64(den)) * 100)
|
||||
} else {
|
||||
return errors.New("Value should be percent and contain '%'")
|
||||
}
|
||||
|
||||
*h = append(*h, f)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-set-header option
|
||||
//
|
||||
type HTTPHeaders []HTTPHeader
|
||||
type HTTPHeader struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (h *HTTPHeaders) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
func (h *HTTPHeaders) Set(value string) error {
|
||||
v := strings.SplitN(value, ":", 2)
|
||||
if len(v) != 2 {
|
||||
return errors.New("Expected `Key: Value`")
|
||||
}
|
||||
|
||||
header := HTTPHeader{
|
||||
strings.TrimSpace(v[0]),
|
||||
strings.TrimSpace(v[1]),
|
||||
}
|
||||
|
||||
*h = append(*h, header)
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-set-param option
|
||||
//
|
||||
type HTTPParams []HTTPParam
|
||||
type HTTPParam struct {
|
||||
Name []byte
|
||||
Value []byte
|
||||
}
|
||||
|
||||
func (h *HTTPParams) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
func (h *HTTPParams) Set(value string) error {
|
||||
v := strings.SplitN(value, "=", 2)
|
||||
if len(v) != 2 {
|
||||
return errors.New("Expected `Key=Value`")
|
||||
}
|
||||
|
||||
param := HTTPParam{
|
||||
[]byte(strings.TrimSpace(v[0])),
|
||||
[]byte(strings.TrimSpace(v[1])),
|
||||
}
|
||||
|
||||
*h = append(*h, param)
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-method option
|
||||
//
|
||||
type HTTPMethods [][]byte
|
||||
|
||||
func (h *HTTPMethods) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
func (h *HTTPMethods) Set(value string) error {
|
||||
*h = append(*h, []byte(value))
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-rewrite-url option
|
||||
//
|
||||
type urlRewrite struct {
|
||||
src *regexp.Regexp
|
||||
target []byte
|
||||
}
|
||||
|
||||
type UrlRewriteMap []urlRewrite
|
||||
|
||||
func (r *UrlRewriteMap) String() string {
|
||||
return fmt.Sprint(*r)
|
||||
}
|
||||
|
||||
func (r *UrlRewriteMap) Set(value string) error {
|
||||
valArr := strings.SplitN(value, ":", 2)
|
||||
if len(valArr) < 2 {
|
||||
return errors.New("need both src and target, colon-delimited (ex. /a:/b)")
|
||||
}
|
||||
regexp, err := regexp.Compile(valArr[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*r = append(*r, urlRewrite{src: regexp, target: []byte(valArr[1])})
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-url option
|
||||
//
|
||||
type urlRegexp struct {
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
type HTTPUrlRegexp []urlRegexp
|
||||
|
||||
func (r *HTTPUrlRegexp) String() string {
|
||||
return fmt.Sprint(*r)
|
||||
}
|
||||
|
||||
func (r *HTTPUrlRegexp) Set(value string) error {
|
||||
regexp, err := regexp.Compile(value)
|
||||
|
||||
*r = append(*r, urlRegexp{regexp: regexp})
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPHeaderFilters(t *testing.T) {
|
||||
filters := HTTPHeaderFilters{}
|
||||
|
||||
err := filters.Set("Header1:^$")
|
||||
if err != nil {
|
||||
t.Error("Should not error on Header1:^$")
|
||||
}
|
||||
|
||||
err = filters.Set("Header2:^:$")
|
||||
if err != nil {
|
||||
t.Error("Should not error on Header2:^:$")
|
||||
}
|
||||
|
||||
// Missing colon
|
||||
err = filters.Set("Header3-^$")
|
||||
if err == nil {
|
||||
t.Error("Should error on Header2:^:$")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPHashFilters(t *testing.T) {
|
||||
filters := HTTPHashFilters{}
|
||||
|
||||
err := filters.Set("Header1:1/2")
|
||||
if err != nil {
|
||||
t.Error("Should support old syntax")
|
||||
}
|
||||
|
||||
if filters[0].percent != 50 {
|
||||
t.Error("Wrong percentage", filters[0].percent)
|
||||
}
|
||||
|
||||
err = filters.Set("Header2:1")
|
||||
if err == nil {
|
||||
t.Error("Should error on Header2 because no % symbol")
|
||||
}
|
||||
|
||||
err = filters.Set("Header2:10%")
|
||||
if err != nil {
|
||||
t.Error("Should pass")
|
||||
}
|
||||
|
||||
if filters[1].percent != 10 {
|
||||
t.Error("Wrong percentage", filters[1].percent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUrlRewriteMap(t *testing.T) {
|
||||
var err error
|
||||
rewrites := UrlRewriteMap{}
|
||||
|
||||
if err = rewrites.Set("/v1/user/([^\\/]+)/ping:/v2/user/$1/ping"); err != nil {
|
||||
t.Error("Should set mapping", err)
|
||||
}
|
||||
|
||||
if err = rewrites.Set("/v1/user/([^\\/]+)/ping"); err == nil {
|
||||
t.Error("Should not set mapping without :")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/buger/gor/proto"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPModifierWithoutConfig(t *testing.T) {
|
||||
if NewHTTPModifier(&HTTPModifierConfig{}) != nil {
|
||||
t.Error("If no config specified should not be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierHeaderFilters(t *testing.T) {
|
||||
filters := HTTPHeaderFilters{}
|
||||
filters.Set("Host:^www.w3.org$")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerFilters: filters,
|
||||
})
|
||||
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if len(modifier.Rewrite(payload)) == 0 {
|
||||
t.Error("Request should pass filters")
|
||||
}
|
||||
|
||||
filters = HTTPHeaderFilters{}
|
||||
// Setting filter that not match our header
|
||||
filters.Set("Host:^www.w4.org$")
|
||||
|
||||
modifier = NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerFilters: filters,
|
||||
})
|
||||
|
||||
if len(modifier.Rewrite(payload)) != 0 {
|
||||
t.Error("Request should not pass filters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierHeaderNegativeFilters(t *testing.T) {
|
||||
filters := HTTPHeaderFilters{}
|
||||
filters.Set("Host:^www.w3.org$")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerNegativeFilters: filters,
|
||||
})
|
||||
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w4.org\r\n\r\na=1&b=2")
|
||||
|
||||
if len(modifier.Rewrite(payload)) == 0 {
|
||||
t.Error("Request should pass filters")
|
||||
}
|
||||
|
||||
filters = HTTPHeaderFilters{}
|
||||
// Setting filter that not match our header
|
||||
filters.Set("Host:^www.w4.org$")
|
||||
|
||||
modifier = NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerNegativeFilters: filters,
|
||||
})
|
||||
|
||||
if len(modifier.Rewrite(payload)) != 0 {
|
||||
t.Error("Request should not pass filters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierURLRewrite(t *testing.T) {
|
||||
var url, newURL []byte
|
||||
|
||||
rewrites := UrlRewriteMap{}
|
||||
|
||||
payload := func(url []byte) []byte {
|
||||
return []byte("POST " + string(url) + " HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
}
|
||||
|
||||
err := rewrites.Set("/v1/user/([^\\/]+)/ping:/v2/user/$1/ping")
|
||||
if err != nil {
|
||||
t.Error("Should not error on /v1/user/([^\\/]+)/ping:/v2/user/$1/ping")
|
||||
}
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
urlRewrite: rewrites,
|
||||
})
|
||||
|
||||
url = []byte("/v1/user/joe/ping")
|
||||
if newURL = proto.Path(modifier.Rewrite(payload(url))); bytes.Equal(newURL, url) {
|
||||
t.Error("Request url should have been rewritten, wasn't", string(newURL))
|
||||
}
|
||||
|
||||
url = []byte("/v1/user/ping")
|
||||
if newURL = proto.Path(modifier.Rewrite(payload(url))); !bytes.Equal(newURL, url) {
|
||||
t.Error("Request url should have been rewritten, wasn't", string(newURL))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierHeaderHashFilters(t *testing.T) {
|
||||
filters := HTTPHashFilters{}
|
||||
filters.Set("Header2:1/2")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerHashFilters: filters,
|
||||
})
|
||||
|
||||
payload := func(header []byte) []byte {
|
||||
return []byte("POST / HTTP/1.1\r\n" + string(header) + "Content-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
}
|
||||
|
||||
if p := modifier.Rewrite(payload([]byte(""))); len(p) == 0 {
|
||||
t.Error("Request should pass filters if Header does not exist")
|
||||
}
|
||||
|
||||
if p := modifier.Rewrite(payload([]byte("Header2: 3\r\n"))); len(p) > 0 {
|
||||
t.Error("Request should not pass filters, Header2 hash too high")
|
||||
}
|
||||
|
||||
if p := modifier.Rewrite(payload([]byte("Header2: 1\r\n"))); len(p) == 0 {
|
||||
t.Error("Request should pass filters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierParamHashFilters(t *testing.T) {
|
||||
filters := HTTPHashFilters{}
|
||||
filters.Set("user_id:1/2")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
paramHashFilters: filters,
|
||||
})
|
||||
|
||||
payload := func(value []byte) []byte {
|
||||
return []byte("POST /" + string(value) + " HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
}
|
||||
|
||||
if p := modifier.Rewrite(payload([]byte(""))); len(p) == 0 {
|
||||
t.Error("Request should pass filters if param does not exist")
|
||||
}
|
||||
|
||||
if p := modifier.Rewrite(payload([]byte("?user_id=3"))); len(p) > 0 {
|
||||
t.Error("Request should not pass filters", string(p))
|
||||
}
|
||||
|
||||
if p := modifier.Rewrite(payload([]byte("?user_id=1"))); len(p) == 0 {
|
||||
t.Error("Request should pass filters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierHeaders(t *testing.T) {
|
||||
headers := HTTPHeaders{}
|
||||
headers.Set("Header1:1")
|
||||
headers.Set("Host:localhost")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
headers: headers,
|
||||
})
|
||||
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
newPayload := []byte("POST /post HTTP/1.1\r\nHeader1: 1\r\nContent-Length: 7\r\nHost: localhost\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = modifier.Rewrite(payload); !bytes.Equal(payload, newPayload) {
|
||||
t.Error("Should update request headers", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierURLRegexp(t *testing.T) {
|
||||
filters := HTTPUrlRegexp{}
|
||||
filters.Set("/v1/app")
|
||||
filters.Set("/v1/api")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
urlRegexp: filters,
|
||||
})
|
||||
|
||||
payload := func(url string) []byte {
|
||||
return []byte("POST " + url + " HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
}
|
||||
|
||||
if len(modifier.Rewrite(payload("/v1/app/test"))) == 0 {
|
||||
t.Error("Should pass url")
|
||||
}
|
||||
|
||||
if len(modifier.Rewrite(payload("/v1/api/test"))) == 0 {
|
||||
t.Error("Should pass url")
|
||||
}
|
||||
|
||||
if len(modifier.Rewrite(payload("/other"))) > 0 {
|
||||
t.Error("Should not pass url")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierURLNegativeRegexp(t *testing.T) {
|
||||
filters := HTTPUrlRegexp{}
|
||||
filters.Set("/restricted1")
|
||||
filters.Set("/some/restricted2")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
urlNegativeRegexp: filters,
|
||||
})
|
||||
|
||||
payload := func(url string) []byte {
|
||||
return []byte("POST " + url + " HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
}
|
||||
|
||||
if len(modifier.Rewrite(payload("/v1/app/test"))) == 0 {
|
||||
t.Error("Should pass url")
|
||||
}
|
||||
|
||||
if len(modifier.Rewrite(payload("/restricted1"))) > 0 {
|
||||
t.Error("Should not pass url")
|
||||
}
|
||||
|
||||
if len(modifier.Rewrite(payload("/some/restricted2"))) > 0 {
|
||||
t.Error("Should not pass url")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierSetHeader(t *testing.T) {
|
||||
filters := HTTPHeaders{}
|
||||
filters.Set("User-Agent:Gor")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
headers: filters,
|
||||
})
|
||||
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter := []byte("POST /post HTTP/1.1\r\nUser-Agent: Gor\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = modifier.Rewrite(payload); !bytes.Equal(payloadAfter, payload) {
|
||||
t.Error("Should add new header", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierSetParam(t *testing.T) {
|
||||
filters := HTTPParams{}
|
||||
filters.Set("api_key=1")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
params: filters,
|
||||
})
|
||||
|
||||
payload := []byte("POST /post?api_key=1234 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter := []byte("POST /post?api_key=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = modifier.Rewrite(payload); !bytes.Equal(payloadAfter, payload) {
|
||||
t.Error("Should override param", string(payload))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DummyInput used for debugging. It generate 1 "GET /"" request per second.
|
||||
type DummyInput struct {
|
||||
data chan []byte
|
||||
}
|
||||
|
||||
// NewDummyInput constructor for DummyInput
|
||||
func NewDummyInput(options string) (di *DummyInput) {
|
||||
di = new(DummyInput)
|
||||
di.data = make(chan []byte)
|
||||
|
||||
go di.emit()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *DummyInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
|
||||
copy(data, buf)
|
||||
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (i *DummyInput) emit() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
uuid := uuid()
|
||||
reqh := payloadHeader(RequestPayload, uuid, time.Now().UnixNano())
|
||||
i.data <- append(reqh, []byte("GET / HTTP/1.1\r\nHost: www.w3.org\r\nUser-Agent: Go 1.1 package http\r\nAccept-Encoding: gzip\r\n\r\n")...)
|
||||
|
||||
resh := payloadHeader(ResponsePayload, uuid, 1)
|
||||
i.data <- append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *DummyInput) String() string {
|
||||
return "Dummy Input"
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FileInput can read requests generated by FileOutput
|
||||
type FileInput struct {
|
||||
data chan []byte
|
||||
exit chan bool
|
||||
path string
|
||||
fileInputReaders []*fileInputReader
|
||||
speedFactor float64
|
||||
loop bool
|
||||
}
|
||||
|
||||
|
||||
type fileInputReader struct {
|
||||
reader *bufio.Reader
|
||||
meta [][]byte
|
||||
data []byte
|
||||
file *os.File
|
||||
timestamp int64
|
||||
}
|
||||
|
||||
// NewFileInput constructor for FileInput. Accepts file path as argument.
|
||||
func NewFileInput(path string, loop bool) (i *FileInput) {
|
||||
i = new(FileInput)
|
||||
i.data = make(chan []byte)
|
||||
i.exit = make(chan bool)
|
||||
i.path = path
|
||||
i.speedFactor = 1
|
||||
i.loop = loop
|
||||
|
||||
if err := i.init(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go i.emit()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type NextFileNotFound struct{}
|
||||
|
||||
func (_ *NextFileNotFound) Error() string {
|
||||
return "There is no new files"
|
||||
}
|
||||
|
||||
func (i *FileInput) init() (err error) {
|
||||
var matches []string
|
||||
|
||||
if matches, err = filepath.Glob(i.path); err != nil {
|
||||
log.Println("Wrong file pattern", i.path, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
log.Println("No files match pattern: ", i.path)
|
||||
return errors.New("No matching files")
|
||||
}
|
||||
|
||||
i.fileInputReaders = make([]*fileInputReader, len(matches))
|
||||
|
||||
for idx, p := range matches {
|
||||
file, _ := os.Open(p)
|
||||
fileInputReader := &fileInputReader{}
|
||||
fileInputReader.file = file
|
||||
if strings.HasSuffix(p, ".gz") {
|
||||
gzReader, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fileInputReader.reader = bufio.NewReader(gzReader)
|
||||
} else {
|
||||
fileInputReader.reader = bufio.NewReader(file)
|
||||
}
|
||||
|
||||
fileInputReader.readNextInput()
|
||||
i.fileInputReaders[idx] = fileInputReader
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *FileInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
copy(data, buf)
|
||||
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (i *FileInput) String() string {
|
||||
return "File input: " + i.path
|
||||
}
|
||||
|
||||
func (f *fileInputReader) readNextInput() {
|
||||
nextInput := f.nextInput()
|
||||
f.parseNextInput(nextInput)
|
||||
}
|
||||
|
||||
func (f *fileInputReader) parseNextInput(input []byte) {
|
||||
if (input != nil) {
|
||||
f.meta = payloadMeta(input)
|
||||
f.timestamp, _ = strconv.ParseInt(string(f.meta[2]), 10, 64)
|
||||
f.data = input
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fileInputReader) nextInput() []byte {
|
||||
payloadSeparatorAsBytes := []byte(payloadSeparator)
|
||||
var buffer bytes.Buffer
|
||||
|
||||
for {
|
||||
line, err := f.reader.ReadBytes('\n')
|
||||
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
f.file.Close()
|
||||
f.file = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if bytes.Equal(payloadSeparatorAsBytes[1:], line) {
|
||||
asBytes := buffer.Bytes()
|
||||
|
||||
// Bytes() returns only pointer, so to remove data-race copy the data to an array
|
||||
newBuf := make([]byte, len(asBytes) - 1)
|
||||
copy(newBuf, asBytes)
|
||||
return newBuf
|
||||
}
|
||||
|
||||
buffer.Write(line)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *FileInput) nextInputReader() *fileInputReader {
|
||||
var nextFileInputReader *fileInputReader
|
||||
for _, fileInputReader := range i.fileInputReaders {
|
||||
if fileInputReader.file == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if fileInputReader.meta[0][0] == ResponsePayload {
|
||||
return fileInputReader
|
||||
}
|
||||
|
||||
if nextFileInputReader == nil || nextFileInputReader.timestamp > fileInputReader.timestamp {
|
||||
nextFileInputReader = fileInputReader
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nextFileInputReader;
|
||||
}
|
||||
|
||||
func (i *FileInput) emit() {
|
||||
var lastTime int64 = -1
|
||||
|
||||
for {
|
||||
fileInputReader := i.nextInputReader()
|
||||
|
||||
if fileInputReader == nil {
|
||||
if i.loop {
|
||||
i.init()
|
||||
lastTime = -1
|
||||
continue
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if fileInputReader.meta[0][0] == RequestPayload {
|
||||
lastTime = i.simulateRequestDelay(fileInputReader, lastTime)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-i.exit:
|
||||
for _, fileInputReader := range i.fileInputReaders {
|
||||
if fileInputReader.file != nil {
|
||||
fileInputReader.file.Close()
|
||||
}
|
||||
}
|
||||
break
|
||||
case i.data <- fileInputReader.data:
|
||||
fileInputReader.readNextInput()
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("FileInput: end of file '%s'\n", i.path)
|
||||
}
|
||||
|
||||
func (i*FileInput) simulateRequestDelay(fileInputReader *fileInputReader, lastTime int64) int64 {
|
||||
if lastTime != -1 {
|
||||
timeDiff := fileInputReader.timestamp - lastTime
|
||||
|
||||
if i.speedFactor != 1 {
|
||||
timeDiff = int64(float64(timeDiff) / i.speedFactor)
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(timeDiff))
|
||||
}
|
||||
|
||||
return fileInputReader.timestamp
|
||||
}
|
||||
|
||||
func (i *FileInput) Close() error {
|
||||
i.exit <- true
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ = log.Println
|
||||
|
||||
func TestInputFileWithGET(t *testing.T) {
|
||||
|
||||
input := NewTestInput()
|
||||
rg := NewRequestGenerator([]io.Reader{input}, func() { input.EmitGET() }, 1)
|
||||
readPayloads := [][]byte{}
|
||||
|
||||
// Given a capture file with a GET request
|
||||
expectedCaptureFile := CreateCaptureFile(rg)
|
||||
defer expectedCaptureFile.TearDown()
|
||||
|
||||
// When the request is read from the capture file
|
||||
err := ReadFromCaptureFile(expectedCaptureFile.file, 1, func(data []byte) {
|
||||
readPayloads = append(readPayloads, Duplicate(data))
|
||||
})
|
||||
|
||||
// The read request should match the original request
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
if !expectedCaptureFile.PayloadsEqual(readPayloads) {
|
||||
t.Error("Request read back from file should match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputFileWithPayloadLargerThan64Kb(t *testing.T) {
|
||||
|
||||
input := NewTestInput()
|
||||
rg := NewRequestGenerator([]io.Reader{input}, func() { input.EmitSizedPOST(64 * 1024) }, 1)
|
||||
readPayloads := [][]byte{}
|
||||
|
||||
// Given a capture file with a request over 64Kb
|
||||
expectedCaptureFile := CreateCaptureFile(rg)
|
||||
defer expectedCaptureFile.TearDown()
|
||||
|
||||
// When the request is read from the capture file
|
||||
err := ReadFromCaptureFile(expectedCaptureFile.file, 1, func(data []byte) {
|
||||
readPayloads = append(readPayloads, Duplicate(data))
|
||||
})
|
||||
|
||||
// The read request should match the original request
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
if !expectedCaptureFile.PayloadsEqual(readPayloads) {
|
||||
t.Error("Request read back from file should match")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInputFileWithGETAndPOST(t *testing.T) {
|
||||
|
||||
input := NewTestInput()
|
||||
rg := NewRequestGenerator([]io.Reader{input}, func() {
|
||||
input.EmitGET()
|
||||
input.EmitPOST()
|
||||
}, 2)
|
||||
readPayloads := [][]byte{}
|
||||
|
||||
// Given a capture file with a GET request
|
||||
expectedCaptureFile := CreateCaptureFile(rg)
|
||||
defer expectedCaptureFile.TearDown()
|
||||
|
||||
// When the requests are read from the capture file
|
||||
err := ReadFromCaptureFile(expectedCaptureFile.file, 2, func(data []byte) {
|
||||
readPayloads = append(readPayloads, Duplicate(data))
|
||||
})
|
||||
|
||||
// The read requests should match the original request
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
if !expectedCaptureFile.PayloadsEqual(readPayloads) {
|
||||
t.Error("Request read back from file should match")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInputFileMultipleFilesWithRequestsOnly(t *testing.T) {
|
||||
rnd := rand.Int63()
|
||||
|
||||
file1, _ := os.OpenFile(fmt.Sprintf("/tmp/%d_0", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
|
||||
file1.Write([]byte("1 1 1\ntest1"))
|
||||
file1.Write([]byte(payloadSeparator))
|
||||
file1.Write([]byte("1 1 3\ntest2"))
|
||||
file1.Write([]byte(payloadSeparator))
|
||||
file1.Close()
|
||||
|
||||
file2, _ := os.OpenFile(fmt.Sprintf("/tmp/%d_1", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
|
||||
file2.Write([]byte("1 1 2\ntest3"))
|
||||
file2.Write([]byte(payloadSeparator))
|
||||
file2.Write([]byte("1 1 4\ntest4"))
|
||||
file2.Write([]byte(payloadSeparator))
|
||||
file2.Close()
|
||||
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false)
|
||||
buf := make([]byte, 1000)
|
||||
|
||||
for i := '1'; i <= '4'; i++ {
|
||||
n, _ := input.Read(buf)
|
||||
if buf[4] != byte(i) {
|
||||
t.Error("Should emit requests in right order", string(buf[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
os.Remove(file1.Name())
|
||||
os.Remove(file2.Name())
|
||||
}
|
||||
|
||||
func TestInputFileRequestsWithLatency(t *testing.T) {
|
||||
rnd := rand.Int63()
|
||||
|
||||
file, _ := os.OpenFile(fmt.Sprintf("/tmp/%d", rnd), os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0660)
|
||||
defer file.Close()
|
||||
|
||||
file.Write([]byte("1 1 100000000\nrequest1"))
|
||||
file.Write([]byte(payloadSeparator))
|
||||
file.Write([]byte("1 2 150000000\nrequest2"))
|
||||
file.Write([]byte(payloadSeparator))
|
||||
file.Write([]byte("1 3 250000000\nrequest3"))
|
||||
file.Write([]byte(payloadSeparator))
|
||||
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d", rnd), false)
|
||||
buf := make([]byte, 1000)
|
||||
|
||||
start := time.Now().UnixNano()
|
||||
for i := 0; i < 3; i++ {
|
||||
input.Read(buf)
|
||||
}
|
||||
end := time.Now().UnixNano()
|
||||
|
||||
var expectedLatency int64 = 250000000 - 100000000
|
||||
realLatency := end - start
|
||||
if realLatency < expectedLatency {
|
||||
t.Errorf("Should emit requests respecting latency. Expected: %v, real: %v", expectedLatency, realLatency)
|
||||
}
|
||||
|
||||
if realLatency > expectedLatency + 10000000 {
|
||||
t.Errorf("Should emit requests respecting latency. Expected: %v, real: %v", expectedLatency, realLatency)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputFileMultipleFilesWithRequestsAndResponses(t *testing.T) {
|
||||
rnd := rand.Int63()
|
||||
|
||||
file1, _ := os.OpenFile(fmt.Sprintf("/tmp/%d_0", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
|
||||
file1.Write([]byte("1 1 1\nrequest1"))
|
||||
file1.Write([]byte(payloadSeparator))
|
||||
file1.Write([]byte("2 1 1\nresponse1"))
|
||||
file1.Write([]byte(payloadSeparator))
|
||||
file1.Write([]byte("1 2 3\nrequest2"))
|
||||
file1.Write([]byte(payloadSeparator))
|
||||
file1.Write([]byte("2 2 3\nresponse2"))
|
||||
file1.Write([]byte(payloadSeparator))
|
||||
file1.Close()
|
||||
|
||||
file2, _ := os.OpenFile(fmt.Sprintf("/tmp/%d_1", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
|
||||
file2.Write([]byte("1 3 2\nrequest3"))
|
||||
file2.Write([]byte(payloadSeparator))
|
||||
file2.Write([]byte("2 3 2\nresponse3"))
|
||||
file2.Write([]byte(payloadSeparator))
|
||||
file2.Write([]byte("1 4 4\nrequest4"))
|
||||
file2.Write([]byte(payloadSeparator))
|
||||
file2.Write([]byte("2 4 4\nresponse4"))
|
||||
file2.Write([]byte(payloadSeparator))
|
||||
file2.Close()
|
||||
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false)
|
||||
buf := make([]byte, 1000)
|
||||
|
||||
for i := '1'; i <= '4'; i++ {
|
||||
n, _ := input.Read(buf)
|
||||
if buf[0] != '1' && buf[4] != byte(i) {
|
||||
t.Error("Shound emit requests in right order", string(buf[:n]))
|
||||
}
|
||||
|
||||
n, _ = input.Read(buf)
|
||||
if buf[0] != '2' && buf[4] != byte(i) {
|
||||
t.Error("Shound emit responses in right order", string(buf[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
os.Remove(file1.Name())
|
||||
os.Remove(file2.Name())
|
||||
}
|
||||
|
||||
func TestInputFileLoop(t *testing.T) {
|
||||
rnd := rand.Int63()
|
||||
|
||||
file, _ := os.OpenFile(fmt.Sprintf("/tmp/%d", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
|
||||
file.Write([]byte("1 1 1\ntest1"))
|
||||
file.Write([]byte(payloadSeparator))
|
||||
file.Write([]byte("1 1 2\ntest2"))
|
||||
file.Write([]byte(payloadSeparator))
|
||||
file.Close()
|
||||
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d", rnd), true)
|
||||
buf := make([]byte, 1000)
|
||||
|
||||
// Even if we have just 2 requests in file, it should indifinitly loop
|
||||
for i := 0; i < 1000; i++ {
|
||||
input.Read(buf)
|
||||
}
|
||||
|
||||
input.Close()
|
||||
os.Remove(file.Name())
|
||||
}
|
||||
|
||||
func TestInputFileCompressed(t *testing.T) {
|
||||
rnd := rand.Int63()
|
||||
|
||||
output := NewFileOutput(fmt.Sprintf("/tmp/%d_0.gz", rnd), &FileOutputConfig{flushInterval: time.Minute, append: true})
|
||||
for i := 0; i < 1000; i++ {
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
}
|
||||
name1 := output.file.Name()
|
||||
output.Close()
|
||||
|
||||
output2 := NewFileOutput(fmt.Sprintf("/tmp/%d_1.gz", rnd), &FileOutputConfig{flushInterval: time.Minute, append: true})
|
||||
for i := 0; i < 1000; i++ {
|
||||
output2.Write([]byte("1 1 1\r\ntest"))
|
||||
}
|
||||
name2 := output2.file.Name()
|
||||
output2.Close()
|
||||
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false)
|
||||
buf := make([]byte, 1000)
|
||||
for i := 0; i < 2000; i++ {
|
||||
input.Read(buf)
|
||||
}
|
||||
|
||||
os.Remove(name1)
|
||||
os.Remove(name2)
|
||||
}
|
||||
|
||||
type CaptureFile struct {
|
||||
data [][]byte
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func NewExpectedCaptureFile(data [][]byte, file *os.File) *CaptureFile {
|
||||
ecf := new(CaptureFile)
|
||||
ecf.file = file
|
||||
ecf.data = data
|
||||
return ecf
|
||||
}
|
||||
|
||||
func (expectedCaptureFile *CaptureFile) TearDown() {
|
||||
if expectedCaptureFile.file != nil {
|
||||
syscall.Unlink(expectedCaptureFile.file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
type RequestGenerator struct {
|
||||
inputs []io.Reader
|
||||
emit func()
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewRequestGenerator(inputs []io.Reader, emit func(), count int) (rg *RequestGenerator) {
|
||||
rg = new(RequestGenerator)
|
||||
rg.inputs = inputs
|
||||
rg.emit = emit
|
||||
rg.wg = new(sync.WaitGroup)
|
||||
rg.wg.Add(count)
|
||||
return
|
||||
}
|
||||
|
||||
func (expectedCaptureFile *CaptureFile) PayloadsEqual(other [][]byte) bool {
|
||||
|
||||
if len(expectedCaptureFile.data) != len(other) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, payload := range other {
|
||||
|
||||
if !bytes.Equal(expectedCaptureFile.data[i], payload) {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
func CreateCaptureFile(requestGenerator *RequestGenerator) *CaptureFile {
|
||||
|
||||
f, err := ioutil.TempFile("", "testmainconf")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
quit := make(chan int)
|
||||
|
||||
readPayloads := [][]byte{}
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
readPayloads = append(readPayloads, Duplicate(data))
|
||||
|
||||
requestGenerator.wg.Done()
|
||||
})
|
||||
|
||||
outputFile := NewFileOutput(f.Name(), &FileOutputConfig{flushInterval: time.Minute, append: true})
|
||||
|
||||
Plugins.Inputs = requestGenerator.inputs
|
||||
Plugins.Outputs = []io.Writer{output, outputFile}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
requestGenerator.emit()
|
||||
requestGenerator.wg.Wait()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
outputFile.Close()
|
||||
|
||||
close(quit)
|
||||
|
||||
return NewExpectedCaptureFile(readPayloads, f)
|
||||
|
||||
}
|
||||
|
||||
func ReadFromCaptureFile(captureFile *os.File, count int, callback writeCallback) (err error) {
|
||||
|
||||
quit := make(chan int)
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
input := NewFileInput(captureFile.Name(), false)
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
callback(data)
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
wg.Add(count)
|
||||
go Start(quit)
|
||||
|
||||
done := make(chan int, 1)
|
||||
go func() {
|
||||
wg.Wait()
|
||||
done <- 1
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
break
|
||||
case <-time.After(2 * time.Second):
|
||||
err = errors.New("Timed out")
|
||||
}
|
||||
close(quit)
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func Duplicate(data []byte) (duplicate []byte) {
|
||||
duplicate = make([]byte, len(data))
|
||||
copy(duplicate, data)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTPInput used for sending requests to Gor via http
|
||||
type HTTPInput struct {
|
||||
data chan []byte
|
||||
address string
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewHTTPInput constructor for HTTPInput. Accepts address with port which he will listen on.
|
||||
func NewHTTPInput(address string) (i *HTTPInput) {
|
||||
i = new(HTTPInput)
|
||||
i.data = make(chan []byte, 10000)
|
||||
i.address = address
|
||||
|
||||
i.listen(address)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *HTTPInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
|
||||
header := payloadHeader(RequestPayload, uuid(), time.Now().UnixNano())
|
||||
|
||||
copy(data[0:len(header)], header)
|
||||
copy(data[len(header):], buf)
|
||||
|
||||
return len(buf) + len(header), nil
|
||||
}
|
||||
|
||||
func (i *HTTPInput) handler(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Scheme = "http"
|
||||
r.URL.Host = i.listener.Addr().String()
|
||||
|
||||
buf, _ := httputil.DumpRequestOut(r, true)
|
||||
http.Error(w, http.StatusText(200), 200)
|
||||
|
||||
select {
|
||||
case i.data <- buf:
|
||||
default:
|
||||
Debug("[INPUT-HTTP] Dropping requests because output can't process them fast enough")
|
||||
}
|
||||
}
|
||||
|
||||
func (i *HTTPInput) listen(address string) {
|
||||
var err error
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/", i.handler)
|
||||
|
||||
i.listener, err = net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Fatal("HTTP input listener failure:", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = http.Serve(i.listener, mux)
|
||||
if err != nil {
|
||||
log.Fatal("HTTP input serve failure:", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *HTTPInput) String() string {
|
||||
return "HTTP input: " + i.address
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/buger/gor/proto"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPInput(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewHTTPInput("127.0.0.1:0")
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
address := strings.Replace(input.listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
http.Get("http://" + address + "/")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestInputHTTPLargePayload(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
dd := exec.Command("dd", "if=/dev/urandom", "of=/tmp/large", "bs=1", "count=4000000")
|
||||
err := dd.Run()
|
||||
if err != nil {
|
||||
log.Fatal("dd error:", err)
|
||||
}
|
||||
|
||||
input := NewHTTPInput("127.0.0.1:0")
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
if len(proto.Body(payloadBody(data))) != 4000000 {
|
||||
t.Error("Should receive full file")
|
||||
}
|
||||
wg.Done()
|
||||
})
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
wg.Add(1)
|
||||
address := strings.Replace(input.listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
curl := exec.Command("curl", "http://"+address, "--data-binary", "@/tmp/large")
|
||||
err = curl.Run()
|
||||
if err != nil {
|
||||
log.Fatal("curl error:", err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/buger/gor/proto"
|
||||
raw "github.com/buger/gor/raw_socket_listener"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RAWInput used for intercepting traffic for given address
|
||||
type RAWInput struct {
|
||||
data chan *raw.TCPMessage
|
||||
address string
|
||||
expire time.Duration
|
||||
quit chan bool
|
||||
engine int
|
||||
realIPHeader []byte
|
||||
trackResponse bool
|
||||
listener *raw.Listener
|
||||
}
|
||||
|
||||
// Available engines for intercepting traffic
|
||||
const (
|
||||
EngineRawSocket = 1 << iota
|
||||
EnginePcap
|
||||
)
|
||||
|
||||
// NewRAWInput constructor for RAWInput. Accepts address with port as argument.
|
||||
func NewRAWInput(address string, engine int, trackResponse bool, expire time.Duration, realIPHeader string) (i *RAWInput) {
|
||||
i = new(RAWInput)
|
||||
i.data = make(chan *raw.TCPMessage)
|
||||
i.address = address
|
||||
i.expire = expire
|
||||
i.engine = engine
|
||||
i.realIPHeader = []byte(realIPHeader)
|
||||
i.quit = make(chan bool)
|
||||
i.trackResponse = trackResponse
|
||||
|
||||
i.listen(address)
|
||||
i.listener.IsReady()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *RAWInput) Read(data []byte) (int, error) {
|
||||
msg := <-i.data
|
||||
buf := msg.Bytes()
|
||||
|
||||
var header []byte
|
||||
|
||||
if msg.IsIncoming {
|
||||
header = payloadHeader(RequestPayload, msg.UUID(), msg.Start.UnixNano())
|
||||
if len(i.realIPHeader) > 0 {
|
||||
buf = proto.SetHeader(buf, i.realIPHeader, []byte(msg.IP().String()))
|
||||
}
|
||||
} else {
|
||||
header = payloadHeader(ResponsePayload, msg.UUID(), msg.End.UnixNano()-msg.AssocMessage.Start.UnixNano())
|
||||
}
|
||||
|
||||
copy(data[0:len(header)], header)
|
||||
copy(data[len(header):], buf)
|
||||
|
||||
return len(buf) + len(header), nil
|
||||
}
|
||||
|
||||
func (i *RAWInput) listen(address string) {
|
||||
Debug("Listening for traffic on: " + address)
|
||||
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("input-raw: error while parsing address", err)
|
||||
}
|
||||
|
||||
i.listener = raw.NewListener(host, port, i.engine, i.trackResponse, i.expire)
|
||||
|
||||
ch := i.listener.Receiver()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-i.quit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Receiving TCPMessage object
|
||||
m := <-ch
|
||||
|
||||
i.data <- m
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *RAWInput) String() string {
|
||||
return "Intercepting traffic from: " + i.address
|
||||
}
|
||||
|
||||
func (i *RAWInput) Close() error {
|
||||
i.listener.Close()
|
||||
close(i.quit)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/buger/gor/proto"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const testRawExpire = time.Millisecond * 200
|
||||
|
||||
func TestRAWInputIPv4(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
origin := &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
go origin.Serve(listener)
|
||||
defer listener.Close()
|
||||
|
||||
originAddr := listener.Addr().String()
|
||||
|
||||
var respCounter, reqCounter int64
|
||||
|
||||
input := NewRAWInput(originAddr, EnginePcap, true, testRawExpire, "X-Real-IP")
|
||||
defer input.Close()
|
||||
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
if data[0] == '1' {
|
||||
body := payloadBody(data)
|
||||
if len(proto.Header(body, []byte("X-Real-IP"))) == 0 {
|
||||
t.Error("Should have X-Real-IP header", string(body))
|
||||
}
|
||||
atomic.AddInt64(&reqCounter, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&respCounter, 1)
|
||||
}
|
||||
|
||||
if Settings.debug {
|
||||
log.Println(reqCounter, respCounter)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
client := NewHTTPClient("http://"+listener.Addr().String(), &HTTPClientConfig{})
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
// request + response
|
||||
wg.Add(2)
|
||||
client.Get("/")
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestRAWInputIPv6(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
listener, err := net.Listen("tcp", "[::1]:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
origin := &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
go origin.Serve(listener)
|
||||
defer listener.Close()
|
||||
|
||||
originAddr := listener.Addr().String()
|
||||
|
||||
var respCounter, reqCounter int64
|
||||
|
||||
input := NewRAWInput(originAddr, EnginePcap, true, testRawExpire, "")
|
||||
defer input.Close()
|
||||
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
if data[0] == '1' {
|
||||
atomic.AddInt64(&reqCounter, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&respCounter, 1)
|
||||
}
|
||||
|
||||
if Settings.debug {
|
||||
log.Println(reqCounter, respCounter)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
client := NewHTTPClient("http://"+listener.Addr().String(), &HTTPClientConfig{})
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
// request + response
|
||||
wg.Add(2)
|
||||
client.Get("/")
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestInputRAW100Expect(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
fileContent, _ := ioutil.ReadFile("COMM-LICENSE")
|
||||
|
||||
// Origing and Replay server initialization
|
||||
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
ioutil.ReadAll(r.Body)
|
||||
wg.Done()
|
||||
}))
|
||||
defer origin.Close()
|
||||
|
||||
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
|
||||
input := NewRAWInput(originAddr, EnginePcap, true, time.Second, "")
|
||||
defer input.Close()
|
||||
|
||||
// We will use it to get content of raw HTTP request
|
||||
testOutput := NewTestOutput(func(data []byte) {
|
||||
switch data[0] {
|
||||
case RequestPayload:
|
||||
if strings.Contains(string(data), "Expect: 100-continue") {
|
||||
t.Error("Should not contain 100-continue header")
|
||||
}
|
||||
wg.Done()
|
||||
case ResponsePayload:
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
replay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
|
||||
if !bytes.Equal(body, fileContent) {
|
||||
buf, _ := httputil.DumpRequest(r, true)
|
||||
t.Error("Wrong POST body:", string(buf))
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer replay.Close()
|
||||
|
||||
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{testOutput, httpOutput}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
// Origin + Response/Request Test Output + Request Http Output
|
||||
wg.Add(4)
|
||||
curl := exec.Command("curl", "http://"+originAddr, "--data-binary", "@COMM-LICENSE")
|
||||
err := curl.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestInputRAWChunkedEncoding(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
fileContent, _ := ioutil.ReadFile("README.md")
|
||||
|
||||
// Origing and Replay server initialization
|
||||
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
ioutil.ReadAll(r.Body)
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
|
||||
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
input := NewRAWInput(originAddr, EnginePcap, true, time.Second, "")
|
||||
defer input.Close()
|
||||
|
||||
replay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
|
||||
if !bytes.Equal(body, fileContent) {
|
||||
buf, _ := httputil.DumpRequest(r, true)
|
||||
t.Error("Wrong POST body:", string(buf))
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer replay.Close()
|
||||
|
||||
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{Debug: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{httpOutput}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
curl := exec.Command("curl", "http://"+originAddr, "--header", "Transfer-Encoding: chunked", "--header", "Expect:", "--data-binary", "@README.md")
|
||||
err := curl.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestInputRAWLargePayload(t *testing.T) {
|
||||
// FIXME: Large payloads does not work for travis for some reason...
|
||||
if os.Getenv("TRAVIS_BUILD_DIR") != "" {
|
||||
return
|
||||
}
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
sizeB := 100 * 1000
|
||||
|
||||
// Generate 100kb file
|
||||
dd := exec.Command("dd", "if=/dev/urandom", "of=/tmp/large", "bs=1", "count="+strconv.Itoa(sizeB))
|
||||
err := dd.Run()
|
||||
if err != nil {
|
||||
log.Fatal("dd error:", err)
|
||||
}
|
||||
|
||||
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
defer req.Body.Close()
|
||||
body, _ := ioutil.ReadAll(req.Body)
|
||||
|
||||
if len(body) != sizeB {
|
||||
t.Error("File size should be 1mb:", len(body))
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
|
||||
input := NewRAWInput(originAddr, EnginePcap, true, testRawExpire, "")
|
||||
defer input.Close()
|
||||
|
||||
replay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
body, _ := ioutil.ReadAll(req.Body)
|
||||
// // req.Body = http.MaxBytesReader(w, req.Body, 1*1024*1024)
|
||||
// // buf := make([]byte, 1*1024*1024)
|
||||
// n, _ := req.Body.Read(buf)
|
||||
// body := buf[0:n]
|
||||
|
||||
if len(body) != sizeB {
|
||||
t.Errorf("File size should be %d bytes: %d", sizeB, len(body))
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer replay.Close()
|
||||
|
||||
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{Debug: false})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{httpOutput}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
wg.Add(2)
|
||||
curl := exec.Command("curl", "http://"+originAddr, "--header", "Transfer-Encoding: chunked", "--header", "Expect:", "--data-binary", "@/tmp/large")
|
||||
err = curl.Run()
|
||||
if err != nil {
|
||||
log.Fatal("curl error:", err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func BenchmarkRAWInput(b *testing.B) {
|
||||
quit := make(chan int)
|
||||
|
||||
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
defer origin.Close()
|
||||
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
|
||||
var respCounter, reqCounter int64
|
||||
|
||||
input := NewRAWInput(originAddr, EnginePcap, true, testRawExpire, "")
|
||||
defer input.Close()
|
||||
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
if data[0] == '1' {
|
||||
atomic.AddInt64(&reqCounter, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&respCounter, 1)
|
||||
}
|
||||
|
||||
// log.Println("Captured ", reqCounter, "requests and ", respCounter, " responses")
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
emitted := 0
|
||||
fileContent, _ := ioutil.ReadFile("LICENSE.txt")
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(10 * 100)
|
||||
emitted += 10 * 100
|
||||
for w := 0; w < 100; w++ {
|
||||
go func() {
|
||||
client := NewHTTPClient(origin.URL, &HTTPClientConfig{})
|
||||
for i := 0; i < 10; i++ {
|
||||
if rand.Int63n(2) == 0 {
|
||||
client.Post("/", fileContent)
|
||||
} else {
|
||||
client.Get("/")
|
||||
}
|
||||
time.Sleep(time.Duration(rand.Int63n(50)) * time.Millisecond)
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
log.Println("Emitted ", emitted, ", Captured ", reqCounter, "requests and ", respCounter, " responses")
|
||||
|
||||
close(quit)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
// TCPInput used for internal communication
|
||||
type TCPInput struct {
|
||||
data chan []byte
|
||||
address string
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewTCPInput constructor for TCPInput, accepts address with port
|
||||
func NewTCPInput(address string) (i *TCPInput) {
|
||||
i = new(TCPInput)
|
||||
i.data = make(chan []byte, 1000)
|
||||
i.address = address
|
||||
|
||||
i.listen(address)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *TCPInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
copy(data, buf)
|
||||
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (i *TCPInput) listen(address string) {
|
||||
listener, err := net.Listen("tcp", address)
|
||||
i.listener = listener
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Can't start:", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
|
||||
if err != nil {
|
||||
log.Println("Error while Accept()", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go i.handleConnection(conn)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *TCPInput) handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
payloadSeparatorAsBytes := []byte(payloadSeparator)
|
||||
reader := bufio.NewReader(conn)
|
||||
var buffer bytes.Buffer
|
||||
|
||||
for {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
fmt.Fprintln(os.Stderr, "Unexpected error in input tcp connection:", err)
|
||||
}
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
if bytes.Equal(payloadSeparatorAsBytes[1:], line) {
|
||||
asBytes := buffer.Bytes()
|
||||
buffer.Reset()
|
||||
|
||||
newBuf := make([]byte, len(asBytes)-1)
|
||||
copy(newBuf, asBytes)
|
||||
|
||||
i.data <- newBuf
|
||||
} else {
|
||||
buffer.Write(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *TCPInput) String() string {
|
||||
return "TCP input: " + i.address
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTCPInput(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTCPInput("127.0.0.1:0")
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", input.listener.Addr().String())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
conn, err := net.DialTCP("tcp", nil, tcpAddr)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
msg := []byte("1 1 1\nGET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
conn.Write(msg)
|
||||
conn.Write([]byte(payloadSeparator))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Limiter is a wrapper for input or output plugin which adds rate limiting
|
||||
type Limiter struct {
|
||||
plugin interface{}
|
||||
limit int
|
||||
isPercent bool
|
||||
|
||||
currentRPS int
|
||||
currentTime int64
|
||||
}
|
||||
|
||||
func parseLimitOptions(options string) (limit int, isPercent bool) {
|
||||
if strings.Contains(options, "%") {
|
||||
limit, _ = strconv.Atoi(strings.Split(options, "%")[0])
|
||||
isPercent = true
|
||||
} else {
|
||||
limit, _ = strconv.Atoi(options)
|
||||
isPercent = false
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewLimiter constructor for Limiter, accepts plugin and options
|
||||
// `options` allow to sprcify relatve or absolute limiting
|
||||
func NewLimiter(plugin interface{}, options string) io.ReadWriter {
|
||||
l := new(Limiter)
|
||||
l.limit, l.isPercent = parseLimitOptions(options)
|
||||
l.plugin = plugin
|
||||
l.currentTime = time.Now().UnixNano()
|
||||
|
||||
// FileInput have its own rate limiting. Unlike other inputs we not just dropping requests, we can slow down or speed up request emittion.
|
||||
if fi, ok := l.plugin.(*FileInput); ok && l.isPercent {
|
||||
fi.speedFactor = float64(l.limit) / float64(100)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Limiter) isLimited() bool {
|
||||
// File input have its own limiting algorithm
|
||||
if _, ok := l.plugin.(*FileInput); ok && l.isPercent {
|
||||
return false
|
||||
}
|
||||
|
||||
if l.isPercent {
|
||||
return l.limit <= rand.Intn(100)
|
||||
}
|
||||
|
||||
if (time.Now().UnixNano() - l.currentTime) > time.Second.Nanoseconds() {
|
||||
l.currentTime = time.Now().UnixNano()
|
||||
l.currentRPS = 0
|
||||
}
|
||||
|
||||
if l.currentRPS >= l.limit {
|
||||
return true
|
||||
}
|
||||
|
||||
l.currentRPS++
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *Limiter) Write(data []byte) (n int, err error) {
|
||||
if l.isLimited() {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
n, err = l.plugin.(io.Writer).Write(data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Limiter) Read(data []byte) (n int, err error) {
|
||||
n, err = l.plugin.(io.Reader).Read(data)
|
||||
|
||||
if l.isLimited() {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Limiter) String() string {
|
||||
return fmt.Sprintf("Limiting %s to: %d (isPercent: %b)", l.plugin, l.limit, l.isPercent)
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// +build !race
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOutputLimiter(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewLimiter(NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
}), "10")
|
||||
wg.Add(10)
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestInputLimiter(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewLimiter(NewTestInput(), "10")
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
wg.Add(10)
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
input.(*Limiter).plugin.(*TestInput).EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
// Should limit all requests
|
||||
func TestPercentLimiter1(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewLimiter(NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
}), "0%")
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
// Should not limit at all
|
||||
func TestPercentLimiter2(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewLimiter(NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
}), "100%")
|
||||
wg.Add(100)
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
// Listener capture TCP traffic using RAW SOCKETS.
|
||||
// Note: it requires sudo or root access.
|
||||
//
|
||||
// Rigt now it suport only HTTP, and only GET requests.
|
||||
package listener
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type HttpRequest struct {
|
||||
Tag string // Not used yet
|
||||
Method string // Right now only 'GET'
|
||||
Url string // Request URL
|
||||
Headers map[string]string // Request Headers
|
||||
}
|
||||
|
||||
// Enable debug logging only if "--verbose" flag passed
|
||||
func Debug(v ...interface{}) {
|
||||
if Settings.verbose {
|
||||
log.Println(v...)
|
||||
}
|
||||
}
|
||||
|
||||
func greeting() {
|
||||
|
||||
}
|
||||
|
||||
// Because its sub-program, Run acts as `main`
|
||||
func Run() {
|
||||
if os.Getuid() != 0 {
|
||||
fmt.Println("Please start the listener as root or sudo!")
|
||||
fmt.Println("This is required since listener sniff traffic on given port.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Listening for HTTP traffic on", Settings.port, "port")
|
||||
fmt.Println("Forwarding requests to replay server:", Settings.ReplayServer())
|
||||
|
||||
// Connection to reaplay server
|
||||
serverAddr, err := net.ResolveUDPAddr("udp4", Settings.ReplayServer())
|
||||
conn, err := net.DialUDP("udp", nil, serverAddr)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Connection error", err)
|
||||
}
|
||||
|
||||
// Sniffing traffic from given port
|
||||
listener := RAWTCPListen("0.0.0.0", Settings.port)
|
||||
|
||||
for {
|
||||
message := listener.Receive()
|
||||
|
||||
go func(m *TCPMessage) {
|
||||
if Settings.verbose {
|
||||
buf := bytes.NewBuffer(m.Bytes())
|
||||
reader := bufio.NewReader(buf)
|
||||
|
||||
request, err := http.ReadRequest(reader)
|
||||
|
||||
if err != nil {
|
||||
Debug("Error while parsing request:", string(m.Bytes()))
|
||||
} else {
|
||||
request.ParseMultipartForm(32 << 20)
|
||||
Debug("Forwarding request:", request)
|
||||
}
|
||||
}
|
||||
|
||||
conn.Write(m.Bytes())
|
||||
}(message)
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RAWTCPListener struct {
|
||||
messages map[uint32]*TCPMessage // buffer of TCPMessages waiting to be send
|
||||
|
||||
c_packets chan *TCPPacket
|
||||
c_messages chan *TCPMessage
|
||||
|
||||
addr string
|
||||
port int
|
||||
}
|
||||
|
||||
func RAWTCPListen(addr string, port int) (listener *RAWTCPListener) {
|
||||
listener = &RAWTCPListener{}
|
||||
listener.messages = make(map[uint32]*TCPMessage)
|
||||
|
||||
listener.c_packets = make(chan *TCPPacket)
|
||||
listener.c_messages = make(chan *TCPMessage)
|
||||
|
||||
listener.addr = addr
|
||||
listener.port = port
|
||||
|
||||
go listener.listen()
|
||||
go listener.readTCPPackets()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *RAWTCPListener) listen() {
|
||||
|
||||
for {
|
||||
var messages chan *TCPMessage
|
||||
var message *TCPMessage
|
||||
|
||||
for _, msg := range t.messages {
|
||||
if msg.Complete() {
|
||||
messages = t.c_messages
|
||||
message = msg
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case messages <- message:
|
||||
delete(t.messages, message.ask)
|
||||
case packet := <-t.c_packets:
|
||||
t.processTCPPacket(packet)
|
||||
|
||||
// Ensure that this will be run at least each 200 ms, to ensure that all messages will be send
|
||||
// Without it last message may not be send (it will be send only on next TCP packets)
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *RAWTCPListener) readTCPPackets() {
|
||||
conn, e := net.ListenPacket("ip4:tcp", t.addr)
|
||||
defer conn.Close()
|
||||
|
||||
if e != nil {
|
||||
log.Fatal(e)
|
||||
}
|
||||
|
||||
for {
|
||||
buf := make([]byte, 65565)
|
||||
n, _, err := conn.ReadFrom(buf)
|
||||
|
||||
if err != nil {
|
||||
Debug("Error:", err)
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
packet := NewTCPPacket(buf[:n])
|
||||
|
||||
if int(packet.dest_port) == t.port {
|
||||
t.c_packets <- packet
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
func (t *RAWTCPListener) processTCPPacket(packet *TCPPacket) {
|
||||
// We interested only in packets that contain some data
|
||||
if !(packet.f_ask && packet.f_psh) {
|
||||
return
|
||||
}
|
||||
|
||||
ask := packet.asknowledgement
|
||||
|
||||
if _, ok := t.messages[ask]; !ok {
|
||||
t.messages[ask] = NewTCPMessage(ask)
|
||||
}
|
||||
|
||||
t.messages[ask].AddPacket(packet)
|
||||
}
|
||||
|
||||
func (t *RAWTCPListener) Receive() *TCPMessage {
|
||||
return <-t.c_messages
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPort = 80
|
||||
defaultNetworkInterface = "any"
|
||||
|
||||
defaultReplayAddress = "localhost:28020"
|
||||
)
|
||||
|
||||
type ListenerSettings struct {
|
||||
networkInterface string
|
||||
port int
|
||||
|
||||
replayAddress string
|
||||
|
||||
verbose bool
|
||||
}
|
||||
|
||||
var Settings ListenerSettings = ListenerSettings{}
|
||||
|
||||
func (s *ListenerSettings) ReplayServer() string {
|
||||
if !strings.Contains(s.replayAddress, ":") {
|
||||
return s.replayAddress + ":28020"
|
||||
}
|
||||
|
||||
return s.replayAddress
|
||||
}
|
||||
|
||||
func init() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "listen" {
|
||||
return
|
||||
}
|
||||
|
||||
flag.IntVar(&Settings.port, "p", defaultPort, "Specify the http server port whose traffic you want to capture")
|
||||
|
||||
flag.StringVar(&Settings.networkInterface, "i", defaultNetworkInterface, "By default it try to listen on all network interfaces.To get list of interfaces run `ifconfig`")
|
||||
|
||||
flag.StringVar(&Settings.replayAddress, "r", defaultReplayAddress, "Address of replay server.")
|
||||
|
||||
flag.BoolVar(&Settings.verbose, "verbose", false, "Log requests")
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TCPMessage ensure that all TCP packets for given request is received, and processed in right sequence
|
||||
// Its needed because all TCP message can be fragmented or re-transmitted
|
||||
//
|
||||
// Each TCP Packet have 2 ids: asknowledgement - message_id, and sequence - packet_id
|
||||
// Message can be compiled from unique packets with same message_id which sorted by sequence
|
||||
// Message is received if we did't receive any packets for 200ms OR if we received packet with "fin" flag
|
||||
type TCPMessage struct {
|
||||
ask uint32 // Message ID
|
||||
packets map[int]*TCPPacket // map[packet.sequence]*TCPPacket
|
||||
updated int64 // time of last packet
|
||||
}
|
||||
|
||||
func NewTCPMessage(ask uint32) (msg *TCPMessage) {
|
||||
msg = &TCPMessage{}
|
||||
msg.packets = make(map[int]*TCPPacket)
|
||||
msg.updated = time.Now().UnixNano()
|
||||
msg.ask = ask
|
||||
return
|
||||
}
|
||||
|
||||
// Sort packets in right orders and return message content
|
||||
func (t *TCPMessage) Bytes() (output []byte) {
|
||||
mk := make([]int, len(t.packets))
|
||||
|
||||
i := 0
|
||||
for k, _ := range t.packets {
|
||||
mk[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
sort.Ints(mk)
|
||||
|
||||
for _, k := range mk {
|
||||
output = append(output, t.packets[k].data...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Add packet to the message
|
||||
func (t *TCPMessage) AddPacket(packet *TCPPacket) {
|
||||
seq := int(packet.sequence)
|
||||
|
||||
if _, ok := t.packets[seq]; !ok {
|
||||
t.packets[seq] = packet
|
||||
} else {
|
||||
Debug("Received packet with same sequence")
|
||||
}
|
||||
|
||||
t.updated = time.Now().UnixNano()
|
||||
}
|
||||
|
||||
// TCP message is complete if we not received any packets for 200ms since last packet
|
||||
func (t *TCPMessage) Complete() bool {
|
||||
ns := time.Now().UnixNano()
|
||||
return (ns - t.updated) > int64(200*time.Millisecond)
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Simple TCP packet parser
|
||||
//
|
||||
// Packet structure: http://en.wikipedia.org/wiki/Transmission_Control_Protocol
|
||||
type TCPPacket struct {
|
||||
packet []byte
|
||||
|
||||
buf *bytes.Buffer
|
||||
|
||||
source_port uint16
|
||||
dest_port uint16
|
||||
|
||||
sequence uint32
|
||||
asknowledgement uint32
|
||||
|
||||
doff_reserved uint16
|
||||
tcph_length uint16
|
||||
|
||||
f_ns bool
|
||||
f_crw bool
|
||||
f_ece bool
|
||||
f_urg bool
|
||||
f_ask bool
|
||||
f_psh bool
|
||||
f_rst bool
|
||||
f_syn bool
|
||||
f_fin bool
|
||||
|
||||
window_size uint16
|
||||
checksum uint16
|
||||
|
||||
data []byte
|
||||
}
|
||||
|
||||
func NewTCPPacket(b []byte) (p *TCPPacket) {
|
||||
buf := bytes.NewBuffer(b)
|
||||
p = &TCPPacket{packet: b, buf: buf}
|
||||
p.Parse()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Helper for binary.Read
|
||||
func (t *TCPPacket) read(data interface{}) {
|
||||
binary.Read(t.buf, binary.BigEndian, data)
|
||||
}
|
||||
|
||||
// Inspired by: https://gist.github.com/clicube/4978853
|
||||
func (t *TCPPacket) Parse() {
|
||||
t.read(&t.source_port)
|
||||
t.read(&t.dest_port)
|
||||
|
||||
t.read(&t.sequence)
|
||||
t.read(&t.asknowledgement)
|
||||
t.read(&t.doff_reserved)
|
||||
|
||||
t.tcph_length = t.doff_reserved >> 12 * 4
|
||||
|
||||
t.f_ns = (t.doff_reserved & 256) != 0
|
||||
t.f_crw = (t.doff_reserved & 128) != 0
|
||||
t.f_ece = (t.doff_reserved & 64) != 0
|
||||
t.f_urg = (t.doff_reserved & 32) != 0
|
||||
t.f_ask = (t.doff_reserved & 16) != 0
|
||||
t.f_psh = (t.doff_reserved & 8) != 0
|
||||
t.f_rst = (t.doff_reserved & 4) != 0
|
||||
t.f_syn = (t.doff_reserved & 2) != 0
|
||||
t.f_fin = (t.doff_reserved & 1) != 0
|
||||
|
||||
t.read(&t.window_size)
|
||||
t.read(&t.checksum)
|
||||
|
||||
t.data = t.packet[t.tcph_length:]
|
||||
}
|
||||
|
||||
func (t *TCPPacket) String() string {
|
||||
return strings.Join([]string{
|
||||
"Source port: " + strconv.Itoa(int(t.source_port)),
|
||||
"Dest port:" + strconv.Itoa(int(t.dest_port)),
|
||||
"Sequence:" + strconv.Itoa(int(t.sequence)),
|
||||
"Acknowledgement:" + strconv.Itoa(int(t.asknowledgement)),
|
||||
"Header len:" + strconv.Itoa(int(t.tcph_length)),
|
||||
|
||||
"Flag ns:" + strconv.FormatBool(t.f_ns),
|
||||
"Flag crw:" + strconv.FormatBool(t.f_crw),
|
||||
"Flag ece:" + strconv.FormatBool(t.f_ece),
|
||||
"Flag urg:" + strconv.FormatBool(t.f_urg),
|
||||
"Flag ask:" + strconv.FormatBool(t.f_ask),
|
||||
"Flag psh:" + strconv.FormatBool(t.f_psh),
|
||||
"Flag rst:" + strconv.FormatBool(t.f_rst),
|
||||
"Flag syn:" + strconv.FormatBool(t.f_syn),
|
||||
"Flag fin:" + strconv.FormatBool(t.f_fin),
|
||||
|
||||
"Window size:" + strconv.Itoa(int(t.window_size)),
|
||||
"Checksum:" + strconv.Itoa(int(t.checksum)),
|
||||
|
||||
"Data:" + string(t.data),
|
||||
}, "\n")
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Middleware struct {
|
||||
command string
|
||||
|
||||
data chan []byte
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
Stdin io.Writer
|
||||
Stdout io.Reader
|
||||
}
|
||||
|
||||
func NewMiddleware(command string) *Middleware {
|
||||
m := new(Middleware)
|
||||
m.command = command
|
||||
m.data = make(chan []byte, 1000)
|
||||
|
||||
commands := strings.Split(command, " ")
|
||||
cmd := exec.Command(commands[0], commands[1:]...)
|
||||
|
||||
m.Stdout, _ = cmd.StdoutPipe()
|
||||
m.Stdin, _ = cmd.StdinPipe()
|
||||
|
||||
if Settings.verbose {
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
go m.read(m.Stdout)
|
||||
|
||||
go func() {
|
||||
err := cmd.Start()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Middleware) ReadFrom(plugin io.Reader) {
|
||||
Debug("[MIDDLEWARE-MASTER] Starting reading from", plugin)
|
||||
go m.copy(m.Stdin, plugin)
|
||||
}
|
||||
|
||||
func (m *Middleware) copy(to io.Writer, from io.Reader) {
|
||||
buf := make([]byte, 5*1024*1024)
|
||||
dst := make([]byte, len(buf)*2)
|
||||
|
||||
for {
|
||||
nr, _ := from.Read(buf)
|
||||
if nr > 0 && len(buf) > nr {
|
||||
|
||||
hex.Encode(dst, buf[0:nr])
|
||||
dst[nr*2] = '\n'
|
||||
|
||||
m.mu.Lock()
|
||||
to.Write(dst[0 : nr*2+1])
|
||||
m.mu.Unlock()
|
||||
|
||||
if Settings.debug {
|
||||
Debug("[MIDDLEWARE-MASTER] Sending:", string(buf[0:nr]), "From:", from)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Middleware) read(from io.Reader) {
|
||||
reader := bufio.NewReader(from)
|
||||
var line []byte
|
||||
var e error
|
||||
|
||||
for {
|
||||
if line, e = reader.ReadBytes('\n'); e != nil {
|
||||
if e == io.EOF {
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
buf := make([]byte, len(line)/2)
|
||||
if _, err := hex.Decode(buf, line[:len(line)-1]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Failed to decode input payload", err, len(line))
|
||||
}
|
||||
|
||||
if Settings.debug {
|
||||
Debug("[MIDDLEWARE-MASTER] Received:", string(buf))
|
||||
}
|
||||
|
||||
m.data <- buf
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Middleware) Read(data []byte) (int, error) {
|
||||
buf := <-m.data
|
||||
copy(data, buf)
|
||||
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (m *Middleware) String() string {
|
||||
return fmt.Sprintf("Modifying traffic using '%s' command", m.command)
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"github.com/buger/gor/proto"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeServiceCb func(string, int, []byte)
|
||||
|
||||
// Simple service that generate token on request, and require this token for accesing to secure area
|
||||
func NewFakeSecureService(wg *sync.WaitGroup, cb fakeServiceCb) *httptest.Server {
|
||||
active_tokens := make([]string, 0)
|
||||
var mu sync.Mutex
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
Debug("Received request: " + req.URL.String())
|
||||
|
||||
switch req.URL.Path {
|
||||
case "/token":
|
||||
// Generate random token
|
||||
token_length := 10
|
||||
buf := make([]byte, token_length)
|
||||
rand.Read(buf)
|
||||
token := hex.EncodeToString(buf)
|
||||
active_tokens = append(active_tokens, token)
|
||||
|
||||
w.Write([]byte(token))
|
||||
|
||||
cb(req.URL.Path, 200, []byte(token))
|
||||
case "/secure":
|
||||
token := req.URL.Query().Get("token")
|
||||
token_found := false
|
||||
|
||||
for _, t := range active_tokens {
|
||||
if t == token {
|
||||
token_found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if token_found {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
cb(req.URL.Path, 202, []byte(nil))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
cb(req.URL.Path, 403, []byte(nil))
|
||||
}
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func TestFakeSecureService(t *testing.T) {
|
||||
var resp, token []byte
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
server := NewFakeSecureService(wg, func(path string, status int, resp []byte) {
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
wg.Add(3)
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true})
|
||||
resp, _ = client.Get("/token")
|
||||
token = proto.Body(resp)
|
||||
|
||||
// Right token
|
||||
resp, _ = client.Get("/secure?token=" + string(token))
|
||||
if !bytes.Equal(proto.Status(resp), []byte("202")) {
|
||||
t.Error("Valid token should return status 202:", string(proto.Status(resp)))
|
||||
}
|
||||
|
||||
// Wrong tokens forbidden
|
||||
resp, _ = client.Get("/secure?token=wrong")
|
||||
if !bytes.Equal(proto.Status(resp), []byte("403")) {
|
||||
t.Error("Wrong token should returns status 403:", string(proto.Status(resp)))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestEchoMiddleware(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
from := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Env", "prod")
|
||||
w.Header().Set("RequestPath", r.URL.Path)
|
||||
wg.Done()
|
||||
}))
|
||||
defer from.Close()
|
||||
|
||||
to := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Env", "test")
|
||||
w.Header().Set("RequestPath", r.URL.Path)
|
||||
wg.Done()
|
||||
}))
|
||||
defer to.Close()
|
||||
|
||||
quit := make(chan int)
|
||||
|
||||
Settings.middleware = "./examples/middleware/echo.sh"
|
||||
|
||||
// Catch traffic from one service
|
||||
fromAddr := strings.Replace(from.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
input := NewRAWInput(fromAddr, EnginePcap, true, testRawExpire, "")
|
||||
defer input.Close()
|
||||
|
||||
// And redirect to another
|
||||
output := NewHTTPOutput(to.URL, &HTTPOutputConfig{Debug: false})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
// Start Gor
|
||||
go Start(quit)
|
||||
|
||||
// Wait till middleware initialization
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Should receive 2 requests from original + 2 from replayed
|
||||
client := NewHTTPClient(from.URL, &HTTPClientConfig{Debug: false})
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(4)
|
||||
// Request should be echoed
|
||||
client.Get("/a")
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
client.Get("/b")
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
Settings.middleware = ""
|
||||
}
|
||||
|
||||
func TestTokenMiddleware(t *testing.T) {
|
||||
var resp, token []byte
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
from := NewFakeSecureService(wg, func(path string, status int, tok []byte) {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
})
|
||||
defer from.Close()
|
||||
|
||||
to := NewFakeSecureService(wg, func(path string, status int, tok []byte) {
|
||||
switch path {
|
||||
case "/secure":
|
||||
if status != 202 {
|
||||
t.Error("Server should receive valid rewritten token")
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
})
|
||||
defer to.Close()
|
||||
|
||||
quit := make(chan int)
|
||||
|
||||
Settings.middleware = "go run ./examples/middleware/token_modifier.go"
|
||||
|
||||
fromAddr := strings.Replace(from.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
// Catch traffic from one service
|
||||
input := NewRAWInput(fromAddr, EnginePcap, true, testRawExpire, "")
|
||||
defer input.Close()
|
||||
|
||||
// And redirect to another
|
||||
output := NewHTTPOutput(to.URL, &HTTPOutputConfig{Debug: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
// Start Gor
|
||||
go Start(quit)
|
||||
|
||||
// Wait for middleware to initialize
|
||||
// Give go compiller time to build programm
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Should receive 2 requests from original + 2 from replayed
|
||||
wg.Add(4)
|
||||
|
||||
client := NewHTTPClient(from.URL, &HTTPClientConfig{Debug: true})
|
||||
|
||||
// Sending traffic to original service
|
||||
resp, _ = client.Get("/token")
|
||||
token = proto.Body(resp)
|
||||
|
||||
// When delay is too smal, middleware does not always rewrite requests in time
|
||||
// Hopefuly client will have delay more then 100ms :)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
resp, _ = client.Get("/secure?token=" + string(token))
|
||||
if !bytes.Equal(proto.Status(resp), []byte("202")) {
|
||||
t.Error("Valid token should return 202:", proto.Status(resp))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
Settings.middleware = ""
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// DummyOutput used for debugging, prints all incoming requests
|
||||
type DummyOutput struct {
|
||||
}
|
||||
|
||||
// NewDummyOutput constructor for DummyOutput
|
||||
func NewDummyOutput() (di *DummyOutput) {
|
||||
di = new(DummyOutput)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *DummyOutput) Write(data []byte) (int, error) {
|
||||
fmt.Println(string(data))
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (i *DummyOutput) String() string {
|
||||
return "Dummy Output"
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var dateFileNameFuncs = map[string]func() string{
|
||||
"%Y": func() string { return time.Now().Format("2006") },
|
||||
"%m": func() string { return time.Now().Format("01") },
|
||||
"%d": func() string { return time.Now().Format("02") },
|
||||
"%H": func() string { return time.Now().Format("15") },
|
||||
"%M": func() string { return time.Now().Format("04") },
|
||||
"%S": func() string { return time.Now().Format("05") },
|
||||
"%NS": func() string { return fmt.Sprint(time.Now().Nanosecond()) },
|
||||
}
|
||||
|
||||
type FileOutputConfig struct {
|
||||
flushInterval time.Duration
|
||||
sizeLimit unitSizeVar
|
||||
queueLimit int
|
||||
append bool
|
||||
}
|
||||
|
||||
// FileOutput output plugin
|
||||
type FileOutput struct {
|
||||
mu sync.Mutex
|
||||
pathTemplate string
|
||||
currentName string
|
||||
file *os.File
|
||||
queueLength int
|
||||
chunkSize int
|
||||
writer io.Writer
|
||||
|
||||
config *FileOutputConfig
|
||||
}
|
||||
|
||||
// NewFileOutput constructor for FileOutput, accepts path
|
||||
func NewFileOutput(pathTemplate string, config *FileOutputConfig) *FileOutput {
|
||||
o := new(FileOutput)
|
||||
o.pathTemplate = pathTemplate
|
||||
o.config = config
|
||||
o.updateName()
|
||||
|
||||
// Force flushing every minute
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(o.config.flushInterval)
|
||||
o.flush()
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
o.updateName()
|
||||
}
|
||||
}()
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func getFileIndex(name string) int {
|
||||
ext := filepath.Ext(name)
|
||||
withoutExt := strings.TrimSuffix(name, ext)
|
||||
|
||||
if idx := strings.LastIndex(withoutExt, "_"); idx != -1 {
|
||||
if i, err := strconv.Atoi(withoutExt[idx+1:]); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func setFileIndex(name string, idx int) string {
|
||||
idxS := strconv.Itoa(idx)
|
||||
ext := filepath.Ext(name)
|
||||
withoutExt := strings.TrimSuffix(name, ext)
|
||||
|
||||
if i := strings.LastIndex(withoutExt, "_"); i != -1 {
|
||||
if _, err := strconv.Atoi(withoutExt[i+1:]); err == nil {
|
||||
withoutExt = withoutExt[:i]
|
||||
}
|
||||
}
|
||||
|
||||
return withoutExt + "_" + idxS + ext
|
||||
}
|
||||
|
||||
func withoutIndex(s string) string {
|
||||
if i := strings.LastIndex(s, "_"); i != -1 {
|
||||
return s[:i]
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
type sortByFileIndex []string
|
||||
|
||||
func (s sortByFileIndex) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s sortByFileIndex) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
func (s sortByFileIndex) Less(i, j int) bool {
|
||||
if withoutIndex(s[i]) == withoutIndex(s[j]) {
|
||||
return getFileIndex(s[i]) < getFileIndex(s[j])
|
||||
}
|
||||
|
||||
return s[i] < s[j]
|
||||
}
|
||||
|
||||
func (o *FileOutput) filename() string {
|
||||
defer o.mu.Unlock()
|
||||
o.mu.Lock()
|
||||
|
||||
path := o.pathTemplate
|
||||
|
||||
for name, fn := range dateFileNameFuncs {
|
||||
path = strings.Replace(path, name, fn(), -1)
|
||||
}
|
||||
|
||||
if !o.config.append {
|
||||
nextChunk := false
|
||||
|
||||
if o.currentName == "" ||
|
||||
((o.config.queueLimit > 0 && o.queueLength >= o.config.queueLimit) ||
|
||||
(o.config.sizeLimit > 0 && o.chunkSize >= int(o.config.sizeLimit))) {
|
||||
nextChunk = true
|
||||
}
|
||||
|
||||
ext := filepath.Ext(path)
|
||||
withoutExt := strings.TrimSuffix(path, ext)
|
||||
|
||||
if matches, err := filepath.Glob(withoutExt + "*" + ext); err == nil {
|
||||
if len(matches) == 0 {
|
||||
return setFileIndex(path, 0)
|
||||
}
|
||||
sort.Sort(sortByFileIndex(matches))
|
||||
|
||||
last := matches[len(matches)-1]
|
||||
|
||||
fileIndex := 0
|
||||
if idx := getFileIndex(last); idx != -1 {
|
||||
fileIndex = idx
|
||||
|
||||
if nextChunk {
|
||||
fileIndex++
|
||||
}
|
||||
}
|
||||
|
||||
return setFileIndex(last, fileIndex)
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func (o *FileOutput) updateName() {
|
||||
o.currentName = filepath.Clean(o.filename())
|
||||
}
|
||||
|
||||
func (o *FileOutput) Write(data []byte) (n int, err error) {
|
||||
if !isOriginPayload(data) {
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
if o.file == nil || o.currentName != o.file.Name() {
|
||||
o.mu.Lock()
|
||||
o.Close()
|
||||
|
||||
o.file, err = os.OpenFile(o.currentName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
|
||||
o.file.Sync()
|
||||
|
||||
if strings.HasSuffix(o.currentName, ".gz") {
|
||||
o.writer = gzip.NewWriter(o.file)
|
||||
} else {
|
||||
o.writer = bufio.NewWriter(o.file)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(o, "Cannot open file %q. Error: %s", o.currentName, err)
|
||||
}
|
||||
|
||||
o.queueLength = 0
|
||||
o.mu.Unlock()
|
||||
}
|
||||
|
||||
o.writer.Write(data)
|
||||
o.writer.Write([]byte(payloadSeparator))
|
||||
|
||||
o.queueLength++
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (o *FileOutput) flush() {
|
||||
defer o.mu.Unlock()
|
||||
o.mu.Lock()
|
||||
|
||||
if o.file != nil {
|
||||
if strings.HasSuffix(o.currentName, ".gz") {
|
||||
o.writer.(*gzip.Writer).Flush()
|
||||
} else {
|
||||
o.writer.(*bufio.Writer).Flush()
|
||||
}
|
||||
|
||||
if stat, err := o.file.Stat(); err != nil {
|
||||
o.chunkSize = int(stat.Size())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *FileOutput) String() string {
|
||||
return "File output: " + o.file.Name()
|
||||
}
|
||||
|
||||
func (o *FileOutput) Close() error {
|
||||
if o.file != nil {
|
||||
if strings.HasSuffix(o.currentName, ".gz") {
|
||||
o.writer.(*gzip.Writer).Close()
|
||||
} else {
|
||||
o.writer.(*bufio.Writer).Flush()
|
||||
}
|
||||
o.file.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var dataUnitMap = map[byte]int64{
|
||||
'k': 1024,
|
||||
'm': 1024 * 1024,
|
||||
'g': 1024 * 1024 * 1024,
|
||||
}
|
||||
|
||||
func parseDataUnit(s string) int64 {
|
||||
// Allow kb, mb, gb
|
||||
if strings.HasSuffix(s, "b") {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
|
||||
if unit, ok := dataUnitMap[s[len(s)-1]]; ok {
|
||||
size, _ := strconv.ParseInt(s[:len(s)-1], 10, 64)
|
||||
return unit * size
|
||||
} else {
|
||||
// If no unit specified use bytes
|
||||
size, _ := strconv.ParseInt(s, 10, 64)
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
type unitSizeVar int64
|
||||
|
||||
func (u unitSizeVar) String() string {
|
||||
return strconv.Itoa(int(u))
|
||||
}
|
||||
|
||||
func (u unitSizeVar) Set(s string) error {
|
||||
u = unitSizeVar(parseDataUnit(s))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFileOutput(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewFileOutput("/tmp/test_requests.gor", &FileOutputConfig{flushInterval: time.Minute, append: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(2)
|
||||
input.EmitGET()
|
||||
input.EmitPOST()
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
output.flush()
|
||||
|
||||
close(quit)
|
||||
|
||||
quit = make(chan int)
|
||||
|
||||
var counter int64
|
||||
input2 := NewFileInput("/tmp/test_requests.gor", false)
|
||||
output2 := NewTestOutput(func(data []byte) {
|
||||
atomic.AddInt64(&counter, 1)
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input2}
|
||||
Plugins.Outputs = []io.Writer{output2}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestFileOutputWithNameCleaning(t *testing.T) {
|
||||
output := &FileOutput{pathTemplate: "./test_requests.gor", config: &FileOutputConfig{flushInterval: time.Minute, append: false}}
|
||||
expectedFileName := "test_requests_0.gor"
|
||||
output.updateName()
|
||||
|
||||
if expectedFileName != output.currentName {
|
||||
t.Errorf("Expected path %s but got %s", expectedFileName, output.currentName)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFileOutputPathTemplate(t *testing.T) {
|
||||
output := &FileOutput{pathTemplate: "/tmp/log-%Y-%m-%d-%S", config: &FileOutputConfig{flushInterval: time.Minute, append: true}}
|
||||
now := time.Now()
|
||||
expectedPath := fmt.Sprintf("/tmp/log-%s-%s-%s-%s", now.Format("2006"), now.Format("01"), now.Format("02"), now.Format("05"))
|
||||
path := output.filename()
|
||||
|
||||
if expectedPath != path {
|
||||
t.Errorf("Expected path %s but got %s", expectedPath, path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileOutputMultipleFiles(t *testing.T) {
|
||||
output := NewFileOutput("/tmp/log-%Y-%m-%d-%S", &FileOutputConfig{append: true, flushInterval: time.Minute})
|
||||
|
||||
if output.file != nil {
|
||||
t.Error("Should not initialize file if no writes")
|
||||
}
|
||||
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
name1 := output.file.Name()
|
||||
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
name2 := output.file.Name()
|
||||
|
||||
time.Sleep(time.Second)
|
||||
output.updateName()
|
||||
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
name3 := output.file.Name()
|
||||
|
||||
if name2 != name1 {
|
||||
t.Error("Fast changes should happen in same file:", name1, name2, name3)
|
||||
}
|
||||
|
||||
if name3 == name1 {
|
||||
t.Error("File name should change:", name1, name2, name3)
|
||||
}
|
||||
|
||||
os.Remove(name1)
|
||||
os.Remove(name3)
|
||||
}
|
||||
|
||||
func TestFileOutputCompression(t *testing.T) {
|
||||
output := NewFileOutput("/tmp/log-%Y-%m-%d-%S.gz", &FileOutputConfig{append: true, flushInterval: time.Minute})
|
||||
|
||||
if output.file != nil {
|
||||
t.Error("Should not initialize file if no writes")
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
}
|
||||
|
||||
name := output.file.Name()
|
||||
output.Close()
|
||||
|
||||
s, _ := os.Stat(name)
|
||||
if s.Size() == 12*1000 {
|
||||
t.Error("Should be compressed file:", s.Size())
|
||||
}
|
||||
|
||||
os.Remove(name)
|
||||
}
|
||||
|
||||
func TestParseDataUnit(t *testing.T) {
|
||||
var tests = []struct {
|
||||
value string
|
||||
size int64
|
||||
}{
|
||||
{"100kb", dataUnitMap['k'] * 100},
|
||||
{"100k", dataUnitMap['k'] * 100},
|
||||
{"1kb", dataUnitMap['k']},
|
||||
{"1g", dataUnitMap['g']},
|
||||
{"10m", dataUnitMap['m'] * 10},
|
||||
{"zsaa312", 0},
|
||||
}
|
||||
|
||||
for _, c := range tests {
|
||||
if parseDataUnit(c.value) != c.size {
|
||||
t.Error(c.value, "should be", c.size, "instead", parseDataUnit(c.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFileIndex(t *testing.T) {
|
||||
var tests = []struct {
|
||||
path string
|
||||
index int
|
||||
}{
|
||||
{"/tmp/logs", -1},
|
||||
{"/tmp/logs_1", 1},
|
||||
{"/tmp/logs_2.gz", 2},
|
||||
{"/tmp/logs_0.gz", 0},
|
||||
}
|
||||
|
||||
for _, c := range tests {
|
||||
if getFileIndex(c.path) != c.index {
|
||||
t.Error(c.path, "should be", c.index, "instead", getFileIndex(c.path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFileIndex(t *testing.T) {
|
||||
var tests = []struct {
|
||||
path string
|
||||
index int
|
||||
newPath string
|
||||
}{
|
||||
{"/tmp/logs", 0, "/tmp/logs_0"},
|
||||
{"/tmp/logs.gz", 1, "/tmp/logs_1.gz"},
|
||||
{"/tmp/logs_1", 0, "/tmp/logs_0"},
|
||||
{"/tmp/logs_0", 10, "/tmp/logs_10"},
|
||||
{"/tmp/logs_0.gz", 10, "/tmp/logs_10.gz"},
|
||||
{"/tmp/logs_underscores.gz", 10, "/tmp/logs_underscores_10.gz"},
|
||||
}
|
||||
|
||||
for _, c := range tests {
|
||||
if setFileIndex(c.path, c.index) != c.newPath {
|
||||
t.Error(c.path, "should be", c.newPath, "instead", setFileIndex(c.path, c.index))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileOutputAppendQueueLimitOverflow(t *testing.T) {
|
||||
rnd := rand.Int63()
|
||||
name := fmt.Sprintf("/tmp/%d", rnd)
|
||||
|
||||
output := NewFileOutput(name, &FileOutputConfig{append: false, flushInterval: time.Minute, queueLimit: 2})
|
||||
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
name1 := output.file.Name()
|
||||
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
name2 := output.file.Name()
|
||||
|
||||
output.updateName()
|
||||
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
name3 := output.file.Name()
|
||||
|
||||
if name2 != name1 || name1 != fmt.Sprintf("/tmp/%d_0", rnd) {
|
||||
t.Error("Fast changes should happen in same file:", name1, name2, name3)
|
||||
}
|
||||
|
||||
if name3 == name1 || name3 != fmt.Sprintf("/tmp/%d_1", rnd) {
|
||||
t.Error("File name should change:", name1, name2, name3)
|
||||
}
|
||||
|
||||
os.Remove(name1)
|
||||
os.Remove(name3)
|
||||
}
|
||||
|
||||
func TestFileOutputAppendQueueLimitNoOverflow(t *testing.T) {
|
||||
rnd := rand.Int63()
|
||||
name := fmt.Sprintf("/tmp/%d", rnd)
|
||||
|
||||
output := NewFileOutput(name, &FileOutputConfig{append: false, flushInterval: time.Minute, queueLimit: 3})
|
||||
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
name1 := output.file.Name()
|
||||
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
name2 := output.file.Name()
|
||||
|
||||
output.updateName()
|
||||
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
name3 := output.file.Name()
|
||||
|
||||
if name2 != name1 || name1 != fmt.Sprintf("/tmp/%d_0", rnd) {
|
||||
t.Error("Fast changes should happen in same file:", name1, name2, name3)
|
||||
}
|
||||
|
||||
if name3 != name1 || name3 != fmt.Sprintf("/tmp/%d_0", rnd) {
|
||||
t.Error("File name should not change:", name1, name2, name3)
|
||||
}
|
||||
|
||||
os.Remove(name1)
|
||||
os.Remove(name3)
|
||||
}
|
||||
|
||||
func TestFileOutputAppendQueueLimitGzips(t *testing.T) {
|
||||
rnd := rand.Int63()
|
||||
name := fmt.Sprintf("/tmp/%d.gz", rnd)
|
||||
|
||||
output := NewFileOutput(name, &FileOutputConfig{append: false, flushInterval: time.Minute, queueLimit: 2})
|
||||
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
name1 := output.file.Name()
|
||||
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
name2 := output.file.Name()
|
||||
|
||||
output.updateName()
|
||||
|
||||
output.Write([]byte("1 1 1\r\ntest"))
|
||||
name3 := output.file.Name()
|
||||
|
||||
if name2 != name1 || name1 != fmt.Sprintf("/tmp/%d_0.gz", rnd) {
|
||||
t.Error("Fast changes should happen in same file:", name1, name2, name3)
|
||||
}
|
||||
|
||||
if name3 == name1 || name3 != fmt.Sprintf("/tmp/%d_1.gz", rnd) {
|
||||
t.Error("File name should change:", name1, name2, name3)
|
||||
}
|
||||
|
||||
os.Remove(name1)
|
||||
os.Remove(name3)
|
||||
}
|
||||
|
||||
func TestFileOutputSort(t *testing.T) {
|
||||
var files = []string{"2016_0", "2014_10", "2015_0", "2015_10", "2015_2"}
|
||||
var expected = []string{"2014_10", "2015_0", "2015_2", "2015_10", "2016_0"}
|
||||
sort.Sort(sortByFileIndex(files))
|
||||
|
||||
if !reflect.DeepEqual(files, expected) {
|
||||
t.Error("Should properly sort file names using indexes", files, expected)
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/buger/gor/proto"
|
||||
)
|
||||
|
||||
const initialDynamicWorkers = 10
|
||||
|
||||
type response struct {
|
||||
payload []byte
|
||||
uuid []byte
|
||||
roundTripTime int64
|
||||
}
|
||||
|
||||
// HTTPOutputConfig struct for holding http output configuration
|
||||
type HTTPOutputConfig struct {
|
||||
redirectLimit int
|
||||
|
||||
stats bool
|
||||
workers int
|
||||
|
||||
elasticSearch string
|
||||
|
||||
Timeout time.Duration
|
||||
OriginalHost bool
|
||||
BufferSize int
|
||||
|
||||
Debug bool
|
||||
|
||||
TrackResponses bool
|
||||
}
|
||||
|
||||
// HTTPOutput plugin manage pool of workers which send request to replayed server
|
||||
// By default workers pool is dynamic and starts with 10 workers
|
||||
// You can specify fixed number of workers using `--output-http-workers`
|
||||
type HTTPOutput struct {
|
||||
// Keep this as first element of struct because it guarantees 64bit
|
||||
// alignment. atomic.* functions crash on 32bit machines if operand is not
|
||||
// aligned at 64bit. See https://github.com/golang/go/issues/599
|
||||
activeWorkers int64
|
||||
|
||||
address string
|
||||
limit int
|
||||
queue chan []byte
|
||||
|
||||
responses chan response
|
||||
|
||||
needWorker chan int
|
||||
|
||||
config *HTTPOutputConfig
|
||||
|
||||
queueStats *GorStat
|
||||
|
||||
elasticSearch *ESPlugin
|
||||
}
|
||||
|
||||
// NewHTTPOutput constructor for HTTPOutput
|
||||
// Initialize workers
|
||||
func NewHTTPOutput(address string, config *HTTPOutputConfig) io.Writer {
|
||||
o := new(HTTPOutput)
|
||||
|
||||
o.address = address
|
||||
o.config = config
|
||||
|
||||
if o.config.stats {
|
||||
o.queueStats = NewGorStat("output_http")
|
||||
}
|
||||
|
||||
o.queue = make(chan []byte, 1000)
|
||||
o.responses = make(chan response, 1000)
|
||||
o.needWorker = make(chan int, 1)
|
||||
|
||||
// Initial workers count
|
||||
if o.config.workers == 0 {
|
||||
o.needWorker <- initialDynamicWorkers
|
||||
} else {
|
||||
o.needWorker <- o.config.workers
|
||||
}
|
||||
|
||||
if o.config.elasticSearch != "" {
|
||||
o.elasticSearch = new(ESPlugin)
|
||||
o.elasticSearch.Init(o.config.elasticSearch)
|
||||
}
|
||||
|
||||
if len(Settings.middleware) > 0 {
|
||||
o.config.TrackResponses = true
|
||||
}
|
||||
|
||||
go o.workerMaster()
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) workerMaster() {
|
||||
for {
|
||||
newWorkers := <-o.needWorker
|
||||
for i := 0; i < newWorkers; i++ {
|
||||
go o.startWorker()
|
||||
}
|
||||
|
||||
// Disable dynamic scaling if workers poll fixed size
|
||||
if o.config.workers != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) startWorker() {
|
||||
client := NewHTTPClient(o.address, &HTTPClientConfig{
|
||||
FollowRedirects: o.config.redirectLimit,
|
||||
Debug: o.config.Debug,
|
||||
OriginalHost: o.config.OriginalHost,
|
||||
Timeout: o.config.Timeout,
|
||||
ResponseBufferSize: o.config.BufferSize,
|
||||
})
|
||||
|
||||
deathCount := 0
|
||||
|
||||
atomic.AddInt64(&o.activeWorkers, 1)
|
||||
|
||||
for {
|
||||
select {
|
||||
case data := <-o.queue:
|
||||
o.sendRequest(client, data)
|
||||
deathCount = 0
|
||||
case <-time.After(time.Millisecond * 100):
|
||||
// When dynamic scaling enabled workers die after 2s of inactivity
|
||||
if o.config.workers == 0 {
|
||||
deathCount++
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if deathCount > 20 {
|
||||
workersCount := atomic.LoadInt64(&o.activeWorkers)
|
||||
|
||||
// At least 1 startWorker should be alive
|
||||
if workersCount != 1 {
|
||||
atomic.AddInt64(&o.activeWorkers, -1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) Write(data []byte) (n int, err error) {
|
||||
if !isRequestPayload(data) {
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
buf := make([]byte, len(data))
|
||||
copy(buf, data)
|
||||
|
||||
o.queue <- buf
|
||||
|
||||
if o.config.stats {
|
||||
o.queueStats.Write(len(o.queue))
|
||||
}
|
||||
|
||||
if o.config.workers == 0 {
|
||||
workersCount := atomic.LoadInt64(&o.activeWorkers)
|
||||
|
||||
if len(o.queue) > int(workersCount) {
|
||||
o.needWorker <- len(o.queue)
|
||||
}
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) Read(data []byte) (int, error) {
|
||||
resp := <-o.responses
|
||||
|
||||
Debug("[OUTPUT-HTTP] Received response:", string(resp.payload))
|
||||
|
||||
header := payloadHeader(ReplayedResponsePayload, resp.uuid, resp.roundTripTime)
|
||||
copy(data[0:len(header)], header)
|
||||
copy(data[len(header):], resp.payload)
|
||||
|
||||
return len(resp.payload) + len(header), nil
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) sendRequest(client *HTTPClient, request []byte) {
|
||||
meta := payloadMeta(request)
|
||||
if len(meta) < 2 {
|
||||
return
|
||||
}
|
||||
uuid := meta[1]
|
||||
|
||||
body := payloadBody(request)
|
||||
if !proto.IsHTTPPayload(body) {
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.Send(body)
|
||||
stop := time.Now()
|
||||
|
||||
if err != nil {
|
||||
Debug("Request error:", err)
|
||||
}
|
||||
|
||||
if o.config.TrackResponses {
|
||||
o.responses <- response{resp, uuid, stop.UnixNano() - start.UnixNano()}
|
||||
}
|
||||
|
||||
if o.elasticSearch != nil {
|
||||
o.elasticSearch.ResponseAnalyze(request, resp, start, stop)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) String() string {
|
||||
return "HTTP output: " + o.address
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
_ "net/http/httputil"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestHTTPOutput(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Header.Get("User-Agent") != "Gor" {
|
||||
t.Error("Wrong header")
|
||||
}
|
||||
|
||||
if req.Method == "OPTIONS" {
|
||||
t.Error("Wrong method")
|
||||
}
|
||||
|
||||
if req.Method == "POST" {
|
||||
defer req.Body.Close()
|
||||
body, _ := ioutil.ReadAll(req.Body)
|
||||
|
||||
if string(body) != "a=1&b=2" {
|
||||
t.Error("Wrong POST body:", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
headers := HTTPHeaders{HTTPHeader{"User-Agent", "Gor"}}
|
||||
methods := HTTPMethods{[]byte("GET"), []byte("PUT"), []byte("POST")}
|
||||
Settings.modifierConfig = HTTPModifierConfig{headers: headers, methods: methods}
|
||||
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{Debug: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(2) // OPTIONS should be ignored
|
||||
input.EmitPOST()
|
||||
input.EmitOPTIONS()
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
|
||||
Settings.modifierConfig = HTTPModifierConfig{}
|
||||
}
|
||||
|
||||
func TestHTTPOutputKeepOriginalHost(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Host != "custom-host.com" {
|
||||
t.Error("Wrong header", req.Host)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
headers := HTTPHeaders{HTTPHeader{"Host", "custom-host.com"}}
|
||||
Settings.modifierConfig = HTTPModifierConfig{headers: headers}
|
||||
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{Debug: false, OriginalHost: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
|
||||
Settings.modifierConfig = HTTPModifierConfig{}
|
||||
}
|
||||
|
||||
func TestOutputHTTPSSL(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
// Origing and Replay server initialization
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
wg.Done()
|
||||
}))
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
input.EmitPOST()
|
||||
input.EmitGET()
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func BenchmarkHTTPOutput(b *testing.B) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitPOST()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TCPOutput used for sending raw tcp payloads
|
||||
// Currently used for internal communication between listener and replay server
|
||||
// Can be used for transfering binary payloads like protocol buffers
|
||||
type TCPOutput struct {
|
||||
address string
|
||||
limit int
|
||||
buf chan []byte
|
||||
bufStats *GorStat
|
||||
}
|
||||
|
||||
// NewTCPOutput constructor for TCPOutput
|
||||
// Initialize 10 workers which hold keep-alive connection
|
||||
func NewTCPOutput(address string) io.Writer {
|
||||
o := new(TCPOutput)
|
||||
|
||||
o.address = address
|
||||
|
||||
o.buf = make(chan []byte, 100)
|
||||
if Settings.outputTCPStats {
|
||||
o.bufStats = NewGorStat("output_tcp")
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go o.worker()
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *TCPOutput) worker() {
|
||||
retries := 1
|
||||
conn, err := o.connect(o.address)
|
||||
for {
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
log.Println("Can't connect to aggregator instance, reconnecting in 1 second. Retries:", retries)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
conn, err = o.connect(o.address)
|
||||
retries++
|
||||
}
|
||||
|
||||
if retries > 0 {
|
||||
log.Println("Connected to aggregator instance after ", retries, " retries")
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
for {
|
||||
conn.Write(<-o.buf)
|
||||
_, err := conn.Write([]byte(payloadSeparator))
|
||||
|
||||
if err != nil {
|
||||
log.Println("Lost connection with aggregator instance, reconnecting")
|
||||
go o.worker()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *TCPOutput) Write(data []byte) (n int, err error) {
|
||||
if !isOriginPayload(data) {
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// We have to copy, because sending data in multiple threads
|
||||
newBuf := make([]byte, len(data))
|
||||
copy(newBuf, data)
|
||||
|
||||
o.buf <- newBuf
|
||||
|
||||
if Settings.outputTCPStats {
|
||||
o.bufStats.Write(len(o.buf))
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (o *TCPOutput) connect(address string) (conn net.Conn, err error) {
|
||||
conn, err = net.Dial("tcp", address)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (o *TCPOutput) String() string {
|
||||
return fmt.Sprintf("TCP output %s, limit: %d", o.address, o.limit)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTCPOutput(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
listener := startTCP(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
input := NewTestInput()
|
||||
output := NewTCPOutput(listener.Addr().String())
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func startTCP(cb func([]byte)) net.Listener {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Can't start:", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, _ := listener.Accept()
|
||||
defer conn.Close()
|
||||
|
||||
go func() {
|
||||
reader := bufio.NewReader(conn)
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Split(payloadScanner)
|
||||
|
||||
for scanner.Scan() {
|
||||
cb(scanner.Bytes())
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
return listener
|
||||
}
|
||||
|
||||
func BenchmarkTCPOutput(b *testing.B) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
listener := startTCP(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
input := NewTestInput()
|
||||
output := NewTCPOutput(listener.Addr().String())
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InOutPlugins struct for holding references to plugins
|
||||
type InOutPlugins struct {
|
||||
Inputs []io.Reader
|
||||
Outputs []io.Writer
|
||||
All []interface{}
|
||||
}
|
||||
|
||||
var pluginMu sync.Mutex
|
||||
|
||||
// Plugins holds all the plugin objects
|
||||
var Plugins *InOutPlugins = new(InOutPlugins)
|
||||
|
||||
// extractLimitOptions detects if plugin get called with limiter support
|
||||
// Returns address and limit
|
||||
func extractLimitOptions(options string) (string, string) {
|
||||
split := strings.Split(options, "|")
|
||||
|
||||
if len(split) > 1 {
|
||||
return split[0], split[1]
|
||||
}
|
||||
|
||||
return split[0], ""
|
||||
}
|
||||
|
||||
// Automatically detects type of plugin and initialize it
|
||||
//
|
||||
// See this article if curious about relfect stuff below: http://blog.burntsushi.net/type-parametric-functions-golang
|
||||
func registerPlugin(constructor interface{}, options ...interface{}) {
|
||||
var path, limit string
|
||||
vc := reflect.ValueOf(constructor)
|
||||
|
||||
// Pre-processing options to make it work with reflect
|
||||
vo := []reflect.Value{}
|
||||
for _, oi := range options {
|
||||
vo = append(vo, reflect.ValueOf(oi))
|
||||
}
|
||||
|
||||
if len(vo) > 0 {
|
||||
// Removing limit options from path
|
||||
path, limit = extractLimitOptions(vo[0].String())
|
||||
|
||||
// Writing value back without limiter "|" options
|
||||
vo[0] = reflect.ValueOf(path)
|
||||
}
|
||||
|
||||
// Calling our constructor with list of given options
|
||||
plugin := vc.Call(vo)[0].Interface()
|
||||
pluginWrapper := plugin
|
||||
|
||||
if limit != "" {
|
||||
pluginWrapper = NewLimiter(plugin, limit)
|
||||
} else {
|
||||
pluginWrapper = plugin
|
||||
}
|
||||
|
||||
_, isR := plugin.(io.Reader)
|
||||
_, isW := plugin.(io.Writer)
|
||||
|
||||
// Some of the output can be Readers as well because return responses
|
||||
if isR && !isW {
|
||||
Plugins.Inputs = append(Plugins.Inputs, pluginWrapper.(io.Reader))
|
||||
}
|
||||
|
||||
if isW {
|
||||
Plugins.Outputs = append(Plugins.Outputs, pluginWrapper.(io.Writer))
|
||||
}
|
||||
|
||||
Plugins.All = append(Plugins.All, plugin)
|
||||
}
|
||||
|
||||
// InitPlugins specify and initialize all available plugins
|
||||
func InitPlugins() {
|
||||
pluginMu.Lock()
|
||||
defer pluginMu.Unlock()
|
||||
|
||||
for _, options := range Settings.inputDummy {
|
||||
registerPlugin(NewDummyInput, options)
|
||||
}
|
||||
|
||||
for range Settings.outputDummy {
|
||||
registerPlugin(NewDummyOutput)
|
||||
}
|
||||
|
||||
if Settings.outputStdout {
|
||||
registerPlugin(NewDummyOutput)
|
||||
}
|
||||
|
||||
engine := EnginePcap
|
||||
if Settings.inputRAWEngine == "raw_socket" {
|
||||
engine = EngineRawSocket
|
||||
}
|
||||
|
||||
for _, options := range Settings.inputRAW {
|
||||
registerPlugin(NewRAWInput, options, engine, Settings.inputRAWTrackResponse, time.Duration(0), Settings.inputRAWRealIPHeader)
|
||||
}
|
||||
|
||||
for _, options := range Settings.inputTCP {
|
||||
registerPlugin(NewTCPInput, options)
|
||||
}
|
||||
|
||||
for _, options := range Settings.outputTCP {
|
||||
registerPlugin(NewTCPOutput, options)
|
||||
}
|
||||
|
||||
for _, options := range Settings.inputFile {
|
||||
registerPlugin(NewFileInput, options, Settings.inputFileLoop)
|
||||
}
|
||||
|
||||
for _, options := range Settings.outputFile {
|
||||
registerPlugin(NewFileOutput, options, &Settings.outputFileConfig)
|
||||
}
|
||||
|
||||
for _, options := range Settings.inputHTTP {
|
||||
registerPlugin(NewHTTPInput, options)
|
||||
}
|
||||
|
||||
// If we explicitly set Host header http output should not rewrite it
|
||||
// Fix: https://github.com/buger/gor/issues/174
|
||||
for _, header := range Settings.modifierConfig.headers {
|
||||
if header.Name == "Host" {
|
||||
Settings.outputHTTPConfig.OriginalHost = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, options := range Settings.outputHTTP {
|
||||
registerPlugin(NewHTTPOutput, options, &Settings.outputHTTPConfig)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPluginsRegistration(t *testing.T) {
|
||||
Plugins.Inputs = []io.Reader{}
|
||||
Plugins.Outputs = []io.Writer{}
|
||||
|
||||
Settings.inputDummy = MultiOption{"[]"}
|
||||
Settings.outputDummy = MultiOption{"[]"}
|
||||
Settings.outputHTTP = MultiOption{"www.example.com|10"}
|
||||
Settings.inputFile = MultiOption{"/dev/null"}
|
||||
|
||||
InitPlugins()
|
||||
|
||||
if len(Plugins.Inputs) != 2 {
|
||||
t.Errorf("Should be 2 inputs %d", len(Plugins.Inputs))
|
||||
}
|
||||
|
||||
if _, ok := Plugins.Inputs[0].(*DummyInput); !ok {
|
||||
t.Errorf("First input should be DummyInput")
|
||||
}
|
||||
|
||||
if _, ok := Plugins.Inputs[1].(*FileInput); !ok {
|
||||
t.Errorf("Second input should be FileInput")
|
||||
}
|
||||
|
||||
if len(Plugins.Outputs) != 2 {
|
||||
t.Errorf("Should be 2 output %d", len(Plugins.Outputs))
|
||||
}
|
||||
|
||||
if _, ok := Plugins.Outputs[0].(*DummyOutput); !ok {
|
||||
t.Errorf("First output should be DummyOutput")
|
||||
}
|
||||
|
||||
if l, ok := Plugins.Outputs[1].(*Limiter); ok {
|
||||
if _, ok := l.plugin.(*HTTPOutput); !ok {
|
||||
t.Errorf("HTTPOutput should be wrapped in limiter")
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Second output should be Limiter")
|
||||
}
|
||||
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
Package proto provides byte-level interaction with HTTP request payload.
|
||||
|
||||
Example of HTTP payload for future references, new line symbols escaped:
|
||||
|
||||
POST /upload HTTP/1.1\r\n
|
||||
User-Agent: Gor\r\n
|
||||
Content-Length: 11\r\n
|
||||
\r\n
|
||||
Hello world
|
||||
|
||||
GET /index.html HTTP/1.1\r\n
|
||||
User-Agent: Gor\r\n
|
||||
\r\n
|
||||
\r\n
|
||||
*/
|
||||
package proto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/buger/gor/byteutils"
|
||||
)
|
||||
|
||||
// In HTTP newline defined by 2 bytes (for both windows and *nix support)
|
||||
var CLRF = []byte("\r\n")
|
||||
|
||||
// New line acts as separator: end of Headers or Body (in some cases)
|
||||
var EmptyLine = []byte("\r\n\r\n")
|
||||
|
||||
// Separator for Header line. Header looks like: `HeaderName: value`
|
||||
var HeaderDelim = []byte(": ")
|
||||
|
||||
// MIMEHeadersEndPos finds end of the Headers section, which should end with empty line.
|
||||
func MIMEHeadersEndPos(payload []byte) int {
|
||||
return bytes.Index(payload, EmptyLine)
|
||||
}
|
||||
|
||||
// MIMEHeadersStartPos finds start of Headers section
|
||||
// It just finds position of second line (first contains location and method).
|
||||
func MIMEHeadersStartPos(payload []byte) int {
|
||||
return bytes.Index(payload, CLRF) + 2 // Find first line end
|
||||
}
|
||||
|
||||
func isLower(b byte) bool {
|
||||
if 'a' <= b && b <= 'z' {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func toUpper(b byte) byte {
|
||||
if 'a' <= b && b <= 'z' {
|
||||
b -= 'a' - 'A'
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func toLower(b byte) byte {
|
||||
if 'A' <= b && b <= 'Z' {
|
||||
b += 'a' - 'A'
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func headerIndex(payload []byte, name []byte) int {
|
||||
isLower := isLower(name[0])
|
||||
i := 0
|
||||
|
||||
for {
|
||||
if i >= len(payload) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if payload[i] == '\n' {
|
||||
i++
|
||||
|
||||
// We are at the end
|
||||
if i == len(payload) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if payload[i] == name[0] ||
|
||||
(!isLower && payload[i] == toLower(name[0])) ||
|
||||
(isLower && payload[i] == toUpper(name[0])) {
|
||||
|
||||
i++
|
||||
j := 1
|
||||
for {
|
||||
if j == len(name) {
|
||||
// Matched, and return start of the header
|
||||
return i - len(name)
|
||||
}
|
||||
|
||||
if payload[i] != name[j] {
|
||||
break
|
||||
}
|
||||
|
||||
// If compound header name do one more case check: Content-Length or Transfer-Encoding
|
||||
if name[j] == '-' {
|
||||
i++
|
||||
j++
|
||||
|
||||
if !(payload[i] == name[j] ||
|
||||
(!isLower && payload[i] == toLower(name[j])) ||
|
||||
(isLower && payload[i] == toUpper(name[j]))) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
j++
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// header return value and positions of header/value start/end.
|
||||
// If not found, value will be blank, and headerStart will be -1
|
||||
// Do not support multi-line headers.
|
||||
func header(payload []byte, name []byte) (value []byte, headerStart, headerEnd, valueStart, valueEnd int) {
|
||||
headerStart = headerIndex(payload, name)
|
||||
|
||||
if headerStart == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
valueStart = headerStart + len(name) + 1 // Skip ":" after header name
|
||||
headerEnd = valueStart + bytes.IndexByte(payload[valueStart:], '\n')
|
||||
|
||||
for valueStart < headerEnd { // Ignore empty space after ':'
|
||||
if payload[valueStart] == ' ' {
|
||||
valueStart++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
valueEnd = valueStart + bytes.IndexByte(payload[valueStart:], '\n')
|
||||
|
||||
if payload[headerEnd-1] == '\r' {
|
||||
valueEnd--
|
||||
}
|
||||
|
||||
// ignore empty space at end of header value
|
||||
for valueStart < valueEnd {
|
||||
if payload[valueEnd-1] == ' ' {
|
||||
valueEnd--
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
value = payload[valueStart:valueEnd]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Header returns header value, if header not found, value will be blank
|
||||
func Header(payload, name []byte) []byte {
|
||||
val, _, _, _, _ := header(payload, name)
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// SetHeader sets header value. If header not found it creates new one.
|
||||
// Returns modified request payload
|
||||
func SetHeader(payload, name, value []byte) []byte {
|
||||
_, hs, _, vs, ve := header(payload, name)
|
||||
|
||||
if hs != -1 {
|
||||
// If header found we just replace its value
|
||||
return byteutils.Replace(payload, vs, ve, value)
|
||||
}
|
||||
|
||||
return AddHeader(payload, name, value)
|
||||
}
|
||||
|
||||
// AddHeader takes http payload and appends new header to the start of headers section
|
||||
// Returns modified request payload
|
||||
func AddHeader(payload, name, value []byte) []byte {
|
||||
header := make([]byte, len(name)+2+len(value)+2)
|
||||
copy(header[0:], name)
|
||||
copy(header[len(name):], HeaderDelim)
|
||||
copy(header[len(name)+2:], value)
|
||||
copy(header[len(header)-2:], CLRF)
|
||||
|
||||
mimeStart := MIMEHeadersStartPos(payload)
|
||||
|
||||
return byteutils.Insert(payload, mimeStart, header)
|
||||
}
|
||||
|
||||
// DelHeader takes http payload and removes header name from headers section
|
||||
// Returns modified request payload
|
||||
func DeleteHeader(payload, name[]byte) []byte {
|
||||
_, hs, he, _, _ := header(payload, name)
|
||||
if hs != -1 {
|
||||
newHeader := make([]byte, len(payload) - (he - hs) - 1)
|
||||
copy(newHeader[:hs], payload[:hs])
|
||||
copy(newHeader[hs:], payload[he + 1:])
|
||||
return newHeader
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
// Body returns request/response body
|
||||
func Body(payload []byte) []byte {
|
||||
// 4 -> len(EMPTY_LINE)
|
||||
return payload[MIMEHeadersEndPos(payload)+4:]
|
||||
}
|
||||
|
||||
// Path takes payload and retuns request path: Split(firstLine, ' ')[1]
|
||||
func Path(payload []byte) []byte {
|
||||
start := bytes.IndexByte(payload, ' ') + 1
|
||||
end := bytes.IndexByte(payload[start:], ' ')
|
||||
|
||||
return payload[start : start+end]
|
||||
}
|
||||
|
||||
// SetPath takes payload, sets new path and returns modified payload
|
||||
func SetPath(payload, path []byte) []byte {
|
||||
start := bytes.IndexByte(payload, ' ') + 1
|
||||
end := bytes.IndexByte(payload[start:], ' ')
|
||||
|
||||
return byteutils.Replace(payload, start, start+end, path)
|
||||
}
|
||||
|
||||
// PathParam returns URL query attribute by given name, if no found: valueStart will be -1
|
||||
func PathParam(payload, name []byte) (value []byte, valueStart, valueEnd int) {
|
||||
path := Path(payload)
|
||||
|
||||
if paramStart := bytes.Index(path, append(name, '=')); paramStart != -1 {
|
||||
valueStart := paramStart + len(name) + 1
|
||||
paramEnd := bytes.IndexByte(path[valueStart:], '&')
|
||||
|
||||
// Param can end with '&' (another param), or end of line
|
||||
if paramEnd == -1 { // It is final param
|
||||
paramEnd = len(path)
|
||||
} else {
|
||||
paramEnd += valueStart
|
||||
}
|
||||
|
||||
return path[valueStart:paramEnd], valueStart, paramEnd
|
||||
}
|
||||
|
||||
return []byte(""), -1, -1
|
||||
}
|
||||
|
||||
// SetPathParam takes payload and updates path Query attribute
|
||||
// If query param not found, it will append new
|
||||
// Returns modified payload
|
||||
func SetPathParam(payload, name, value []byte) []byte {
|
||||
path := Path(payload)
|
||||
_, vs, ve := PathParam(payload, name)
|
||||
|
||||
if vs != -1 { // If param found, replace its value and set new Path
|
||||
newPath := make([]byte, len(path))
|
||||
copy(newPath, path)
|
||||
newPath = byteutils.Replace(newPath, vs, ve, value)
|
||||
|
||||
return SetPath(payload, newPath)
|
||||
}
|
||||
|
||||
// if param not found append to end of url
|
||||
// Adding 2 because of '?' or '&' at start, and '=' in middle
|
||||
newParam := make([]byte, len(name)+len(value)+2)
|
||||
|
||||
if bytes.IndexByte(path, '?') == -1 {
|
||||
newParam[0] = '?'
|
||||
} else {
|
||||
newParam[0] = '&'
|
||||
}
|
||||
|
||||
// Copy "param=value" into buffer, after it looks like "?param=value"
|
||||
copy(newParam[1:], name)
|
||||
newParam[1+len(name)] = '='
|
||||
copy(newParam[2+len(name):], value)
|
||||
|
||||
// Append param to the end of path
|
||||
newPath := make([]byte, len(path)+len(newParam))
|
||||
copy(newPath, path)
|
||||
copy(newPath[len(path):], newParam)
|
||||
|
||||
return SetPath(payload, newPath)
|
||||
}
|
||||
|
||||
// SetHost updates Host header for HTTP/1.1 or updates host in path for HTTP/1.0 or Proxy requests
|
||||
// Returns modified payload
|
||||
func SetHost(payload, url, host []byte) []byte {
|
||||
// If this is HTTP 1.0 traffic or proxy traffic it may include host right into path variable, so instead of setting Host header we rewrite Path
|
||||
// Fix for https://github.com/buger/gor/issues/156
|
||||
if path := Path(payload); bytes.HasPrefix(path, []byte("http")) {
|
||||
hostStart := bytes.IndexByte(path, ':') // : position "https?:"
|
||||
hostStart += 3 // Skip 1 ':' and 2 '\'
|
||||
hostEnd := hostStart + bytes.IndexByte(path[hostStart:], '/')
|
||||
|
||||
newPath := make([]byte, len(path))
|
||||
copy(newPath, path)
|
||||
newPath = byteutils.Replace(newPath, 0, hostEnd, url)
|
||||
|
||||
return SetPath(payload, newPath)
|
||||
}
|
||||
|
||||
return SetHeader(payload, []byte("Host"), host)
|
||||
}
|
||||
|
||||
// Method returns HTTP method
|
||||
func Method(payload []byte) []byte {
|
||||
end := bytes.IndexByte(payload, ' ')
|
||||
|
||||
return payload[:end]
|
||||
}
|
||||
|
||||
// Status returns response status.
|
||||
// It happend to be in same position as request payload path
|
||||
func Status(payload []byte) []byte {
|
||||
return Path(payload)
|
||||
}
|
||||
|
||||
var httpMethods []string = []string{
|
||||
"GET ", "OPTI", "HEAD", "POST", "PUT ", "DELE", "TRAC", "CONN", /* custom methods */"BAN", "PURG",
|
||||
}
|
||||
|
||||
func IsHTTPPayload(payload []byte) bool {
|
||||
if len(payload) < 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
method := string(payload[0:4])
|
||||
|
||||
for _, m := range httpMethods {
|
||||
if method == m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHeader(t *testing.T) {
|
||||
var payload, val []byte
|
||||
var headerStart int
|
||||
|
||||
// Value with space at start
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if val = Header(payload, []byte("Content-Length")); !bytes.Equal(val, []byte("7")) {
|
||||
t.Error("Should find header value")
|
||||
}
|
||||
|
||||
// Value with space at end
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7 \r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if val = Header(payload, []byte("Content-Length")); !bytes.Equal(val, []byte("7")) {
|
||||
t.Error("Should find header value without space after 7")
|
||||
}
|
||||
|
||||
// Value without space at start
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length:7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if val = Header(payload, []byte("Content-Length")); !bytes.Equal(val, []byte("7")) {
|
||||
t.Error("Should find header value without space after :")
|
||||
}
|
||||
|
||||
// Value is empty
|
||||
payload = []byte("GET /p HTTP/1.1\r\nCookie:\r\nHost: www.w3.org\r\n\r\n")
|
||||
|
||||
if val = Header(payload, []byte("Cookie")); len(val) > 0 {
|
||||
t.Error("Should return empty value")
|
||||
}
|
||||
|
||||
// Wrong delimeter
|
||||
payload = []byte("GET /p HTTP/1.1\r\nCookie: 123\nHost: www.w3.org\r\n\r\n")
|
||||
|
||||
if val = Header(payload, []byte("Cookie")); !bytes.Equal(val, []byte("123")) {
|
||||
t.Error("Should handle wrong header delimeter")
|
||||
}
|
||||
|
||||
// Header not found
|
||||
if _, headerStart, _, _, _ = header(payload, []byte("Not-Found")); headerStart != -1 {
|
||||
t.Error("Should not found header")
|
||||
}
|
||||
|
||||
// Lower case headers
|
||||
payload = []byte("POST /post HTTP/1.1\r\ncontent-length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if val = Header(payload, []byte("Content-Length")); !bytes.Equal(val, []byte("7")) {
|
||||
t.Error("Should find lower case 2 word header")
|
||||
}
|
||||
|
||||
payload = []byte("POST /post HTTP/1.1\r\ncontent-length: 7\r\nhost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if val = Header(payload, []byte("host")); !bytes.Equal(val, []byte("www.w3.org")) {
|
||||
t.Error("Should find lower case 1 word header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMIMEHeadersEndPos(t *testing.T) {
|
||||
head := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org")
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
end := MIMEHeadersEndPos(payload)
|
||||
|
||||
if !bytes.Equal(payload[:end], head) {
|
||||
t.Error("Wrong headers end position:", end)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMIMEHeadersStartPos(t *testing.T) {
|
||||
headers := []byte("Content-Length: 7\r\nHost: www.w3.org")
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
start := MIMEHeadersStartPos(payload)
|
||||
end := MIMEHeadersEndPos(payload)
|
||||
|
||||
if !bytes.Equal(payload[start:end], headers) {
|
||||
t.Error("Wrong headers end position:", start, end)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetHeader(t *testing.T) {
|
||||
var payload, payloadAfter []byte
|
||||
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post HTTP/1.1\r\nContent-Length: 14\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetHeader(payload, []byte("Content-Length"), []byte("14")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should update header if it exists", string(payload))
|
||||
}
|
||||
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post HTTP/1.1\r\nUser-Agent: Gor\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetHeader(payload, []byte("User-Agent"), []byte("Gor")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should add header if not found", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteHeader(t *testing.T) {
|
||||
var payload, payloadAfter []byte
|
||||
|
||||
payload = []byte("POST /post HTTP/1.1\r\nUser-Agent: Gor\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = DeleteHeader(payload, []byte("User-Agent")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should delete header if found", string(payload), string(payloadAfter))
|
||||
}
|
||||
|
||||
//Whitespace at end of User-Agent
|
||||
payload = []byte("POST /post HTTP/1.1\r\nUser-Agent: Gor \r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = DeleteHeader(payload, []byte("User-Agent")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should delete header if found", string(payload), string(payloadAfter))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
var path, payload []byte
|
||||
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if path = Path(payload); !bytes.Equal(path, []byte("/post")) {
|
||||
t.Error("Should find path", string(path))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPath(t *testing.T) {
|
||||
var payload, payloadAfter []byte
|
||||
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /new_path HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetPath(payload, []byte("/new_path")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should replace path", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathParam(t *testing.T) {
|
||||
var payload []byte
|
||||
|
||||
payload = []byte("POST /post?param=test&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if val, _, _ := PathParam(payload, []byte("param")); !bytes.Equal(val, []byte("test")) {
|
||||
t.Error("Should detect attribute", string(val))
|
||||
}
|
||||
|
||||
if val, _, _ := PathParam(payload, []byte("user_id")); !bytes.Equal(val, []byte("1")) {
|
||||
t.Error("Should detect attribute", string(val))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPathParam(t *testing.T) {
|
||||
var payload, payloadAfter []byte
|
||||
|
||||
payload = []byte("POST /post?param=test&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post?param=new&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetPathParam(payload, []byte("param"), []byte("new")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should replace existing value", string(payload))
|
||||
}
|
||||
|
||||
payload = []byte("POST /post?param=test&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post?param=test&user_id=2 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetPathParam(payload, []byte("user_id"), []byte("2")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should replace existing value", string(payload))
|
||||
}
|
||||
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post?param=test HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetPathParam(payload, []byte("param"), []byte("test")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should set param if url have no params", string(payload))
|
||||
}
|
||||
|
||||
payload = []byte("POST /post?param=test HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post?param=test&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetPathParam(payload, []byte("user_id"), []byte("1")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should set param at the end if url params", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetHostHTTP10(t *testing.T) {
|
||||
var payload, payloadAfter []byte
|
||||
|
||||
payload = []byte("POST http://example.com/post HTTP/1.0\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST http://new.com/post HTTP/1.0\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetHost(payload, []byte("http://new.com"), []byte("new.com")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should replace host", string(payload))
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
RequestPayload = '1'
|
||||
ResponsePayload = '2'
|
||||
ReplayedResponsePayload = '3'
|
||||
)
|
||||
|
||||
func uuid() []byte {
|
||||
b := make([]byte, 20)
|
||||
rand.Read(b)
|
||||
|
||||
uuid := make([]byte, 40)
|
||||
hex.Encode(uuid, b)
|
||||
|
||||
return uuid
|
||||
}
|
||||
|
||||
var payloadSeparator = "\n🐵🙈🙉\n"
|
||||
|
||||
func payloadScanner(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
if i := bytes.Index(data, []byte(payloadSeparator)); i >= 0 {
|
||||
// We have a full newline-terminated line.
|
||||
return i + len([]byte(payloadSeparator)), data[0:i], nil
|
||||
}
|
||||
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// Timing is request start or round-trip time, depending on payloadType
|
||||
func payloadHeader(payloadType byte, uuid []byte, timing int64) (header []byte) {
|
||||
sTime := strconv.FormatInt(timing, 10)
|
||||
|
||||
//Example:
|
||||
// 3 f45590522cd1838b4a0d5c5aab80b77929dea3b3 1231\n
|
||||
// `+ 1` indicates space characters or end of line
|
||||
header = make([]byte, 1+1+len(uuid)+1+len(sTime)+1)
|
||||
header[0] = payloadType
|
||||
header[1] = ' '
|
||||
header[2+len(uuid)] = ' '
|
||||
header[len(header)-1] = '\n'
|
||||
|
||||
copy(header[2:], uuid)
|
||||
copy(header[3+len(uuid):], sTime)
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
func payloadBody(payload []byte) []byte {
|
||||
headerSize := bytes.IndexByte(payload, '\n')
|
||||
return payload[headerSize+1:]
|
||||
}
|
||||
|
||||
func payloadMeta(payload []byte) [][]byte {
|
||||
headerSize := bytes.IndexByte(payload, '\n')
|
||||
if headerSize < 0 {
|
||||
headerSize = 0
|
||||
}
|
||||
return bytes.Split(payload[:headerSize], []byte{' '})
|
||||
}
|
||||
|
||||
func isOriginPayload(payload []byte) bool {
|
||||
switch payload[0] {
|
||||
case RequestPayload, ResponsePayload:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isRequestPayload(payload []byte) bool {
|
||||
return payload[0] == RequestPayload
|
||||
}
|
||||
@@ -0,0 +1,682 @@
|
||||
/*
|
||||
Package rawSocket provides traffic sniffier using RAW sockets.
|
||||
|
||||
Capture traffic from socket using RAW_SOCKET's
|
||||
http://en.wikipedia.org/wiki/Raw_socket
|
||||
|
||||
RAW_SOCKET allow you listen for traffic on any port (e.g. sniffing) because they operate on IP level.
|
||||
|
||||
Ports is TCP feature, same as flow control, reliable transmission and etc.
|
||||
|
||||
This package implements own TCP layer: TCP packets is parsed using tcp_packet.go, and flow control is managed by tcp_message.go
|
||||
*/
|
||||
package rawSocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/google/gopacket/pcap"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"github.com/buger/gor/proto"
|
||||
)
|
||||
|
||||
var _ = fmt.Println
|
||||
|
||||
// Listener handle traffic capture
|
||||
type Listener struct {
|
||||
mu sync.Mutex
|
||||
// buffer of TCPMessages waiting to be send
|
||||
// ID -> TCPMessage
|
||||
messages map[tcpID]*TCPMessage
|
||||
|
||||
// Expect: 100-continue request is send in 2 tcp messages
|
||||
// We store ACK aliases to merge this packets together
|
||||
ackAliases map[uint32]uint32
|
||||
// To get ACK of second message we need to compute its Seq and wait for them message
|
||||
seqWithData map[uint32]uint32
|
||||
|
||||
// Ack -> Req
|
||||
respAliases map[uint32]*TCPMessage
|
||||
|
||||
// Ack -> ID
|
||||
respWithoutReq map[uint32]tcpID
|
||||
|
||||
// Messages ready to be send to client
|
||||
packetsChan chan []byte
|
||||
|
||||
// Messages ready to be send to client
|
||||
messagesChan chan *TCPMessage
|
||||
|
||||
addr string // IP to listen
|
||||
port uint16 // Port to listen
|
||||
|
||||
trackResponse bool
|
||||
messageExpire time.Duration
|
||||
|
||||
conn net.PacketConn
|
||||
pcapHandles []*pcap.Handle
|
||||
|
||||
quit chan bool
|
||||
readyCh chan bool
|
||||
}
|
||||
|
||||
type request struct {
|
||||
id tcpID
|
||||
start time.Time
|
||||
ack uint32
|
||||
}
|
||||
|
||||
// Available engines for intercepting traffic
|
||||
const (
|
||||
EngineRawSocket = 1 << iota
|
||||
EnginePcap
|
||||
)
|
||||
|
||||
// NewListener creates and initializes new Listener object
|
||||
func NewListener(addr string, port string, engine int, trackResponse bool, expire time.Duration) (l *Listener) {
|
||||
l = &Listener{}
|
||||
|
||||
l.packetsChan = make(chan []byte, 10000)
|
||||
l.messagesChan = make(chan *TCPMessage, 10000)
|
||||
l.quit = make(chan bool)
|
||||
l.readyCh = make(chan bool, 1)
|
||||
|
||||
l.messages = make(map[tcpID]*TCPMessage)
|
||||
l.ackAliases = make(map[uint32]uint32)
|
||||
l.seqWithData = make(map[uint32]uint32)
|
||||
l.respAliases = make(map[uint32]*TCPMessage)
|
||||
l.respWithoutReq = make(map[uint32]tcpID)
|
||||
l.trackResponse = trackResponse
|
||||
|
||||
l.addr = addr
|
||||
_port, _ := strconv.Atoi(port)
|
||||
l.port = uint16(_port)
|
||||
|
||||
if expire.Nanoseconds() == 0 {
|
||||
expire = 2000 * time.Millisecond
|
||||
}
|
||||
|
||||
l.messageExpire = expire
|
||||
|
||||
go l.listen()
|
||||
|
||||
// Special case for testing
|
||||
if l.port != 0 {
|
||||
switch engine {
|
||||
case EngineRawSocket:
|
||||
go l.readRAWSocket()
|
||||
case EnginePcap:
|
||||
go l.readPcap()
|
||||
default:
|
||||
log.Fatal("Unknown traffic interception engine:", engine)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *Listener) listen() {
|
||||
gcTicker := time.Tick(t.messageExpire / 2)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.quit:
|
||||
if t.conn != nil {
|
||||
t.conn.Close()
|
||||
}
|
||||
return
|
||||
case data := <-t.packetsChan:
|
||||
packet := ParseTCPPacket(data[:16], data[16:])
|
||||
t.processTCPPacket(packet)
|
||||
case <-gcTicker:
|
||||
now := time.Now()
|
||||
|
||||
// Dispatch requests before responses
|
||||
for _, message := range t.messages {
|
||||
if now.Sub(message.End) >= t.messageExpire {
|
||||
t.dispatchMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Listener) deleteMessage(message *TCPMessage) {
|
||||
delete(t.messages, message.ID())
|
||||
delete(t.ackAliases, message.Ack)
|
||||
if message.DataAck != 0 {
|
||||
delete(t.ackAliases, message.DataAck)
|
||||
}
|
||||
if message.DataSeq != 0 {
|
||||
delete(t.seqWithData, message.DataSeq)
|
||||
}
|
||||
|
||||
delete(t.respAliases, message.ResponseAck)
|
||||
}
|
||||
|
||||
func (t *Listener) dispatchMessage(message *TCPMessage) {
|
||||
// If already dispatched
|
||||
if _, ok := t.messages[message.ID()]; !ok {
|
||||
return
|
||||
}
|
||||
|
||||
t.deleteMessage(message)
|
||||
|
||||
// log.Println("Dispatching, message", message.Start.UnixNano(), message.Seq, message.Ack, string(message.Bytes()))
|
||||
|
||||
if message.IsIncoming {
|
||||
// If there were response before request
|
||||
// log.Println("Looking for Response: ", t.respWithoutReq, message.ResponseAck)
|
||||
if t.trackResponse {
|
||||
if respID, ok := t.respWithoutReq[message.ResponseAck]; ok {
|
||||
if resp, rok := t.messages[respID]; rok {
|
||||
// if resp.AssocMessage == nil {
|
||||
// log.Println("FOUND RESPONSE")
|
||||
resp.AssocMessage = message
|
||||
message.AssocMessage = resp
|
||||
|
||||
if resp.IsFinished() {
|
||||
defer t.dispatchMessage(resp)
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
if resp, ok := t.messages[message.ResponseID]; ok {
|
||||
resp.AssocMessage = message
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if message.AssocMessage == nil {
|
||||
if responseRequest, ok := t.respAliases[message.Ack]; ok {
|
||||
message.AssocMessage = responseRequest
|
||||
responseRequest.AssocMessage = message
|
||||
}
|
||||
}
|
||||
|
||||
delete(t.respAliases, message.Ack)
|
||||
delete(t.respWithoutReq, message.Ack)
|
||||
|
||||
// Do not track responses which have no associated requests
|
||||
if message.AssocMessage == nil {
|
||||
// log.Println("Can't dispatch resp", message.Seq, message.Ack, string(message.Bytes()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.messagesChan <- message
|
||||
}
|
||||
|
||||
// DeviceNotFoundError raised if user specified wrong ip
|
||||
type DeviceNotFoundError struct {
|
||||
addr string
|
||||
}
|
||||
|
||||
func (e *DeviceNotFoundError) Error() string {
|
||||
devices, _ := pcap.FindAllDevs()
|
||||
|
||||
if len(devices) == 0 {
|
||||
return "Can't get list of network interfaces, ensure that you running Gor as root user or sudo.\nTo run as non-root users see this docs https://github.com/buger/gor/wiki/Running-as-non-root-user"
|
||||
}
|
||||
|
||||
var msg string
|
||||
msg += "Can't find interfaces with addr: " + e.addr + ". Provide available IP for intercepting traffic: \n"
|
||||
for _, device := range devices {
|
||||
msg += "Name: " + device.Name + "\n"
|
||||
if device.Description != "" {
|
||||
msg += "Description: " + device.Description + "\n"
|
||||
}
|
||||
for _, address := range device.Addresses {
|
||||
msg += "- IP address: " + address.IP.String() + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func findPcapDevices(addr string) (interfaces []pcap.Interface, err error) {
|
||||
devices, err := pcap.FindAllDevs()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, device := range devices {
|
||||
if (addr == "" || addr == "0.0.0.0" || addr == "[::]" || addr == "::") && len(device.Addresses) > 0 {
|
||||
interfaces = append(interfaces, device)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, address := range device.Addresses {
|
||||
if device.Name == addr || address.IP.String() == addr {
|
||||
interfaces = append(interfaces, device)
|
||||
return interfaces, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(interfaces) == 0 {
|
||||
return nil, &DeviceNotFoundError{addr}
|
||||
} else {
|
||||
return interfaces, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Listener) readPcap() {
|
||||
devices, err := findPcapDevices(t.addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
bpfSupported := true
|
||||
if runtime.GOOS == "darwin" {
|
||||
bpfSupported = false
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(devices))
|
||||
|
||||
for _, d := range devices {
|
||||
go func(device pcap.Interface) {
|
||||
handle, err := pcap.OpenLive(device.Name, 65536, true, t.messageExpire)
|
||||
if err != nil {
|
||||
log.Println("Pcap Error while opening device", device.Name, err)
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
defer handle.Close()
|
||||
|
||||
t.mu.Lock()
|
||||
t.pcapHandles = append(t.pcapHandles, handle)
|
||||
|
||||
var bpfDstHost, bpfSrcHost string
|
||||
for i, addr := range device.Addresses {
|
||||
bpfDstHost += "dst host " + addr.IP.String()
|
||||
bpfSrcHost += "src host " + addr.IP.String()
|
||||
if i != len(device.Addresses) - 1 {
|
||||
bpfDstHost += " or "
|
||||
bpfSrcHost += " or "
|
||||
}
|
||||
}
|
||||
|
||||
if bpfSupported {
|
||||
var bpf string
|
||||
|
||||
if t.trackResponse {
|
||||
bpf = "(tcp dst port " + strconv.Itoa(int(t.port)) + " and (" + bpfDstHost + ")) or (" + "tcp src port " + strconv.Itoa(int(t.port)) + " and (" + bpfSrcHost + "))"
|
||||
} else {
|
||||
bpf = "tcp dst port " + strconv.Itoa(int(t.port)) + " and (" + bpfDstHost + ")"
|
||||
}
|
||||
|
||||
if err := handle.SetBPFFilter(bpf); err != nil {
|
||||
log.Println("BPF filter error:", err, "Device:", device.Name, bpf)
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
var decoder gopacket.Decoder
|
||||
|
||||
// Special case for tunnel interface https://github.com/google/gopacket/issues/99
|
||||
if handle.LinkType() == 12 {
|
||||
decoder = layers.LayerTypeIPv4
|
||||
} else {
|
||||
decoder = handle.LinkType()
|
||||
}
|
||||
|
||||
source := gopacket.NewPacketSource(handle, decoder)
|
||||
source.Lazy = true
|
||||
source.NoCopy = true
|
||||
|
||||
wg.Done()
|
||||
|
||||
var data, srcIP, dstIP []byte
|
||||
|
||||
for {
|
||||
packet, err := source.NextPacket()
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// We should remove network layer before parsing TCP/IP data
|
||||
var of int
|
||||
switch decoder {
|
||||
case layers.LinkTypeEthernet:
|
||||
of = 14
|
||||
case layers.LinkTypePPP:
|
||||
of = 1
|
||||
case layers.LinkTypeFDDI:
|
||||
of = 13
|
||||
case layers.LinkTypeNull:
|
||||
of = 4
|
||||
case layers.LinkTypeLoop:
|
||||
of = 4
|
||||
case layers.LinkTypeRaw:
|
||||
of = 0
|
||||
case layers.LinkTypeLinuxSLL:
|
||||
of = 16
|
||||
default:
|
||||
log.Println("Unknown packet layer", packet)
|
||||
break
|
||||
}
|
||||
|
||||
data = packet.Data()[of:]
|
||||
|
||||
version := uint8(data[0]) >> 4
|
||||
|
||||
if version == 4 {
|
||||
ihl := uint8(data[0]) & 0x0F
|
||||
|
||||
// Truncated IP info
|
||||
if len(data) < int(ihl*4) {
|
||||
continue
|
||||
}
|
||||
|
||||
srcIP = data[12:16]
|
||||
dstIP = data[16:20]
|
||||
data = data[ihl*4:]
|
||||
} else {
|
||||
// Truncated IP info
|
||||
if len(data) < 40 {
|
||||
continue
|
||||
}
|
||||
|
||||
srcIP = data[8:24]
|
||||
dstIP = data[24:40]
|
||||
|
||||
data = data[40:]
|
||||
}
|
||||
|
||||
// Truncated TCP info
|
||||
if len(data) < 13 {
|
||||
continue
|
||||
}
|
||||
|
||||
dataOffset := (data[12] & 0xF0) >> 4
|
||||
|
||||
// We need only packets with data inside
|
||||
// Check that the buffer is larger than the size of the TCP header
|
||||
if len(data) > int(dataOffset*4) {
|
||||
if !bpfSupported {
|
||||
destPort := binary.BigEndian.Uint16(data[2:4])
|
||||
srcPort := binary.BigEndian.Uint16(data[0:2])
|
||||
|
||||
var addrCheck []byte
|
||||
|
||||
if destPort == t.port {
|
||||
addrCheck = dstIP
|
||||
}
|
||||
|
||||
if t.trackResponse && srcPort == t.port {
|
||||
addrCheck = srcIP
|
||||
}
|
||||
|
||||
if len(addrCheck) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
addrMatched := false
|
||||
for _, a := range device.Addresses {
|
||||
if a.IP.Equal(net.IP(addrCheck)) {
|
||||
addrMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !addrMatched {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
newBuf := make([]byte, len(data)+16)
|
||||
copy(newBuf[:16], srcIP)
|
||||
copy(newBuf[16:], data)
|
||||
|
||||
t.packetsChan <- newBuf
|
||||
}
|
||||
}
|
||||
}(d)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
t.readyCh <- true
|
||||
}
|
||||
|
||||
func (t *Listener) readRAWSocket() {
|
||||
conn, e := net.ListenPacket("ip:tcp", t.addr)
|
||||
t.conn = conn
|
||||
|
||||
if e != nil {
|
||||
log.Fatal(e)
|
||||
}
|
||||
|
||||
defer t.conn.Close()
|
||||
|
||||
buf := make([]byte, 64*1024) // 64kb
|
||||
|
||||
t.readyCh <- true
|
||||
|
||||
for {
|
||||
// Note: ReadFrom receive messages without IP header
|
||||
n, addr, err := t.conn.ReadFrom(buf)
|
||||
|
||||
if err != nil {
|
||||
if strings.HasSuffix(err.Error(), "closed network connection") {
|
||||
return
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
if t.isValidPacket(buf[:n]) {
|
||||
newBuf := make([]byte, n+16)
|
||||
copy(newBuf[16:], buf[:n])
|
||||
copy(newBuf[:16], []byte(addr.(*net.IPAddr).IP))
|
||||
|
||||
t.packetsChan <- newBuf
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Listener) isValidPacket(buf []byte) bool {
|
||||
// To avoid full packet parsing every time, we manually parsing values needed for packet filtering
|
||||
// http://en.wikipedia.org/wiki/Transmission_Control_Protocol
|
||||
destPort := binary.BigEndian.Uint16(buf[2:4])
|
||||
srcPort := binary.BigEndian.Uint16(buf[0:2])
|
||||
|
||||
// Because RAW_SOCKET can't be bound to port, we have to control it by ourself
|
||||
if destPort == t.port || (t.trackResponse && srcPort == t.port) {
|
||||
// Get the 'data offset' (size of the TCP header in 32-bit words)
|
||||
dataOffset := (buf[12] & 0xF0) >> 4
|
||||
|
||||
// We need only packets with data inside
|
||||
// Check that the buffer is larger than the size of the TCP header
|
||||
if len(buf) > int(dataOffset*4) {
|
||||
// We should create new buffer because go slices is pointers. So buffer data shoud be immutable.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Trying to add packet to existing message or creating new message
|
||||
//
|
||||
// For TCP message unique id is Acknowledgment number (see tcp_packet.go)
|
||||
func (t *Listener) processTCPPacket(packet *TCPPacket) {
|
||||
// Don't exit on panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("PANIC: pkg:", r, packet, string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
// log.Println("Processing packet:", packet.Ack, packet.Seq, packet.ID)
|
||||
|
||||
var message *TCPMessage
|
||||
|
||||
isIncoming := packet.DestPort == t.port
|
||||
|
||||
// Seek for 100-expect chunks
|
||||
if parentAck, ok := t.seqWithData[packet.Seq]; ok {
|
||||
// In case if non-first data chunks comes first
|
||||
for _, m := range t.messages {
|
||||
if m.Ack == packet.Ack && bytes.Equal(m.packets[0].Addr, packet.Addr) {
|
||||
t.deleteMessage(m)
|
||||
|
||||
if m.AssocMessage != nil {
|
||||
m.AssocMessage.AssocMessage = nil
|
||||
}
|
||||
|
||||
for _, pkt := range m.packets {
|
||||
// log.Println("Updating ack", parentAck, pkt.Ack)
|
||||
pkt.UpdateAck(parentAck)
|
||||
// Re-queue this packets
|
||||
t.processTCPPacket(pkt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.ackAliases[packet.Ack] = parentAck
|
||||
packet.UpdateAck(parentAck)
|
||||
}
|
||||
|
||||
if alias, ok := t.ackAliases[packet.Ack]; ok {
|
||||
packet.UpdateAck(alias)
|
||||
}
|
||||
|
||||
var responseRequest *TCPMessage
|
||||
|
||||
if !isIncoming {
|
||||
responseRequest, _ = t.respAliases[packet.Ack]
|
||||
}
|
||||
|
||||
message, ok := t.messages[packet.ID]
|
||||
|
||||
if !ok {
|
||||
message = NewTCPMessage(packet.Seq, packet.Ack, isIncoming)
|
||||
t.messages[packet.ID] = message
|
||||
|
||||
if !isIncoming {
|
||||
if responseRequest != nil {
|
||||
message.AssocMessage = responseRequest
|
||||
responseRequest.AssocMessage = message
|
||||
} else {
|
||||
t.respWithoutReq[packet.Ack] = packet.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adding packet to message
|
||||
message.AddPacket(packet)
|
||||
|
||||
// Handling Expect: 100-continue requests
|
||||
if message.Is100Continue() {
|
||||
seq := packet.Seq + uint32(len(packet.Data))
|
||||
t.seqWithData[seq] = packet.Ack
|
||||
message.DataSeq = seq
|
||||
|
||||
// In case if sequence packet came first
|
||||
for _, m := range t.messages {
|
||||
if m.Seq == seq {
|
||||
t.deleteMessage(m)
|
||||
if m.AssocMessage != nil {
|
||||
message.AssocMessage = m.AssocMessage
|
||||
}
|
||||
// log.Println("2: Adding ack alias:", m.Ack, packet.Ack)
|
||||
t.ackAliases[m.Ack] = packet.Ack
|
||||
|
||||
for _, pkt := range m.packets {
|
||||
pkt.UpdateAck(packet.Ack)
|
||||
message.AddPacket(pkt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removing `Expect: 100-continue` header
|
||||
packet.Data = proto.DeleteHeader(packet.Data, bExpectHeader)
|
||||
}
|
||||
|
||||
// log.Println("Received message:", string(message.Bytes()), message.ID(), t.messages)
|
||||
|
||||
if isIncoming {
|
||||
// If message have multiple packets, delete previous alias
|
||||
if len(message.packets) > 1 {
|
||||
delete(t.respAliases, message.ResponseAck)
|
||||
}
|
||||
|
||||
message.UpdateResponseAck()
|
||||
t.respAliases[message.ResponseAck] = message
|
||||
}
|
||||
|
||||
// If message contains only single packet immediately dispatch it
|
||||
if message.IsFinished() {
|
||||
if isIncoming {
|
||||
// log.Println("I'm finished", string(message.Bytes()), message.ResponseID, t.messages)
|
||||
if t.trackResponse {
|
||||
if resp, ok := t.messages[message.ResponseID]; ok {
|
||||
t.dispatchMessage(message)
|
||||
if resp.IsFinished() {
|
||||
t.dispatchMessage(resp)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.dispatchMessage(message)
|
||||
}
|
||||
} else {
|
||||
if message.AssocMessage == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req, ok := t.messages[message.AssocMessage.ID()]; ok {
|
||||
if req.IsFinished() {
|
||||
t.dispatchMessage(req)
|
||||
t.dispatchMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Listener) IsReady() bool {
|
||||
select {
|
||||
case <-t.readyCh:
|
||||
return true
|
||||
case <-time.After(5 * time.Second):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Receive TCP messages from the listener channel
|
||||
func (t *Listener) Receiver() chan *TCPMessage {
|
||||
return t.messagesChan
|
||||
}
|
||||
|
||||
func (t *Listener) Close() {
|
||||
close(t.quit)
|
||||
if t.conn != nil {
|
||||
t.conn.Close()
|
||||
}
|
||||
|
||||
for _, h := range t.pcapHandles {
|
||||
h.Close()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
package rawSocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRawListenerInput(t *testing.T) {
|
||||
var req, resp *TCPMessage
|
||||
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket := buildPacket(true, 1, 1, []byte("GET / HTTP/1.1\r\n\r\n"))
|
||||
|
||||
respAck := reqPacket.Seq + uint32(len(reqPacket.Data))
|
||||
respPacket := buildPacket(false, respAck, reqPacket.Seq+1, []byte("HTTP/1.1 200 OK\r\n\r\n"))
|
||||
|
||||
listener.packetsChan <- reqPacket.Dump()
|
||||
listener.packetsChan <- respPacket.Dump()
|
||||
|
||||
select {
|
||||
case req = <-listener.messagesChan:
|
||||
case <-time.After(time.Millisecond):
|
||||
t.Error("Should return request immediately")
|
||||
return
|
||||
}
|
||||
|
||||
if !req.IsIncoming {
|
||||
t.Error("Should be request")
|
||||
}
|
||||
|
||||
select {
|
||||
case resp = <-listener.messagesChan:
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
t.Error("Should return response immediately")
|
||||
return
|
||||
}
|
||||
|
||||
if resp.IsIncoming {
|
||||
t.Error("Should be response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawListenerInputWithoutResponse(t *testing.T) {
|
||||
var req *TCPMessage
|
||||
|
||||
listener := NewListener("", "0", EnginePcap, false, 10*time.Millisecond)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket := buildPacket(true, 1, 1, []byte("GET / HTTP/1.1\r\n\r\n"))
|
||||
|
||||
listener.packetsChan <- reqPacket.Dump()
|
||||
|
||||
select {
|
||||
case req = <-listener.messagesChan:
|
||||
case <-time.After(time.Millisecond):
|
||||
t.Error("Should return request immediately")
|
||||
return
|
||||
}
|
||||
|
||||
if !req.IsIncoming {
|
||||
t.Error("Should be request")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawListenerResponse(t *testing.T) {
|
||||
var req, resp *TCPMessage
|
||||
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket := buildPacket(true, 1, 1, []byte("GET / HTTP/1.1\r\n\r\n"))
|
||||
respPacket := buildPacket(false, 1+uint32(len(reqPacket.Data)), 2, []byte("HTTP/1.1 200 OK\r\n\r\n"))
|
||||
|
||||
// If response packet comes before request
|
||||
listener.packetsChan <- respPacket.Dump()
|
||||
listener.packetsChan <- reqPacket.Dump()
|
||||
|
||||
select {
|
||||
case req = <-listener.messagesChan:
|
||||
case <-time.After(time.Millisecond):
|
||||
t.Error("Should return respose immediately")
|
||||
return
|
||||
}
|
||||
|
||||
if !req.IsIncoming {
|
||||
t.Error("Should be request")
|
||||
}
|
||||
|
||||
select {
|
||||
case resp = <-listener.messagesChan:
|
||||
case <-time.After(time.Millisecond):
|
||||
t.Error("Should return response immediately")
|
||||
return
|
||||
}
|
||||
|
||||
if resp.IsIncoming {
|
||||
t.Error("Should be response")
|
||||
}
|
||||
|
||||
if !bytes.Equal(resp.UUID(), req.UUID()) {
|
||||
t.Error("Resp and Req UUID should be equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShort100Continue(t *testing.T) {
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket1 := buildPacket(true, 1, 1, []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\nExpect: 100-continue\r\n\r\n"))
|
||||
// Packet with data have different Seq
|
||||
reqPacket2 := buildPacket(true, 2, reqPacket1.Seq+uint32(len(reqPacket1.Data)), []byte("a"))
|
||||
reqPacket3 := buildPacket(true, 2, reqPacket2.Seq+1, []byte("b"))
|
||||
|
||||
respPacket1 := buildPacket(false, 10, 3, []byte("HTTP/1.1 100 Continue\r\n"))
|
||||
|
||||
// panic(int(uint32(len(reqPacket1.Data)) + uint32(len(reqPacket2.Data)) + uint32(len(reqPacket3.Data))))
|
||||
respPacket2 := buildPacket(false, reqPacket3.Seq+1 /* len of data */, 2, []byte("HTTP/1.1 200 OK\r\n"))
|
||||
|
||||
result := []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\nab")
|
||||
|
||||
testRawListener100Continue(t, listener, result, reqPacket1, reqPacket2, reqPacket3, respPacket1, respPacket2)
|
||||
}
|
||||
|
||||
// Response comes before Request
|
||||
func Test100ContinueWrongOrder(t *testing.T) {
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket1 := buildPacket(true, 1, 1, []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\nExpect: 100-continue\r\n\r\n"))
|
||||
// Packet with data have different Seq
|
||||
reqPacket2 := buildPacket(true, 2, reqPacket1.Seq+uint32(len(reqPacket1.Data)), []byte("a"))
|
||||
reqPacket3 := buildPacket(true, 2, reqPacket2.Seq+1, []byte("b"))
|
||||
|
||||
respPacket1 := buildPacket(false, 10, 3, []byte("HTTP/1.1 100 Continue\r\n"))
|
||||
|
||||
// panic(int(uint32(len(reqPacket1.Data)) + uint32(len(reqPacket2.Data)) + uint32(len(reqPacket3.Data))))
|
||||
respPacket2 := buildPacket(false, reqPacket3.Seq+1 /* len of data */, 2, []byte("HTTP/1.1 200 OK\r\n"))
|
||||
|
||||
result := []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\nab")
|
||||
|
||||
testRawListener100Continue(t, listener, result, respPacket1, respPacket2, reqPacket1, reqPacket2, reqPacket3)
|
||||
}
|
||||
|
||||
func TestAlt100ContinueHeaderOrder(t *testing.T) {
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket1 := buildPacket(true, 1, 1, []byte("POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 2\r\n\r\n"))
|
||||
// Packet with data have different Seq
|
||||
reqPacket2 := buildPacket(true, 2, reqPacket1.Seq+uint32(len(reqPacket1.Data)), []byte("a"))
|
||||
reqPacket3 := buildPacket(true, 2, reqPacket2.Seq+1, []byte("b"))
|
||||
|
||||
respPacket1 := buildPacket(false, 10, 3, []byte("HTTP/1.1 100 Continue\r\n"))
|
||||
|
||||
// panic(int(uint32(len(reqPacket1.Data)) + uint32(len(reqPacket2.Data)) + uint32(len(reqPacket3.Data))))
|
||||
respPacket2 := buildPacket(false, reqPacket3.Seq+1 /* len of data */, 2, []byte("HTTP/1.1 200 OK\r\n"))
|
||||
|
||||
result := []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\nab")
|
||||
|
||||
testRawListener100Continue(t, listener, result, reqPacket1, reqPacket2, reqPacket3, respPacket1, respPacket2)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func testRawListener100Continue(t *testing.T, listener *Listener, result []byte, packets ...*TCPPacket) {
|
||||
var req, resp *TCPMessage
|
||||
for _, p := range packets {
|
||||
listener.packetsChan <- p.Dump()
|
||||
}
|
||||
|
||||
select {
|
||||
case req = <-listener.messagesChan:
|
||||
break
|
||||
case <-time.After(11 * time.Millisecond):
|
||||
t.Error("Should return response after expire time")
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(req.Bytes(), result) {
|
||||
t.Error("Should receive full message", string(req.Bytes()))
|
||||
}
|
||||
|
||||
if !req.IsIncoming {
|
||||
t.Error("Should be request")
|
||||
}
|
||||
|
||||
select {
|
||||
case resp = <-listener.messagesChan:
|
||||
break
|
||||
case <-time.After(21 * time.Millisecond):
|
||||
t.Error("Should return response after expire time")
|
||||
return
|
||||
}
|
||||
|
||||
if resp.IsIncoming {
|
||||
t.Error("Should be response")
|
||||
}
|
||||
|
||||
if !bytes.Equal(resp.UUID(), req.UUID()) {
|
||||
t.Error("Resp and Req UUID should be equal")
|
||||
}
|
||||
}
|
||||
|
||||
func testChunkedSequence(t *testing.T, listener *Listener, packets ...*TCPPacket) {
|
||||
var r, req, resp *TCPMessage
|
||||
|
||||
for _, p := range packets {
|
||||
listener.packetsChan <- p.Dump()
|
||||
}
|
||||
|
||||
select {
|
||||
case r = <-listener.messagesChan:
|
||||
if r.IsIncoming {
|
||||
req = r
|
||||
} else {
|
||||
resp = r
|
||||
}
|
||||
break
|
||||
case <-time.After(25 * time.Millisecond):
|
||||
t.Error("Should return request after expire time")
|
||||
return
|
||||
}
|
||||
select {
|
||||
case r = <-listener.messagesChan:
|
||||
if r.IsIncoming {
|
||||
if req != nil {
|
||||
t.Error("Request already received", r)
|
||||
return
|
||||
}
|
||||
req = r
|
||||
} else {
|
||||
if resp != nil {
|
||||
t.Error("Response already received", r)
|
||||
return
|
||||
}
|
||||
resp = r
|
||||
}
|
||||
break
|
||||
case <-time.After(25 * time.Millisecond):
|
||||
t.Error("Should return request after expire time")
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(req.Bytes(), []byte("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n1\r\na\r\n1\r\nb\r\n0\r\n\r\n")) {
|
||||
t.Error("Should receive full message", string(req.Bytes()))
|
||||
}
|
||||
|
||||
if !req.IsIncoming {
|
||||
t.Error("Should be request")
|
||||
}
|
||||
|
||||
if resp.IsIncoming {
|
||||
t.Error("Should be response")
|
||||
}
|
||||
|
||||
if !bytes.Equal(resp.UUID(), req.UUID()) {
|
||||
t.Error("Resp and Req UUID should be equal", string(resp.UUID()), string(req.UUID()))
|
||||
}
|
||||
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
if len(listener.packetsChan) != 0 {
|
||||
t.Fatal("packetsChan non empty:", listener.packetsChan)
|
||||
}
|
||||
|
||||
if len(listener.messagesChan) != 0 {
|
||||
t.Fatal("messagesChan non empty:", <-listener.messagesChan)
|
||||
}
|
||||
|
||||
if len(listener.messages) != 0 {
|
||||
t.Fatal("Messages non empty:", listener.messages)
|
||||
}
|
||||
|
||||
if len(listener.ackAliases) != 0 {
|
||||
t.Fatal("ackAliases non empty:", listener.ackAliases)
|
||||
}
|
||||
|
||||
if len(listener.seqWithData) != 0 {
|
||||
t.Fatal("seqWithData non empty:", listener.seqWithData)
|
||||
}
|
||||
|
||||
if len(listener.respAliases) != 0 {
|
||||
t.Fatal("respAliases non empty:", listener.respAliases)
|
||||
}
|
||||
|
||||
if len(listener.respWithoutReq) != 0 {
|
||||
t.Fatal("respWithoutReq non empty:", listener.respWithoutReq)
|
||||
}
|
||||
}
|
||||
|
||||
func permutation(n int, list []*TCPPacket) []*TCPPacket {
|
||||
if len(list) == 1 {
|
||||
return list
|
||||
}
|
||||
|
||||
k := n % len(list)
|
||||
|
||||
first := []*TCPPacket{list[k]}
|
||||
next := make([]*TCPPacket, len(list)-1)
|
||||
|
||||
copy(next, append(list[:k], list[k+1:]...))
|
||||
|
||||
return append(first, permutation(n/len(list), next)...)
|
||||
}
|
||||
|
||||
// Response comes before Request
|
||||
func TestRawListenerChunkedWrongOrder(t *testing.T) {
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket1 := buildPacket(true, 1, 1, []byte("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nExpect: 100-continue\r\n\r\n"))
|
||||
// Packet with data have different Seq
|
||||
reqPacket2 := buildPacket(true, 2, reqPacket1.Seq+uint32(len(reqPacket1.Data)), []byte("1\r\na\r\n"))
|
||||
reqPacket3 := buildPacket(true, 2, reqPacket2.Seq+uint32(len(reqPacket2.Data)), []byte("1\r\nb\r\n"))
|
||||
reqPacket4 := buildPacket(true, 2, reqPacket3.Seq+uint32(len(reqPacket3.Data)), []byte("0\r\n\r\n"))
|
||||
|
||||
respPacket1 := buildPacket(false, 10, 3, []byte("HTTP/1.1 100 Continue\r\n"))
|
||||
|
||||
// panic(int(uint32(len(reqPacket1.Data)) + uint32(len(reqPacket2.Data)) + uint32(len(reqPacket3.Data))))
|
||||
respPacket2 := buildPacket(false, reqPacket4.Seq+5 /* len of data */, 2, []byte("HTTP/1.1 200 OK\r\n"))
|
||||
|
||||
// Should re-construct message from all possible combinations
|
||||
for i := 0; i < 6*5*4*3*2*1; i++ {
|
||||
|
||||
if i < 54 || i > 57 {
|
||||
continue
|
||||
}
|
||||
|
||||
packets := permutation(i, []*TCPPacket{reqPacket1, reqPacket2, reqPacket3, reqPacket4, respPacket1, respPacket2})
|
||||
|
||||
t.Log("permutation:", i)
|
||||
testChunkedSequence(t, listener, packets...)
|
||||
}
|
||||
}
|
||||
|
||||
func chunkedPostMessage() []*TCPPacket {
|
||||
ack := uint32(rand.Int63())
|
||||
seq := uint32(rand.Int63())
|
||||
|
||||
reqPacket1 := buildPacket(true, ack, seq, []byte("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"))
|
||||
// Packet with data have different Seq
|
||||
reqPacket2 := buildPacket(true, ack, seq+47, []byte("1\r\na\r\n"))
|
||||
reqPacket3 := buildPacket(true, ack, reqPacket2.Seq+5, []byte("1\r\nb\r\n"))
|
||||
reqPacket4 := buildPacket(true, ack, reqPacket3.Seq+5, []byte("0\r\n\r\n"))
|
||||
|
||||
respPacket := buildPacket(false, reqPacket4.Seq+5 /* len of data */, ack, []byte("HTTP/1.1 200 OK\r\n"))
|
||||
|
||||
return []*TCPPacket{
|
||||
reqPacket1, reqPacket2, reqPacket3, reqPacket4, respPacket,
|
||||
}
|
||||
}
|
||||
|
||||
func postMessage() []*TCPPacket {
|
||||
ack := uint32(rand.Int63())
|
||||
seq2 := uint32(rand.Int63())
|
||||
seq := uint32(rand.Int63())
|
||||
|
||||
c := 10000
|
||||
data := make([]byte, c)
|
||||
rand.Read(data)
|
||||
|
||||
head := []byte("POST / HTTP/1.1\r\nContent-Length: 9958\r\n\r\n")
|
||||
for i := range head {
|
||||
data[i] = head[i]
|
||||
}
|
||||
|
||||
return []*TCPPacket{
|
||||
buildPacket(true, ack, seq, data),
|
||||
buildPacket(false, seq+uint32(len(data)), seq2, []byte("HTTP/1.1 200 OK\r\n")),
|
||||
}
|
||||
}
|
||||
|
||||
func getMessage() []*TCPPacket {
|
||||
ack := uint32(rand.Int63())
|
||||
seq2 := uint32(rand.Int63())
|
||||
seq := uint32(rand.Int63())
|
||||
|
||||
return []*TCPPacket{
|
||||
buildPacket(true, ack, seq, []byte("GET / HTTP/1.1\r\n\r\n")),
|
||||
buildPacket(false, seq+18, seq2, []byte("HTTP/1.1 200 OK\r\n")),
|
||||
}
|
||||
}
|
||||
|
||||
// Response comes before Request
|
||||
func TestRawListenerBench(t *testing.T) {
|
||||
l := NewListener("", "0", EnginePcap, true, 200*time.Millisecond)
|
||||
defer l.Close()
|
||||
|
||||
// Should re-construct message from all possible combinations
|
||||
for i := 0; i < 1000; i++ {
|
||||
go func(i int) {
|
||||
for j := 0; j < 100; j++ {
|
||||
var packets []*TCPPacket
|
||||
|
||||
if j%5 == 0 {
|
||||
packets = chunkedPostMessage()
|
||||
} else if j%3 == 0 {
|
||||
packets = postMessage()
|
||||
} else {
|
||||
packets = getMessage()
|
||||
}
|
||||
|
||||
for _, p := range packets {
|
||||
// Randomly drop packets
|
||||
if (i+j)%5 == 0 {
|
||||
if rand.Int63()%3 == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
l.packetsChan <- p.Dump()
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
ch := l.Receiver()
|
||||
|
||||
var count int32
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ch:
|
||||
atomic.AddInt32(&count, 1)
|
||||
case <-time.After(2000 * time.Millisecond):
|
||||
log.Println("Emitted 200000 messages, captured: ", count, len(l.ackAliases), len(l.seqWithData), len(l.respAliases), len(l.respWithoutReq), len(l.packetsChan))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package rawSocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"github.com/buger/gor/proto"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
"net"
|
||||
)
|
||||
|
||||
var _ = log.Println
|
||||
|
||||
// TCPMessage ensure that all TCP packets for given request is received, and processed in right sequence
|
||||
// Its needed because all TCP message can be fragmented or re-transmitted
|
||||
//
|
||||
// Each TCP Packet have 2 ids: acknowledgment - message_id, and sequence - packet_id
|
||||
// Message can be compiled from unique packets with same message_id which sorted by sequence
|
||||
// Message is received if we didn't receive any packets for 2000ms
|
||||
type TCPMessage struct {
|
||||
Seq uint32
|
||||
Ack uint32
|
||||
ResponseAck uint32
|
||||
ResponseID tcpID
|
||||
DataAck uint32
|
||||
DataSeq uint32
|
||||
|
||||
AssocMessage *TCPMessage
|
||||
Start time.Time
|
||||
End time.Time
|
||||
IsIncoming bool
|
||||
|
||||
packets []*TCPPacket
|
||||
|
||||
delChan chan *TCPMessage
|
||||
}
|
||||
|
||||
// NewTCPMessage pointer created from a Acknowledgment number and a channel of messages readuy to be deleted
|
||||
func NewTCPMessage(Seq, Ack uint32, IsIncoming bool) (msg *TCPMessage) {
|
||||
msg = &TCPMessage{Seq: Seq, Ack: Ack, IsIncoming: IsIncoming}
|
||||
msg.Start = time.Now()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Bytes return message content
|
||||
func (t *TCPMessage) Bytes() (output []byte) {
|
||||
for _, p := range t.packets {
|
||||
output = append(output, p.Data...)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// Size returns total body size
|
||||
func (t *TCPMessage) BodySize() (size int) {
|
||||
if len(t.packets) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
size += len(proto.Body(t.packets[0].Data))
|
||||
|
||||
for _, p := range t.packets[1:] {
|
||||
size += len(p.Data)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Size returns total size of message
|
||||
func (t *TCPMessage) Size() (size int) {
|
||||
if len(t.packets) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
for _, p := range t.packets {
|
||||
size += len(p.Data)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AddPacket to the message and ensure packet uniqueness
|
||||
// TCP allows that packet can be re-send multiple times
|
||||
func (t *TCPMessage) AddPacket(packet *TCPPacket) {
|
||||
packetFound := false
|
||||
|
||||
for _, pkt := range t.packets {
|
||||
if packet.Seq == pkt.Seq {
|
||||
packetFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !packetFound {
|
||||
// Packets not always captured in same Seq order, and sometimes we need to prepend
|
||||
if len(t.packets) == 0 || packet.Seq > t.packets[len(t.packets)-1].Seq {
|
||||
t.packets = append(t.packets, packet)
|
||||
} else if packet.Seq < t.packets[0].Seq {
|
||||
t.packets = append([]*TCPPacket{packet}, t.packets...)
|
||||
t.Seq = packet.Seq // Message Seq should indicated starting seq
|
||||
} else { // insert somewhere in the middle...
|
||||
for i, p := range t.packets {
|
||||
if packet.Seq < p.Seq {
|
||||
t.packets = append(t.packets[:i], append([]*TCPPacket{packet}, t.packets[i:]...)...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if t.IsIncoming {
|
||||
t.End = time.Now()
|
||||
} else {
|
||||
t.End = time.Now().Add(time.Millisecond)
|
||||
}
|
||||
|
||||
if packet.OrigAck != 0 {
|
||||
t.DataAck = packet.OrigAck
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is missing packet
|
||||
func (t *TCPMessage) isSeqMissing() bool {
|
||||
if len(t.packets) == 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, p := range t.packets {
|
||||
// If final packet
|
||||
if len(t.packets) == i+1 {
|
||||
return false
|
||||
}
|
||||
np := t.packets[i+1]
|
||||
|
||||
if np.Seq != p.Seq+uint32(len(p.Data)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var EmptyLine = []byte("\r\n\r\n")
|
||||
var ChunkEnd = []byte("0\r\n\r\n")
|
||||
|
||||
func (t *TCPMessage) isHeadersReceived() bool {
|
||||
for _, p := range t.packets {
|
||||
if bytes.LastIndex(p.Data, EmptyLine) != -1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isMultipart returns true if message contains from multiple tcp packets
|
||||
func (t *TCPMessage) IsFinished() bool {
|
||||
payload := t.packets[0].Data
|
||||
|
||||
if len(payload) < 4 {
|
||||
return true
|
||||
}
|
||||
|
||||
m := payload[:4]
|
||||
|
||||
if t.IsIncoming {
|
||||
// If one GET, OPTIONS, or HEAD request
|
||||
if bytes.Equal(m, []byte("GET ")) || bytes.Equal(m, []byte("OPTI")) || bytes.Equal(m, []byte("HEAD")) {
|
||||
if !t.isSeqMissing() && t.isHeadersReceived() {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// Sometimes header comes after the body :(
|
||||
if bytes.Equal(m, []byte("POST")) || bytes.Equal(m, []byte("PUT ")) || bytes.Equal(m, []byte("PATC")) {
|
||||
|
||||
if t.isHeadersReceived() {
|
||||
if length := proto.Header(payload, []byte("Content-Length")); len(length) > 0 {
|
||||
l, _ := strconv.Atoi(string(length))
|
||||
|
||||
// If content-length equal current body length
|
||||
if l > 0 && l == t.BodySize() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Request not found
|
||||
// Can be because response came first or request request was just missing
|
||||
if t.AssocMessage == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !bytes.Equal(m, []byte("HTTP")) {
|
||||
return false
|
||||
}
|
||||
|
||||
if length := proto.Header(payload, []byte("Content-Length")); len(length) > 0 {
|
||||
if length[0] == '0' {
|
||||
return true
|
||||
}
|
||||
|
||||
l, _ := strconv.Atoi(string(length))
|
||||
|
||||
// If content-length equal current body length
|
||||
if l > 0 && l == t.BodySize() {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if enc := proto.Header(payload, []byte("Transfer-Encoding")); len(enc) == 0 {
|
||||
return true
|
||||
} else {
|
||||
if len(t.packets) > 1 && bytes.LastIndex(t.packets[len(t.packets)-1].Data, ChunkEnd) != -1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var bExpectHeader = []byte("Expect:")
|
||||
var bExpect100Value = []byte("100-continue")
|
||||
var bPOST = []byte("POST")
|
||||
var bCRLFx2 = []byte("\r\n\r\n")
|
||||
|
||||
func (t *TCPMessage) Is100Continue() bool {
|
||||
d := t.packets[0].Data
|
||||
|
||||
if len(d) < 25 {
|
||||
return false
|
||||
}
|
||||
|
||||
if !bytes.Equal(d[0:4], bPOST) {
|
||||
return false
|
||||
}
|
||||
|
||||
// reading last 4 bytes for double CRLF
|
||||
if !bytes.Equal(d[len(d)-4:], bCRLFx2) {
|
||||
return false
|
||||
}
|
||||
|
||||
// look for an expect:100-continue header
|
||||
if !bytes.Equal(bExpect100Value, proto.Header(d, bExpectHeader)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *TCPMessage) UUID() []byte {
|
||||
var key []byte
|
||||
|
||||
if t.IsIncoming {
|
||||
// log.Println("UUID:", t.Ack, t.Start.UnixNano())
|
||||
key = strconv.AppendInt(key, t.Start.UnixNano(), 10)
|
||||
key = strconv.AppendUint(key, uint64(t.Ack), 10)
|
||||
} else {
|
||||
// log.Println("RequestMessage:", t.AssocMessage.Ack, t.AssocMessage.Start.UnixNano())
|
||||
key = strconv.AppendInt(key, t.AssocMessage.Start.UnixNano(), 10)
|
||||
key = strconv.AppendUint(key, uint64(t.AssocMessage.Ack), 10)
|
||||
}
|
||||
|
||||
uuid := make([]byte, 40)
|
||||
sha := sha1.Sum(key)
|
||||
hex.Encode(uuid, sha[:20])
|
||||
|
||||
return uuid
|
||||
}
|
||||
|
||||
// UpdateResponseAck should be called after packet is added
|
||||
func (t *TCPMessage) UpdateResponseAck() uint32 {
|
||||
lastPacket := t.packets[len(t.packets)-1]
|
||||
respAck := lastPacket.Seq + uint32(len(lastPacket.Data))
|
||||
|
||||
if t.ResponseAck != respAck {
|
||||
t.ResponseAck = lastPacket.Seq + uint32(len(lastPacket.Data))
|
||||
|
||||
// We swappwed src and dst port
|
||||
copy(t.ResponseID[:16], lastPacket.Addr)
|
||||
copy(t.ResponseID[16:], lastPacket.Raw[2:4]) // Src port
|
||||
copy(t.ResponseID[18:], lastPacket.Raw[0:2]) // Dest port
|
||||
binary.BigEndian.PutUint32(t.ResponseID[20:24], t.ResponseAck)
|
||||
}
|
||||
|
||||
return t.ResponseAck
|
||||
}
|
||||
|
||||
func (t *TCPMessage) ID() tcpID {
|
||||
return t.packets[0].ID
|
||||
}
|
||||
|
||||
func (t *TCPMessage) IP() net.IP {
|
||||
return net.IP(t.packets[0].Addr)
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package rawSocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
_ "log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func buildPacket(isIncoming bool, Ack, Seq uint32, Data []byte) (packet *TCPPacket) {
|
||||
var srcPort, destPort uint16
|
||||
|
||||
// For tests `listening` port is 0
|
||||
if isIncoming {
|
||||
srcPort = 1
|
||||
} else {
|
||||
destPort = 1
|
||||
}
|
||||
|
||||
buf := make([]byte, 16)
|
||||
binary.BigEndian.PutUint16(buf[2:4], destPort)
|
||||
binary.BigEndian.PutUint16(buf[0:2], srcPort)
|
||||
binary.BigEndian.PutUint32(buf[4:8], Seq)
|
||||
binary.BigEndian.PutUint32(buf[8:12], Ack)
|
||||
buf[12] = 64
|
||||
buf = append(buf, Data...)
|
||||
|
||||
packet = ParseTCPPacket([]byte("123"), buf)
|
||||
|
||||
return packet
|
||||
}
|
||||
|
||||
func buildMessage(p *TCPPacket) *TCPMessage {
|
||||
isIncoming := false
|
||||
if p.SrcPort == 1 {
|
||||
isIncoming = true
|
||||
}
|
||||
|
||||
m := NewTCPMessage(p.Seq, p.Ack, isIncoming)
|
||||
m.AddPacket(p)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func TestTCPMessagePacketsOrder(t *testing.T) {
|
||||
msg := buildMessage(buildPacket(true, 1, 1, []byte("a")))
|
||||
msg.AddPacket(buildPacket(true, 1, 2, []byte("b")))
|
||||
|
||||
if !bytes.Equal(msg.Bytes(), []byte("ab")) {
|
||||
t.Error("Should contatenate packets in right order")
|
||||
}
|
||||
|
||||
// When first packet have wrong order (Seq)
|
||||
msg = buildMessage(buildPacket(true, 1, 2, []byte("b")))
|
||||
msg.AddPacket(buildPacket(true, 1, 1, []byte("a")))
|
||||
|
||||
if !bytes.Equal(msg.Bytes(), []byte("ab")) {
|
||||
t.Error("Should contatenate packets in right order")
|
||||
}
|
||||
|
||||
// Should ignore packets with same sequence
|
||||
msg = buildMessage(buildPacket(true, 1, 1, []byte("a")))
|
||||
msg.AddPacket(buildPacket(true, 1, 1, []byte("a")))
|
||||
|
||||
if !bytes.Equal(msg.Bytes(), []byte("a")) {
|
||||
t.Error("Should ignore packet with same Seq")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPMessageSize(t *testing.T) {
|
||||
msg := buildMessage(buildPacket(true, 1, 1, []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\na")))
|
||||
msg.AddPacket(buildPacket(true, 1, 2, []byte("b")))
|
||||
|
||||
if msg.BodySize() != 2 {
|
||||
t.Error("Should count only body", msg.BodySize())
|
||||
}
|
||||
|
||||
if msg.Size() != 40 {
|
||||
t.Error("Should count all sizes", msg.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPMessageIsFinished(t *testing.T) {
|
||||
methodsWithoutBodies := []string{"GET", "OPTIONS", "HEAD"}
|
||||
|
||||
for _, m := range methodsWithoutBodies {
|
||||
msg := buildMessage(buildPacket(true, 1, 1, []byte(m+" / HTTP/1.1\r\n\r\n")))
|
||||
|
||||
if !msg.IsFinished() {
|
||||
t.Error(m, " request should be finished")
|
||||
}
|
||||
}
|
||||
|
||||
methodsWithBodies := []string{"POST", "PUT", "PATCH"}
|
||||
|
||||
for _, m := range methodsWithBodies {
|
||||
msg := buildMessage(buildPacket(true, 1, 1, []byte(m+" / HTTP/1.1\r\nContent-Length: 1\r\n\r\na")))
|
||||
|
||||
if !msg.IsFinished() {
|
||||
t.Error(m, " should be finished as body length == content length")
|
||||
}
|
||||
|
||||
msg = buildMessage(buildPacket(true, 1, 1, []byte(m+" / HTTP/1.1\r\nContent-Length: 2\r\n\r\na")))
|
||||
|
||||
if msg.IsFinished() {
|
||||
t.Error(m, " should not be finished as body length != content length")
|
||||
}
|
||||
}
|
||||
|
||||
msg := buildMessage(buildPacket(true, 1, 1, []byte("UNKNOWN / HTTP/1.1\r\n\r\n")))
|
||||
if msg.IsFinished() {
|
||||
t.Error("non http or wrong methods considered as not finished")
|
||||
}
|
||||
|
||||
// Responses
|
||||
msg = buildMessage(buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\n\r\n")))
|
||||
msg.AssocMessage = &TCPMessage{}
|
||||
if !msg.IsFinished() {
|
||||
t.Error("Should mark simple response as finished")
|
||||
}
|
||||
|
||||
msg = buildMessage(buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\n\r\n")))
|
||||
msg.AssocMessage = nil
|
||||
if msg.IsFinished() {
|
||||
t.Error("Should not mark responses without associated requests")
|
||||
}
|
||||
|
||||
msg = buildMessage(buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n")))
|
||||
msg.AssocMessage = &TCPMessage{}
|
||||
|
||||
if msg.IsFinished() {
|
||||
t.Error("Should mark chunked response as non finished")
|
||||
}
|
||||
|
||||
msg = buildMessage(buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")))
|
||||
msg.AssocMessage = &TCPMessage{}
|
||||
|
||||
if !msg.IsFinished() {
|
||||
t.Error("Should mark Content-Length: 0 respones as finished")
|
||||
}
|
||||
|
||||
msg = buildMessage(buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\nContent-Length: 1\r\n\r\na")))
|
||||
msg.AssocMessage = &TCPMessage{}
|
||||
|
||||
if !msg.IsFinished() {
|
||||
t.Error("Should mark valid Content-Length respones as finished")
|
||||
}
|
||||
|
||||
msg = buildMessage(buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\na")))
|
||||
msg.AssocMessage = &TCPMessage{}
|
||||
|
||||
if msg.IsFinished() {
|
||||
t.Error("Should not mark not valid Content-Length respones as finished")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPMessageIsSeqMissing(t *testing.T) {
|
||||
p1 := buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\n"))
|
||||
p2 := buildPacket(false, 1, p1.Seq+uint32(len(p1.Data)), []byte("Content-Length: 10\r\n\r\n"))
|
||||
p3 := buildPacket(false, 1, p2.Seq+uint32(len(p2.Data)), []byte("a"))
|
||||
|
||||
msg := buildMessage(p1)
|
||||
if msg.isSeqMissing() {
|
||||
t.Error("Should be complete if have only 1 packet")
|
||||
}
|
||||
|
||||
msg.AddPacket(p3)
|
||||
if !msg.isSeqMissing() {
|
||||
t.Error("Should be incomplete because missing middle component")
|
||||
}
|
||||
|
||||
msg.AddPacket(p2)
|
||||
if msg.isSeqMissing() {
|
||||
t.Error("Should be complete once missing packet added")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPMessageIsHeadersReceived(t *testing.T) {
|
||||
p1 := buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\n"))
|
||||
p2 := buildPacket(false, 1, p1.Seq+uint32(len(p1.Data)), []byte("Content-Length: 10\r\n\r\n"))
|
||||
|
||||
msg := buildMessage(p1)
|
||||
if msg.isHeadersReceived() {
|
||||
t.Error("Should be complete if have only 1 packet")
|
||||
}
|
||||
|
||||
msg.AddPacket(p2)
|
||||
if !msg.isHeadersReceived() {
|
||||
t.Error("Should found double new line: headers received")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package rawSocket
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = log.Println
|
||||
|
||||
// TCP Flags
|
||||
const (
|
||||
fFIN = 1 << iota
|
||||
fSYN
|
||||
fRST
|
||||
fPSH
|
||||
fACK
|
||||
fURG
|
||||
fECE
|
||||
fCWR
|
||||
fNS
|
||||
)
|
||||
|
||||
type tcpID [24]byte
|
||||
|
||||
// TCPPacket provides tcp packet parser
|
||||
// Packet structure: http://en.wikipedia.org/wiki/Transmission_Control_Protocol
|
||||
type TCPPacket struct {
|
||||
SrcPort uint16
|
||||
DestPort uint16
|
||||
Seq uint32
|
||||
Ack uint32
|
||||
OrigAck uint32
|
||||
DataOffset uint8
|
||||
|
||||
Raw []byte
|
||||
Data []byte
|
||||
Addr []byte
|
||||
ID tcpID
|
||||
}
|
||||
|
||||
// ParseTCPPacket takes address and tcp payload and returns parsed TCPPacket
|
||||
func ParseTCPPacket(addr []byte, data []byte) (p *TCPPacket) {
|
||||
p = &TCPPacket{Raw: data}
|
||||
p.ParseBasic()
|
||||
p.Addr = addr
|
||||
p.GenID()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *TCPPacket) GenID() {
|
||||
copy(p.ID[:16], p.Addr)
|
||||
copy(p.ID[16:], p.Raw[0:2]) // Src port
|
||||
copy(p.ID[18:], p.Raw[2:4]) // Dest port
|
||||
copy(p.ID[20:], p.Raw[8:12]) // Ack
|
||||
}
|
||||
|
||||
func (p *TCPPacket) UpdateAck(ack uint32) {
|
||||
p.OrigAck = p.Ack
|
||||
p.Ack = ack
|
||||
binary.BigEndian.PutUint32(p.Raw[8:12], ack)
|
||||
p.GenID()
|
||||
}
|
||||
|
||||
// ParseBasic set of fields
|
||||
func (t *TCPPacket) ParseBasic() {
|
||||
t.DestPort = binary.BigEndian.Uint16(t.Raw[2:4])
|
||||
t.SrcPort = binary.BigEndian.Uint16(t.Raw[0:2])
|
||||
t.Seq = binary.BigEndian.Uint32(t.Raw[4:8])
|
||||
t.Ack = binary.BigEndian.Uint32(t.Raw[8:12])
|
||||
t.DataOffset = (t.Raw[12] & 0xF0) >> 4
|
||||
|
||||
// log.Println("DataOffset:", t.DataOffset, t.DestPort, t.SrcPort, t.Seq, t.Ack)
|
||||
|
||||
t.Data = t.Raw[t.DataOffset*4:]
|
||||
}
|
||||
|
||||
func (t *TCPPacket) Dump() []byte {
|
||||
buf := make([]byte, len(t.Data)+16+16)
|
||||
copy(buf[:16], t.Addr)
|
||||
|
||||
tcpBuf := buf[16:]
|
||||
|
||||
binary.BigEndian.PutUint16(tcpBuf[2:4], t.DestPort)
|
||||
binary.BigEndian.PutUint16(tcpBuf[0:2], t.SrcPort)
|
||||
|
||||
binary.BigEndian.PutUint32(tcpBuf[4:8], t.Seq)
|
||||
binary.BigEndian.PutUint32(tcpBuf[8:12], t.Ack)
|
||||
|
||||
tcpBuf[12] = 64
|
||||
copy(tcpBuf[16:], t.Data)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// String output for a TCP Packet
|
||||
func (t *TCPPacket) String() string {
|
||||
maxLen := len(t.Data)
|
||||
if maxLen > 200 {
|
||||
maxLen = 200
|
||||
}
|
||||
|
||||
return strings.Join([]string{
|
||||
"Addr: " + string(t.Addr),
|
||||
"Source port: " + strconv.Itoa(int(t.SrcPort)),
|
||||
"Dest port:" + strconv.Itoa(int(t.DestPort)),
|
||||
"Sequence:" + strconv.Itoa(int(t.Seq)),
|
||||
"Acknowledgment:" + strconv.Itoa(int(t.Ack)),
|
||||
"Header len:" + strconv.Itoa(int(t.DataOffset)),
|
||||
|
||||
"Data size:" + strconv.Itoa(len(t.Data)),
|
||||
"Data:" + string(t.Data[:maxLen]),
|
||||
}, "\n")
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// Replay server receive requests objects from Listeners and forward it to given address.
|
||||
// Basic usage:
|
||||
//
|
||||
// gor replay -f http://staging.server
|
||||
//
|
||||
//
|
||||
// Rate limiting
|
||||
//
|
||||
// It can be useful if you want forward only part of production traffic, not to overload staging environment. You can specify desired request per second using "|" operator after server address:
|
||||
//
|
||||
// # staging.server not get more than 10 requests per second
|
||||
// gor replay -f "http://staging.server|10"
|
||||
//
|
||||
//
|
||||
// Forward to multiple addresses
|
||||
//
|
||||
// Just separate addresses by coma:
|
||||
// gor replay -f "http://staging.server|10,http://dev.server|20"
|
||||
//
|
||||
//
|
||||
// For more help run:
|
||||
//
|
||||
// gor replay -h
|
||||
//
|
||||
package replay
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const bufSize = 1024 * 10
|
||||
|
||||
// Enable debug logging only if "--verbose" flag passed
|
||||
func Debug(v ...interface{}) {
|
||||
if Settings.verbose {
|
||||
log.Println(v...)
|
||||
}
|
||||
}
|
||||
|
||||
func ParseRequest(data []byte) (request *http.Request, err error) {
|
||||
buf := bytes.NewBuffer(data)
|
||||
reader := bufio.NewReader(buf)
|
||||
|
||||
request, err = http.ReadRequest(reader)
|
||||
return
|
||||
}
|
||||
|
||||
// Because its sub-program, Run acts as `main`
|
||||
// Replay server listen to UDP traffic from Listeners
|
||||
// Each request processed by RequestFactory
|
||||
func Run() {
|
||||
var buf [bufSize]byte
|
||||
|
||||
addr, err := net.ResolveUDPAddr("udp", Settings.Address())
|
||||
if err != nil {
|
||||
log.Fatal("Can't start:", err)
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP("udp", addr)
|
||||
log.Println("Starting replay server at:", Settings.Address())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Can't start:", err)
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
for _, host := range Settings.ForwardedHosts() {
|
||||
log.Println("Forwarding requests to:", host.Url, "limit:", host.Limit)
|
||||
}
|
||||
|
||||
requestFactory := NewRequestFactory()
|
||||
|
||||
for {
|
||||
n, _, err := conn.ReadFromUDP(buf[0:])
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
if n > bufSize {
|
||||
Debug("Too large udp packet", bufSize)
|
||||
}
|
||||
|
||||
if request, err := ParseRequest(buf[0:n]); err != nil {
|
||||
Debug("Error while parsing request", err, buf[0:n])
|
||||
} else {
|
||||
requestFactory.Add(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package replay
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type HttpResponse struct {
|
||||
host *ForwardHost
|
||||
req *http.Request
|
||||
resp *http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
// Class for processing requests
|
||||
//
|
||||
// Basic workflow:
|
||||
//
|
||||
// 1. When request added via Add() it get pushed to `responses` chan
|
||||
// 2. handleRequest() listen for `responses` chan and decide where request should be forwarded, and apply rate-limit if needed
|
||||
// 3. sendRequest() forwards request and returns response info to `responses` chan
|
||||
// 4. handleRequest() listen for `response` channel and updates stats
|
||||
type RequestFactory struct {
|
||||
responses chan *HttpResponse
|
||||
requests chan *http.Request
|
||||
}
|
||||
|
||||
// RequestFactory contstuctor
|
||||
// One created, it starts listening for incoming requests: requests channel
|
||||
func NewRequestFactory() (factory *RequestFactory) {
|
||||
factory = &RequestFactory{}
|
||||
factory.responses = make(chan *HttpResponse)
|
||||
factory.requests = make(chan *http.Request)
|
||||
|
||||
go factory.handleRequests()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Forward http request to given host
|
||||
func (f *RequestFactory) sendRequest(host *ForwardHost, request *http.Request) {
|
||||
client := &http.Client{}
|
||||
|
||||
URL := host.Url + request.URL.String()
|
||||
|
||||
request.RequestURI = ""
|
||||
request.URL, _ = url.ParseRequestURI(URL)
|
||||
|
||||
if Settings.verbose {
|
||||
Debug("Sending request:", request)
|
||||
}
|
||||
|
||||
resp, err := client.Do(request)
|
||||
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
} else {
|
||||
Debug("Request error:", err)
|
||||
}
|
||||
|
||||
f.responses <- &HttpResponse{host, request, resp, err}
|
||||
}
|
||||
|
||||
// Handle incoming requests, and they responses
|
||||
func (f *RequestFactory) handleRequests() {
|
||||
hosts := Settings.ForwardedHosts()
|
||||
|
||||
for {
|
||||
select {
|
||||
case req := <-f.requests:
|
||||
for _, host := range hosts {
|
||||
// Ensure that we have actual stats for given timestamp
|
||||
host.Stat.Touch()
|
||||
|
||||
if host.Limit == 0 || host.Stat.Count < host.Limit {
|
||||
// Increment Stat.Count
|
||||
host.Stat.IncReq()
|
||||
|
||||
go f.sendRequest(host, req)
|
||||
}
|
||||
}
|
||||
case resp := <-f.responses:
|
||||
// Increment returned http code stats, and elapsed time
|
||||
resp.host.Stat.IncResp(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add request to channel for further processing
|
||||
func (f *RequestFactory) Add(request *http.Request) {
|
||||
f.requests <- request
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package replay
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Stats stores in context of current timestamp
|
||||
type RequestStat struct {
|
||||
timestamp int64
|
||||
|
||||
Codes map[int]int // { 200: 10, 404:2, 500:1 }
|
||||
|
||||
Count int // All requests including errors
|
||||
Errors int // Rquests with errors (timeout or host not reachable). Not include 50x errors.
|
||||
|
||||
host *ForwardHost
|
||||
}
|
||||
|
||||
// Ensure that current stats is actual (for current timestamp)
|
||||
func (s *RequestStat) Touch() {
|
||||
if s.timestamp != time.Now().Unix() {
|
||||
s.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Called on request start
|
||||
func (s *RequestStat) IncReq() {
|
||||
s.Count++
|
||||
}
|
||||
|
||||
// Called after response
|
||||
func (s *RequestStat) IncResp(resp *HttpResponse) {
|
||||
s.Touch()
|
||||
|
||||
if resp.err != nil {
|
||||
s.Errors++
|
||||
return
|
||||
}
|
||||
|
||||
s.Codes[resp.resp.StatusCode]++
|
||||
}
|
||||
|
||||
// Updated stats timestamp to current time and reset to zero all stats values
|
||||
// TODO: Further on reset it should write stats to file
|
||||
func (s *RequestStat) reset() {
|
||||
if s.timestamp != 0 {
|
||||
Debug("Host:", s.host.Url, "Requests:", s.Count, "Errors:", s.Errors, "Status codes:", s.Codes)
|
||||
}
|
||||
|
||||
s.timestamp = time.Now().Unix()
|
||||
|
||||
s.Codes = make(map[int]int)
|
||||
s.Count = 0
|
||||
s.Errors = 0
|
||||
}
|
||||
|
||||
// RequestStat constructor
|
||||
func NewRequestStats(host *ForwardHost) (stat *RequestStat) {
|
||||
stat = &RequestStat{host: host}
|
||||
stat.reset()
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package replay
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"os"
|
||||
"flag"
|
||||
)
|
||||
|
||||
type ForwardHost struct {
|
||||
Url string
|
||||
Limit int
|
||||
|
||||
Stat *RequestStat
|
||||
}
|
||||
|
||||
type ReplaySettings struct {
|
||||
port int
|
||||
host string
|
||||
|
||||
forwardAddress string
|
||||
|
||||
verbose bool
|
||||
}
|
||||
|
||||
// ForwardedHosts implements forwardAddress syntax support for multiple hosts (coma separated), and rate limiting by specifing "|maxRps" after host name.
|
||||
//
|
||||
// -f "host1,http://host2|10,host3"
|
||||
//
|
||||
func (r *ReplaySettings) ForwardedHosts() (hosts []*ForwardHost) {
|
||||
hosts = make([]*ForwardHost, 0, 10)
|
||||
|
||||
for _, address := range strings.Split(r.forwardAddress, ",") {
|
||||
host_info := strings.Split(address, "|")
|
||||
|
||||
if strings.Index(host_info[0], "http") == -1 {
|
||||
host_info[0] = "http://" + host_info[0]
|
||||
}
|
||||
|
||||
host := &ForwardHost{Url: host_info[0]}
|
||||
host.Stat = NewRequestStats(host)
|
||||
|
||||
if len(host_info) > 1 {
|
||||
host.Limit, _ = strconv.Atoi(host_info[1])
|
||||
}
|
||||
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Helper to return address with port, e.g.: 127.0.0.1:28020
|
||||
func (r *ReplaySettings) Address() string {
|
||||
return r.host + ":" + strconv.Itoa(r.port)
|
||||
}
|
||||
|
||||
var Settings ReplaySettings = ReplaySettings{}
|
||||
|
||||
|
||||
func init() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "replay" {
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
defaultPort = 28020
|
||||
defaultHost = "0.0.0.0"
|
||||
|
||||
defaultAddress = "http://localhost:8080"
|
||||
)
|
||||
|
||||
flag.IntVar(&Settings.port, "p", defaultPort, "specify port number")
|
||||
|
||||
flag.StringVar(&Settings.host, "ip", defaultHost, "ip addresses to listen on")
|
||||
|
||||
flag.StringVar(&Settings.forwardAddress, "f", defaultAddress, "http address to forward traffic.\n\tYou can limit requests per second by adding `|num` after address.\n\tIf you have multiple addresses with different limits. For example: http://staging.example.com|100,http://dev.example.com|10")
|
||||
|
||||
flag.BoolVar(&Settings.verbose, "verbose", false, "Log requests")
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var VERSION string
|
||||
|
||||
// MultiOption allows to specify multiple flags with same name and collects all values into array
|
||||
type MultiOption []string
|
||||
|
||||
func (h *MultiOption) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
// Set gets called multiple times for each flag with same name
|
||||
func (h *MultiOption) Set(value string) error {
|
||||
*h = append(*h, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AppSettings is the struct of main configuration
|
||||
type AppSettings struct {
|
||||
verbose bool
|
||||
debug bool
|
||||
stats bool
|
||||
|
||||
splitOutput bool
|
||||
|
||||
inputDummy MultiOption
|
||||
outputDummy MultiOption
|
||||
outputStdout bool
|
||||
|
||||
inputTCP MultiOption
|
||||
outputTCP MultiOption
|
||||
outputTCPStats bool
|
||||
|
||||
inputFile MultiOption
|
||||
inputFileLoop bool
|
||||
outputFile MultiOption
|
||||
outputFileConfig FileOutputConfig
|
||||
|
||||
inputRAW MultiOption
|
||||
inputRAWEngine string
|
||||
inputRAWTrackResponse bool
|
||||
inputRAWRealIPHeader string
|
||||
|
||||
middleware string
|
||||
|
||||
inputHTTP MultiOption
|
||||
outputHTTP MultiOption
|
||||
|
||||
outputHTTPConfig HTTPOutputConfig
|
||||
modifierConfig HTTPModifierConfig
|
||||
}
|
||||
|
||||
// Settings holds Gor configuration
|
||||
var Settings AppSettings
|
||||
|
||||
func usage() {
|
||||
fmt.Printf("Gor is a simple http traffic replication tool written in Go. Its main goal is to replay traffic from production servers to staging and dev environments.\nProject page: https://github.com/buger/gor\nAuthor: <Leonid Bugaev> leonsbox@gmail.com\nCurrent Version: %s\n\n", VERSION)
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.Usage = usage
|
||||
|
||||
flag.BoolVar(&Settings.verbose, "verbose", false, "Turn on more verbose output")
|
||||
flag.BoolVar(&Settings.debug, "debug", false, "Turn on debug output, shows all intercepted traffic. Works only when with `verbose` flag")
|
||||
flag.BoolVar(&Settings.stats, "stats", false, "Turn on queue stats output")
|
||||
|
||||
flag.BoolVar(&Settings.splitOutput, "split-output", false, "By default each output gets same traffic. If set to `true` it splits traffic equally among all outputs.")
|
||||
|
||||
flag.Var(&Settings.inputDummy, "input-dummy", "Used for testing outputs. Emits 'Get /' request every 1s")
|
||||
flag.Var(&Settings.outputDummy, "output-dummy", "DEPRECATED: use --output-stdout instead")
|
||||
|
||||
flag.BoolVar(&Settings.outputStdout, "output-stdout", false, "Used for testing inputs. Just prints to console data coming from inputs.")
|
||||
|
||||
flag.Var(&Settings.inputTCP, "input-tcp", "Used for internal communication between Gor instances. Example: \n\t# Receive requests from other Gor instances on 28020 port, and redirect output to staging\n\tgor --input-tcp :28020 --output-http staging.com")
|
||||
flag.Var(&Settings.outputTCP, "output-tcp", "Used for internal communication between Gor instances. Example: \n\t# Listen for requests on 80 port and forward them to other Gor instance on 28020 port\n\tgor --input-raw :80 --output-tcp replay.local:28020")
|
||||
flag.BoolVar(&Settings.outputTCPStats, "output-tcp-stats", false, "Report TCP output queue stats to console every 5 seconds.")
|
||||
|
||||
flag.Var(&Settings.inputFile, "input-file", "Read requests from file: \n\tgor --input-file ./requests.gor --output-http staging.com")
|
||||
flag.BoolVar(&Settings.inputFileLoop, "input-file-loop", false, "Loop input files, useful for performance testing.")
|
||||
|
||||
flag.Var(&Settings.outputFile, "output-file", "Write incoming requests to file: \n\tgor --input-raw :80 --output-file ./requests.gor")
|
||||
flag.DurationVar(&Settings.outputFileConfig.flushInterval, "output-file-flush-interval", time.Minute, "Interval for forcing buffer flush to the file, default: 60s.")
|
||||
flag.BoolVar(&Settings.outputFileConfig.append, "output-file-append", false, "The flushed chunk is appended to existence file or not. ")
|
||||
|
||||
// Set default
|
||||
Settings.outputFileConfig.sizeLimit.Set("32mb")
|
||||
flag.Var(&Settings.outputFileConfig.sizeLimit, "output-file-size-limit", "Size of each chunk. Default: 32mb")
|
||||
flag.IntVar(&Settings.outputFileConfig.queueLimit, "output-file-queue-limit", 256, "The length of the chunk queue. Default: 256")
|
||||
|
||||
flag.Var(&Settings.inputRAW, "input-raw", "Capture traffic from given port (use RAW sockets and require *sudo* access):\n\t# Capture traffic from 8080 port\n\tgor --input-raw :8080 --output-http staging.com")
|
||||
|
||||
flag.BoolVar(&Settings.inputRAWTrackResponse, "input-raw-track-response", false, "If turned on Gor will track responses in addition to requests, and they will be available to middleware and file output.")
|
||||
|
||||
flag.StringVar(&Settings.inputRAWEngine, "input-raw-engine", "libpcap", "Intercept traffic using `libpcap` (default), and `raw_socket`")
|
||||
|
||||
flag.StringVar(&Settings.inputRAWRealIPHeader, "input-raw-realip-header", "", "If not blank, injects header with given name and real IP value to the request payload. Usually this header should be named: X-Real-IP")
|
||||
|
||||
flag.StringVar(&Settings.middleware, "middleware", "", "Used for modifying traffic using external command")
|
||||
|
||||
flag.Var(&Settings.inputHTTP, "input-http", "Read requests from HTTP, should be explicitly sent from your application:\n\t# Listen for http on 9000\n\tgor --input-http :9000 --output-http staging.com")
|
||||
|
||||
flag.Var(&Settings.outputHTTP, "output-http", "Forwards incoming requests to given http address.\n\t# Redirect all incoming requests to staging.com address \n\tgor --input-raw :80 --output-http http://staging.com")
|
||||
flag.IntVar(&Settings.outputHTTPConfig.BufferSize, "output-http-response-buffer", 0, "HTTP response buffer size, all data after this size will be discarded.")
|
||||
flag.IntVar(&Settings.outputHTTPConfig.workers, "output-http-workers", 0, "Gor uses dynamic worker scaling by default. Enter a number to run a set number of workers.")
|
||||
flag.IntVar(&Settings.outputHTTPConfig.redirectLimit, "output-http-redirects", 0, "Enable how often redirects should be followed.")
|
||||
flag.DurationVar(&Settings.outputHTTPConfig.Timeout, "output-http-timeout", 0, "Specify HTTP request/response timeout. By default 5s. Example: --output-http-timeout 30s")
|
||||
|
||||
flag.BoolVar(&Settings.outputHTTPConfig.stats, "output-http-stats", false, "Report http output queue stats to console every 5 seconds.")
|
||||
flag.BoolVar(&Settings.outputHTTPConfig.OriginalHost, "http-original-host", false, "Normally gor replaces the Host http header with the host supplied with --output-http. This option disables that behavior, preserving the original Host header.")
|
||||
flag.BoolVar(&Settings.outputHTTPConfig.Debug, "output-http-debug", false, "Enables http debug output.")
|
||||
|
||||
flag.StringVar(&Settings.outputHTTPConfig.elasticSearch, "output-http-elasticsearch", "", "Send request and response stats to ElasticSearch:\n\tgor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.headers, "http-set-header", "Inject additional headers to http reqest:\n\tgor --input-raw :8080 --output-http staging.com --http-set-header 'User-Agent: Gor'")
|
||||
flag.Var(&Settings.modifierConfig.headers, "output-http-header", "WARNING: `--output-http-header` DEPRECATED, use `--http-set-header` instead")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.params, "http-set-param", "Set request url param, if param already exists it will be overwritten:\n\tgor --input-raw :8080 --output-http staging.com --http-set-param api_key=1")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.methods, "http-allow-method", "Whitelist of HTTP methods to replay. Anything else will be dropped:\n\tgor --input-raw :8080 --output-http staging.com --http-allow-method GET --http-allow-method OPTIONS")
|
||||
flag.Var(&Settings.modifierConfig.methods, "output-http-method", "WARNING: `--output-http-method` DEPRECATED, use `--http-allow-method` instead")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.urlRegexp, "http-allow-url", "A regexp to match requests against. Filter get matched against full url with domain. Anything else will be dropped:\n\t gor --input-raw :8080 --output-http staging.com --http-allow-url ^www.")
|
||||
flag.Var(&Settings.modifierConfig.urlRegexp, "output-http-url-regexp", "WARNING: `--output-http-url-regexp` DEPRECATED, use `--http-allow-url` instead")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.urlNegativeRegexp, "http-disallow-url", "A regexp to match requests against. Filter get matched against full url with domain. Anything else will be forwarded:\n\t gor --input-raw :8080 --output-http staging.com --http-disallow-url ^www.")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.urlRewrite, "http-rewrite-url", "Rewrite the request url based on a mapping:\n\tgor --input-raw :8080 --output-http staging.com --http-rewrite-url /v1/user/([^\\/]+)/ping:/v2/user/$1/ping")
|
||||
flag.Var(&Settings.modifierConfig.urlRewrite, "output-http-rewrite-url", "WARNING: `--output-http-rewrite-url` DEPRECATED, use `--http-rewrite-url` instead")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.headerFilters, "http-allow-header", "A regexp to match a specific header against. Requests with non-matching headers will be dropped:\n\t gor --input-raw :8080 --output-http staging.com --http-allow-header api-version:^v1")
|
||||
flag.Var(&Settings.modifierConfig.headerFilters, "output-http-header-filter", "WARNING: `--output-http-header-filter` DEPRECATED, use `--http-allow-header` instead")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.headerFilters, "http-disallow-header", "A regexp to match a specific header against. Requests with matching headers will be dropped:\n\t gor --input-raw :8080 --output-http staging.com --http-disallow-header \"User-Agent: Replayed by Gor\"")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.headerHashFilters, "http-header-limiter", "Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific header:\n\t gor --input-raw :8080 --output-http staging.com --http-header-imiter user-id:25%")
|
||||
flag.Var(&Settings.modifierConfig.headerHashFilters, "output-http-header-hash-filter", "WARNING: `output-http-header-hash-filter` DEPRECATED, use `--http-header-hash-limiter` instead")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.paramHashFilters, "http-param-limiter", "Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific GET param:\n\t gor --input-raw :8080 --output-http staging.com --http-param-limiter user_id:25%")
|
||||
}
|
||||
|
||||
var previousDebugTime int64
|
||||
var debugMutex sync.Mutex
|
||||
|
||||
// Debug gets called only if --verbose flag specified
|
||||
func Debug(args ...interface{}) {
|
||||
if Settings.verbose {
|
||||
debugMutex.Lock()
|
||||
now := time.Now()
|
||||
diff := float64(now.UnixNano()-previousDebugTime) / 1000000
|
||||
previousDebugTime = now.UnixNano()
|
||||
debugMutex.Unlock()
|
||||
|
||||
fmt.Printf("[DEBUG][PID %d][%d][%fms] ", os.Getpid(), now.UnixNano(), diff)
|
||||
fmt.Println(args...)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestInput used for testing purpose, it allows emitting requests on demand
|
||||
type TestInput struct {
|
||||
data chan []byte
|
||||
}
|
||||
|
||||
// NewTestInput constructor for TestInput
|
||||
func NewTestInput() (i *TestInput) {
|
||||
i = new(TestInput)
|
||||
i.data = make(chan []byte, 100)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *TestInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
|
||||
header := payloadHeader(RequestPayload, uuid(), time.Now().UnixNano())
|
||||
copy(data[0:len(header)], header)
|
||||
copy(data[len(header):], buf)
|
||||
|
||||
return len(buf) + len(header), nil
|
||||
}
|
||||
|
||||
// EmitGET emits GET request without headers
|
||||
func (i *TestInput) EmitGET() {
|
||||
i.data <- []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
}
|
||||
|
||||
// EmitPOST emits POST request with Content-Length
|
||||
func (i *TestInput) EmitPOST() {
|
||||
i.data <- []byte("POST /pub/WWW/ HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
}
|
||||
|
||||
// EmitChunkedPOST emits POST request with `Transfer-Encoding: chunked` and chunked body
|
||||
func (i *TestInput) EmitChunkedPOST() {
|
||||
i.data <- []byte("POST /pub/WWW/ HTTP/1.1\r\nHost: www.w3.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
|
||||
}
|
||||
|
||||
// EmitLargePOST emits POST request with large payload (5mb)
|
||||
func (i *TestInput) EmitLargePOST() {
|
||||
size := 5 * 1024 * 1024 // 5 MB
|
||||
rb := make([]byte, size)
|
||||
rand.Read(rb)
|
||||
|
||||
rs := base64.URLEncoding.EncodeToString(rb)
|
||||
|
||||
i.data <- []byte("POST / HTTP/1.1\nHost: www.w3.org\nContent-Length:5242880\r\n\r\n" + rs)
|
||||
Debug("Sent large POST")
|
||||
}
|
||||
|
||||
// EmitSizedPOST emit a POST with a payload set to a supplied size
|
||||
func (i *TestInput) EmitSizedPOST(payloadSize int) {
|
||||
rb := make([]byte, payloadSize)
|
||||
rand.Read(rb)
|
||||
|
||||
rs := base64.URLEncoding.EncodeToString(rb)
|
||||
|
||||
i.data <- []byte("POST / HTTP/1.1\nHost: www.w3.org\nContent-Length:5242880\r\n\r\n" + rs)
|
||||
Debug("Sent large POST")
|
||||
}
|
||||
|
||||
// EmitOPTIONS emits OPTIONS request, similar to GET
|
||||
func (i *TestInput) EmitOPTIONS() {
|
||||
i.data <- []byte("OPTIONS / HTTP/1.1\nHost: www.w3.org\r\n\r\n")
|
||||
}
|
||||
|
||||
func (i *TestInput) String() string {
|
||||
return "Test Input"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
type writeCallback func(data []byte)
|
||||
|
||||
// TestOutput used in testing to intercept any output into callback
|
||||
type TestOutput struct {
|
||||
cb writeCallback
|
||||
}
|
||||
|
||||
// NewTestOutput constructor for TestOutput, accepts callback which get called on each incoming Write
|
||||
func NewTestOutput(cb writeCallback) (i *TestOutput) {
|
||||
i = new(TestOutput)
|
||||
i.cb = cb
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *TestOutput) Write(data []byte) (int, error) {
|
||||
i.cb(data)
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (i *TestOutput) String() string {
|
||||
return "Test Input"
|
||||
}
|
||||
Reference in New Issue
Block a user