mirror of
https://github.com/buger/goreplay.git
synced 2026-10-19 16:15:42 +00:00
Compare commits
471
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd8e9161e2 | ||
|
|
5cbb5ea85f | ||
|
|
ea822d83da | ||
|
|
3dff07107a | ||
|
|
c20b2734dc | ||
|
|
39b1f80457 | ||
|
|
8462d5d09f | ||
|
|
a7c5788052 | ||
|
|
a63ce611df | ||
|
|
f89c52445b | ||
|
|
60d949a243 | ||
|
|
9d5caade25 | ||
|
|
28688e29e8 | ||
|
|
e9bbc252e0 | ||
|
|
73e26d48d9 | ||
|
|
1c87a339a5 | ||
|
|
312d9e8b7d | ||
|
|
540e42b9d4 | ||
|
|
15bd2608f7 | ||
|
|
d67bc13db3 | ||
|
|
ab2ba8400d | ||
|
|
db28858804 | ||
|
|
9d71900ad9 | ||
|
|
0b437716be | ||
|
|
932c894b6b | ||
|
|
73b1621656 | ||
|
|
b1ab61f2b2 | ||
|
|
080240ca9f | ||
|
|
b0f3048876 | ||
|
|
261c592b83 | ||
|
|
766cb77410 | ||
|
|
e1bf25682f | ||
|
|
039d7c3f26 | ||
|
|
71387ffc90 | ||
|
|
dbead6b428 | ||
|
|
2fb9b2054e | ||
|
|
17f7c3c74b | ||
|
|
8c1a194a51 | ||
|
|
857bef93f0 | ||
|
|
aa1eb9f8b7 | ||
|
|
7925dbb0b1 | ||
|
|
c1f14e8cfb | ||
|
|
21e0a2f8c2 | ||
|
|
803257c710 | ||
|
|
28899ea1e1 | ||
|
|
263108976a | ||
|
|
92a8bfb27c | ||
|
|
5c5ef3ac15 | ||
|
|
2d78f195d2 | ||
|
|
df14b29e74 | ||
|
|
cad7f37c98 | ||
|
|
7e90a73b81 | ||
|
|
1a5ac70c24 | ||
|
|
11bd20f251 | ||
|
|
40d57cac36 | ||
|
|
9cd5ad719b | ||
|
|
fa56e7644c | ||
|
|
ffc1a09ca1 | ||
|
|
ae4a6259c2 | ||
|
|
71eeef5ced | ||
|
|
7ce4fc50ed | ||
|
|
df143cdeec | ||
|
|
64cf9e5509 | ||
|
|
daddbb9c42 | ||
|
|
34954eb8e8 | ||
|
|
81e8f98ab6 | ||
|
|
dfae555381 | ||
|
|
f7e01997a2 | ||
|
|
36ca809e4a | ||
|
|
107740b57d | ||
|
|
7300b83894 | ||
|
|
c48d07d2c6 | ||
|
|
a8cfaa7581 | ||
|
|
707b311456 | ||
|
|
b37fe727b4 | ||
|
|
f6b0d5d794 | ||
|
|
e3f3348105 | ||
|
|
60e150de67 | ||
|
|
c092718ee6 | ||
|
|
84e99a8bb0 | ||
|
|
279aa3fc3f | ||
|
|
408f1d0f31 | ||
|
|
f57703ee5c | ||
|
|
264d8cf00e | ||
|
|
6f70a939f4 | ||
|
|
c693938ef9 | ||
|
|
267d6d0e2b | ||
|
|
e9a5cc19b5 | ||
|
|
d8cace421e | ||
|
|
bf98a636c4 | ||
|
|
476ed808f9 | ||
|
|
ec3f104c8c | ||
|
|
85c6070775 | ||
|
|
b4a2c018c1 | ||
|
|
3fa4a8feb7 | ||
|
|
fb3870d68c | ||
|
|
6eea02ee86 | ||
|
|
962087065a | ||
|
|
7b0f81b82f | ||
|
|
633cfd5fb4 | ||
|
|
a5a3012347 | ||
|
|
393eb74f9c | ||
|
|
166c8e8d87 | ||
|
|
52ed8b4d1e | ||
|
|
c8cf370b62 | ||
|
|
29139e9eda | ||
|
|
d0d1527d9b | ||
|
|
c63f2a8113 | ||
|
|
9fd7a46e7e | ||
|
|
f85bb29327 | ||
|
|
0ffe3edcbf | ||
|
|
a97c04b9e2 | ||
|
|
61301e78b1 | ||
|
|
eeb09a1494 | ||
|
|
68082bfb11 | ||
|
|
28d646d118 | ||
|
|
c93ff3598c | ||
|
|
0bc3fba5d0 | ||
|
|
65fd1983b1 | ||
|
|
88972ff301 | ||
|
|
85a7bc1e9a | ||
|
|
36a8845ffe | ||
|
|
9eb6ab262b | ||
|
|
896dbe312a | ||
|
|
edae33717e | ||
|
|
c5d1112e7a | ||
|
|
18610d3555 | ||
|
|
0fea94a76c | ||
|
|
7b1544157f | ||
|
|
4e46bb46af | ||
|
|
b1092d1567 | ||
|
|
0c0ef97b67 | ||
|
|
fe6cfd90fe | ||
|
|
c9fe4a11ad | ||
|
|
e9294aeb5f | ||
|
|
cb1ae0e7c0 | ||
|
|
2db143aaef | ||
|
|
a65256d707 | ||
|
|
1f62555ca8 | ||
|
|
52f4ad03f3 | ||
|
|
35a5957801 | ||
|
|
d265594b00 | ||
|
|
0097c599df | ||
|
|
f8a1b9dde6 | ||
|
|
22a67df6c0 | ||
|
|
5bd47ee1df | ||
|
|
45d7527128 | ||
|
|
bf8198d639 | ||
|
|
3acec0fd42 | ||
|
|
cc005ddcbb | ||
|
|
debb265d6d | ||
|
|
c5f12b567b | ||
|
|
4255d4155f | ||
|
|
927215bd9b | ||
|
|
08efdef37a | ||
|
|
4bb64be27e | ||
|
|
e310fc8369 | ||
|
|
c8f4493eca | ||
|
|
0f8e2f2d52 | ||
|
|
74a5c8570d | ||
|
|
d309650589 | ||
|
|
17f6c59061 | ||
|
|
9cf1ad65df | ||
|
|
16b6a3cc31 | ||
|
|
dfb67b4456 | ||
|
|
efce146244 | ||
|
|
eb8eb1db52 | ||
|
|
d7ffc3c359 | ||
|
|
1c3cd633d1 | ||
|
|
b8befff286 | ||
|
|
2b79791fe6 | ||
|
|
39ebff56b4 | ||
|
|
c138fa9739 | ||
|
|
89c34c6bf5 | ||
|
|
a1054b10e8 | ||
|
|
cf402a1d0e | ||
|
|
211b16888e | ||
|
|
06ec570286 | ||
|
|
09f47b25bf | ||
|
|
652e589e2b | ||
|
|
163bf1f1c0 | ||
|
|
5aa1696680 | ||
|
|
21531cea9a | ||
|
|
03767d1a77 | ||
|
|
2680ce9dd4 | ||
|
|
b3dffae66f | ||
|
|
aa4a878436 | ||
|
|
4fe95be416 | ||
|
|
23771b8994 | ||
|
|
a1a4460c90 | ||
|
|
2b20c1107a | ||
|
|
d755c579e4 | ||
|
|
064519ce89 | ||
|
|
cdd818fe77 | ||
|
|
c23f95d4cf | ||
|
|
4cf59cc913 | ||
|
|
8f450c77a6 | ||
|
|
a49b250ab4 | ||
|
|
de47f6d6f6 | ||
|
|
d9ce263c6a | ||
|
|
74225ebb22 | ||
|
|
80777061a7 | ||
|
|
d1ddda4181 | ||
|
|
a15055ef7f | ||
|
|
2b6203c110 | ||
|
|
2104bec5be | ||
|
|
1aa24a9910 | ||
|
|
dcc7af4c09 | ||
|
|
0fb98a8f48 | ||
|
|
6ea23de38b | ||
|
|
375538263b | ||
|
|
da8b75222b | ||
|
|
a2a1bd5c67 | ||
|
|
ce46188dc2 | ||
|
|
7c67b8c566 | ||
|
|
e584479911 | ||
|
|
5a8d56e10b | ||
|
|
c8dfe67ccb | ||
|
|
ecb119e82a | ||
|
|
e1f3d71f4e | ||
|
|
858dfaac78 | ||
|
|
9173379cc8 | ||
|
|
6a48882125 | ||
|
|
bf1c67c5a1 | ||
|
|
e726e45dc0 | ||
|
|
b1a0198354 | ||
|
|
1a32de3dc6 | ||
|
|
d66ba0353a | ||
|
|
ecb16664db | ||
|
|
8c16220532 | ||
|
|
ed37589748 | ||
|
|
590304d687 | ||
|
|
e69570367b | ||
|
|
043c0b7436 | ||
|
|
973b028517 | ||
|
|
b48aadc9ea | ||
|
|
768a29e2af | ||
|
|
5e90377a7f | ||
|
|
f520b338f3 | ||
|
|
53cbd8863d | ||
|
|
def3c91898 | ||
|
|
4ea306b5a9 | ||
|
|
3b51158163 | ||
|
|
fe321cd055 | ||
|
|
8421ddb6ff | ||
|
|
e5935a001d | ||
|
|
a519c09770 | ||
|
|
17c1abfc5e | ||
|
|
ea8d3f7299 | ||
|
|
962fd70d1a | ||
|
|
b0b697cba6 | ||
|
|
060f521a0a | ||
|
|
8e5a256226 | ||
|
|
8ce5318fbf | ||
|
|
db1e3d71e7 | ||
|
|
f5e8e26e96 | ||
|
|
826cc45d44 | ||
|
|
5909dd71c5 | ||
|
|
3043189d55 | ||
|
|
9b40730338 | ||
|
|
be49c7e7ac | ||
|
|
92b02e4af4 | ||
|
|
5a7bdd3d3a | ||
|
|
dfd5fa0c4f | ||
|
|
5b5c3f33f4 | ||
|
|
1f19e550bb | ||
|
|
cd51e09fcd | ||
|
|
ddf2dc2876 | ||
|
|
c4869d9338 | ||
|
|
fd5118b76d | ||
|
|
bc1a6c9524 | ||
|
|
708b2cebd3 | ||
|
|
7c7882969f | ||
|
|
5e6b58e7a6 | ||
|
|
a9ab7ca2f1 | ||
|
|
0a74db6aae | ||
|
|
b462c06a7c | ||
|
|
c6244435ca | ||
|
|
fa0e96b7ef | ||
|
|
ca61e39790 | ||
|
|
bea255ab11 | ||
|
|
bde2977268 | ||
|
|
12043e5a01 | ||
|
|
719e9b8e56 | ||
|
|
cedc5fae32 | ||
|
|
963f7a73d7 | ||
|
|
1f24c709c2 | ||
|
|
a1174a159c | ||
|
|
f90dcc763b | ||
|
|
638ddfb144 | ||
|
|
40f7facc33 | ||
|
|
0a038575f3 | ||
|
|
d424167f04 | ||
|
|
41d3ce48a3 | ||
|
|
9b96dc20df | ||
|
|
e3a9a9b2c2 | ||
|
|
76fb91966b | ||
|
|
22466d4028 | ||
|
|
47db326b0f | ||
|
|
0831476beb | ||
|
|
7f7cb7c439 | ||
|
|
0b986a9c3c | ||
|
|
967c380dc3 | ||
|
|
1674b58707 | ||
|
|
a2f5e43375 | ||
|
|
fc0361dcdc | ||
|
|
647f7022cf | ||
|
|
082106dfd8 | ||
|
|
75b31fa701 | ||
|
|
a12357b497 | ||
|
|
8e37136b8e | ||
|
|
dac3b171a3 | ||
|
|
daa9754dd6 | ||
|
|
ecd7e3a5e5 | ||
|
|
4268dcf7d1 | ||
|
|
544d9402f0 | ||
|
|
25ded56a59 | ||
|
|
392d9a2f0e | ||
|
|
fbebe72f02 | ||
|
|
e38f6a5801 | ||
|
|
25b998fa3f | ||
|
|
f1f8e2f2a4 | ||
|
|
07fa6d9a7c | ||
|
|
2faae82db0 | ||
|
|
7e245dfb18 | ||
|
|
f0acd31d88 | ||
|
|
d34c27c3de | ||
|
|
a4826fcff9 | ||
|
|
2e768f776d | ||
|
|
1d811f34db | ||
|
|
7ce13e36d2 | ||
|
|
edaf43027c | ||
|
|
a6d263df1c | ||
|
|
35bb655f2d | ||
|
|
24fc5b0078 | ||
|
|
8e0a8b31a9 | ||
|
|
dac04cbef3 | ||
|
|
dae6a8915d | ||
|
|
6620d25626 | ||
|
|
c78933f1df | ||
|
|
55fe5b9c11 | ||
|
|
12287d8bae | ||
|
|
a5cc943af4 | ||
|
|
e65035dd95 | ||
|
|
c128d46088 | ||
|
|
1ed8691b38 | ||
|
|
f17f51390a | ||
|
|
5a0c03ec33 | ||
|
|
803d53a760 | ||
|
|
7c33336433 | ||
|
|
37650df2c8 | ||
|
|
f2ff542cea | ||
|
|
c756a11333 | ||
|
|
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 | ||
|
|
699eefe317 | ||
|
|
2a955dcdbf |
@@ -1 +1,3 @@
|
||||
*.tar.gz
|
||||
gor
|
||||
gor.test
|
||||
|
||||
+12
@@ -6,9 +6,21 @@
|
||||
*.bin
|
||||
|
||||
*.gz
|
||||
*.zip
|
||||
|
||||
*.class
|
||||
|
||||
*.test
|
||||
.idea
|
||||
*.iml
|
||||
gor
|
||||
|
||||
*.mprof
|
||||
|
||||
*.pcap
|
||||
|
||||
.DS_Store
|
||||
|
||||
corpus
|
||||
crashers
|
||||
suppressions
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
POST /post HTTP/1.1
|
||||
Content-Length: 7
|
||||
Host: www.w3.org
|
||||
|
||||
a=1&b=2
|
||||
+5
-2
@@ -1,3 +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 60s -race"
|
||||
go: 1.14
|
||||
script: sudo -E bash -c "source /etc/profile && eval '$(gimme 1.14)' && 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
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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 GoReplay LLC. READ IT CAREFULLY BEFORE COMPLETING THE INSTALLATION PROCESS AND USING GOREPLAY 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 GoReplay 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 concurrently run the software on an unlimited number of Hosts.
|
||||
|
||||
1.3 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 GoReplay LLC 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 GoReplay LLC any royalties for your distribution of the Software in accordance with this EULA.
|
||||
|
||||
1.4 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.5 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. GoReplay LLC 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 GoReplay LLC; (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 GoReplay LLC 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 GoReplay LLC; (j) use the Software to develop a product which is competitive with any GoReplay LLC 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 GoReplay LLC in writing. If your unique Source URL is ever published, GoReplay LLC 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, GoReplay LLC 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 GoReplay LLC).
|
||||
|
||||
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 GoReplay Pro support page (https://github.com/buger/goreplay/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 GoReplay LLC 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 GoReplay LLC.
|
||||
|
||||
8. Disclaimer of Warranties. The Software is provided "as is," with all faults, defects and errors, and without warranty of any kind. GoReplay LLC does not warrant that the Software will be free of bugs, errors, viruses or other defects, and GoReplay LLC 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 GoReplay LLC to do so.
|
||||
To the maximum extent permitted by applicable law, GoReplay LLC 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.
|
||||
Notwithstanding the foregoing, GoReplay LLC represents and warrants that it either owns the entire right to, title to, interest in, or has the right to license, the Software, and your proper use of the same will not violate any intellectual property or proprietary rights of another.
|
||||
|
||||
9. Limitation of Liability.
|
||||
|
||||
In no event will either party 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 such party has been advised of the possibility of such damages.
|
||||
|
||||
In no event will GoReplay LLC 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 GoReplay LLC’ entire liability for breach of this Agreement shall be limited, at GoReplay LLC’ sole and exclusive discretion, to (a) replacement of any defective software or documentation; or (b) refund of the license fee paid to GoReplay LLC, payable in accordance with GoReplay LLC' refund policy.
|
||||
|
||||
11. Acknowledgements.
|
||||
|
||||
11.1 Consent to the Use of Data. You agree that GoReplay LLC and its affiliates may collect and use technical information gathered as part of the product support services to be used in an anonymized manner. GoReplay LLC 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, subject to your reasonable information security and confidentiality protections. 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. GoReplay LLC 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/goreplay/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 GoReplay LLC and any attempted assignment without such consent shall be void.
|
||||
|
||||
13.4 Export Compliance. Each party agrees 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 GoReplay LLC 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 GoReplay LLC for any reason, please direct correspondence to info@gortool.com
|
||||
+7
-21
@@ -1,22 +1,8 @@
|
||||
FROM golang:latest
|
||||
FROM alpine:latest as builder
|
||||
RUN apk add --no-cache ca-certificates openssl
|
||||
RUN wget https://github.com/buger/goreplay/releases/download/v1.0.0/gor_1.0.0_x64.tar.gz -O gor.tar.gz
|
||||
RUN tar xzf gor.tar.gz
|
||||
|
||||
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 wget http://apache-mirror.rbc.ru/pub/apache//commons/io/binaries/commons-io-2.4-bin.tar.gz -P /tmp
|
||||
RUN tar xzf /tmp/commons-io-2.4-bin.tar.gz -C /tmp
|
||||
|
||||
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 -u github.com/golang/lint/golint
|
||||
RUN go get
|
||||
FROM scratch
|
||||
COPY --from=builder /gor .
|
||||
ENTRYPOINT ["./gor"]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM golang:1.14
|
||||
|
||||
RUN apt-get update && apt-get install ruby vim-common -y
|
||||
|
||||
RUN apt-get install flex bison -y
|
||||
RUN wget http://www.tcpdump.org/release/libpcap-1.8.1.tar.gz && tar xzf libpcap-1.8.1.tar.gz && cd libpcap-1.8.1 && ./configure && make install
|
||||
RUN apt-get install default-jre default-jdk -y
|
||||
RUN go get github.com/google/gopacket
|
||||
RUN go get -u golang.org/x/lint/golint
|
||||
|
||||
WORKDIR /go/src/github.com/buger/goreplay/
|
||||
ADD . /go/src/github.com/buger/goreplay/
|
||||
|
||||
RUN go get
|
||||
+20
-183
@@ -1,191 +1,28 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
Copyright (c) 2011-present Leonid Bugaev
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
1. Definitions.
|
||||
* All content residing under the "doc/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license".
|
||||
* Files with the following names "pro.go", "s3_reader.go", "output_s3.go" are released under the commercial license specified in "COMM-LICENSE" file.
|
||||
* Content outside of the above mentioned directories or restrictions above is available under the "LGPLv3" license as defined below.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
GoReplay 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.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, "control" means (i) the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
As a special exception to the GNU Lesser General Public License version 3
|
||||
("LGPL3"), the copyright holders of this Library give you permission to
|
||||
convey to a third party a Combined Work that links statically or dynamically
|
||||
to this Library without providing any Minimal Corresponding Source or
|
||||
Minimal Application Code as set out in 4d or providing the installation
|
||||
information set out in section 4e, provided that you comply with the other
|
||||
provisions of LGPL3 and provided that you meet, for the Application the
|
||||
terms and conditions of the license(s) which apply to the Application.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
TLDR: You are free to use Gor subpackages like `byteutils` or `proto` in your commercial projects.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
"submitted" means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution.
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
6. Trademarks.
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
8. Limitation of Liability.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability.
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets "[]" replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same "printed page" as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
GoReplay Pro has a commercial-friendly license allowing private forks
|
||||
and modifications of GoReplay. Please see https://goreplay.org/pro.html for
|
||||
more detail. You can find the commercial license terms in COMM-LICENSE.
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
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
|
||||
SOURCE_PATH = /go/src/github.com/buger/gor/
|
||||
RUN = docker run -v `pwd`:$(SOURCE_PATH) -p 0.0.0.0:8000:8000 -t -i gor
|
||||
SOURCE = $(shell ls -1 *.go | grep -v _test.go)
|
||||
SOURCE_PATH = /go/src/github.com/buger/goreplay/
|
||||
PORT = 8000
|
||||
FADDR = :8000
|
||||
CONTAINER=gor
|
||||
PREFIX=
|
||||
RUN = docker run -v `pwd`:$(SOURCE_PATH) -e AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) -e AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) -p 0.0.0.0:$(PORT):$(PORT) -t -i $(CONTAINER)
|
||||
BENCHMARK = BenchmarkRAWInput
|
||||
TEST = TestRawListenerBench
|
||||
VERSION = DEV-$(shell date +%s)
|
||||
LDFLAGS = -ldflags "-X main.VERSION=$(VERSION)$(PREFIX) -extldflags \"-static\" -X main.DEMO=$(DEMO)"
|
||||
MAC_LDFLAGS = -ldflags "-X main.VERSION=$(VERSION)$(PREFIX) -X main.DEMO=$(DEMO)"
|
||||
FADDR = ":8000"
|
||||
|
||||
release: release-x86 release-x64
|
||||
release: release-x64 release-mac
|
||||
|
||||
release-bin:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i gor go build -o gor -tags netgo $(LDFLAGS)
|
||||
|
||||
release-x64:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 --env CGO_ENABLED=0 -i gor go build -ldflags "-X main.VERSION=$(VERSION)"&& tar -czf gor_$(VERSION)_x64.tar.gz gor && rm gor
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i $(CONTAINER) go build -o gor -tags netgo $(LDFLAGS) && tar -czf gor_$(VERSION)$(PREFIX)_x64.tar.gz gor && rm gor
|
||||
|
||||
release-x86:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=386 --env CGO_ENABLED=0 -i gor go build -ldflags "-X main.VERSION=$(VERSION)" && tar -czf gor_$(VERSION)_x86.tar.gz gor && rm gor
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=386 -i $(CONTAINER) go build -o gor -tags netgo $(LDFLAGS) && tar -czf gor_$(VERSION)$(PREFIX)_x86.tar.gz gor && rm gor
|
||||
|
||||
release-mac:
|
||||
go build -o gor $(MAC_LDFLAGS) && tar -czf gor_$(VERSION)$(PREFIX)_mac.tar.gz gor && rm gor
|
||||
|
||||
install:
|
||||
go install $(MAC_LDFLAGS)
|
||||
|
||||
build:
|
||||
docker build -t gor .
|
||||
|
||||
docker build -t $(CONTAINER) -f Dockerfile.dev .
|
||||
|
||||
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
|
||||
@@ -24,10 +42,13 @@ race:
|
||||
$(RUN) go test ./... $(ARGS) -v -race -timeout 15s
|
||||
|
||||
test:
|
||||
$(RUN) go test ./... -timeout 10s $(ARGS) -v
|
||||
$(RUN) go test ./. -timeout 120s $(LDFLAGS) $(ARGS) -v
|
||||
|
||||
test_all:
|
||||
$(RUN) go test ./... -timeout 120s $(LDFLAGS) $(ARGS) -v
|
||||
|
||||
testone:
|
||||
$(RUN) go test ./... -timeout 4s -run $(TEST) $(ARGS) -v
|
||||
$(RUN) go test ./. -timeout 60s $(LDFLAGS) -run $(TEST) $(ARGS) -v
|
||||
|
||||
cover:
|
||||
$(RUN) go test $(ARGS) -race -v -timeout 15s -coverprofile=coverage.out
|
||||
@@ -40,14 +61,30 @@ vet:
|
||||
$(RUN) go vet
|
||||
|
||||
bench:
|
||||
$(RUN) go test -v -run NOT_EXISTING -bench HTTP
|
||||
$(RUN) go test $(LDFLAGS) -v -run NOT_EXISTING -bench $(BENCHMARK) -benchtime 5s
|
||||
|
||||
profile_test:
|
||||
$(RUN) go test $(LDFLAGS) -run $(TEST) ./capture/. $(ARGS) -memprofile mem.mprof -cpuprofile cpu.out
|
||||
$(RUN) go test $(LDFLAGS) -run $(TEST) ./capture/. $(ARGS) -c
|
||||
|
||||
# Used mainly for debugging, because docker container do not have access to parent machine ports
|
||||
run:
|
||||
$(RUN) go run $(SOURCE) --input-dummy=0 --output-http="http://localhost:9000" --input-raw :9000 --input-http :9000 --verbose --debug --middleware "./examples/middleware/echo.sh"
|
||||
$(RUN) go run $(LDFLAGS) $(SOURCE) --input-dummy=0 --output-http="http://localhost:9000" --input-raw-track-response --input-raw 127.0.0.1:9000 --verbose --debug --middleware "./examples/middleware/echo.sh" --output-file requests.gor
|
||||
|
||||
run-2:
|
||||
$(RUN) go run $(SOURCE) --input-file ./fixtures/requests.gor --output-dummy=0
|
||||
$(RUN) go run $(LDFLAGS) $(SOURCE) --input-raw :8000 --input-raw-bpf-filter "dst port 8000" --output-stdout --output-http "http://localhost:8000" --input-dummy=0
|
||||
|
||||
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)
|
||||
|
||||
readpcap:
|
||||
go run $(SOURCE) --input-raw $(FILE) --input-raw-track-response --input-raw-engine pcap_file --output-stdout
|
||||
|
||||
record:
|
||||
$(RUN) go run $(SOURCE) --input-dummy=0 --output-file=requests.gor --verbose --debug
|
||||
@@ -57,3 +94,20 @@ replay:
|
||||
|
||||
bash:
|
||||
$(RUN) /bin/bash
|
||||
|
||||
|
||||
FPMCOMMON= \
|
||||
--name goreplay \
|
||||
--description "GoReplay is an open-source network monitoring tool which can record your live traffic, and use it for shadowing, load testing, monitoring and detailed analysis." \
|
||||
-v $(VERSION) \
|
||||
--vendor "Leonid Bugaev" \
|
||||
-m "<support@goreplay.org>" \
|
||||
--url "https://goreplay.org" \
|
||||
-s dir \
|
||||
-C /tmp/gor-build \
|
||||
|
||||
build_packages:
|
||||
mkdir -p /tmp/gor-build
|
||||
go build -i -o /tmp/gor-build/gor
|
||||
fpm $(FPMCOMMON) -a amd64 -t deb ./=/usr/local/bin
|
||||
fpm $(FPMCOMMON) -a amd64 -t rpm ./=/usr/local/bin
|
||||
|
||||
@@ -1,471 +1,48 @@
|
||||
[](https://travis-ci.org/buger/gor) [](https://codebeat.co/projects/github-com-buger-gor) [](https://goreportcard.com/report/github.com/buger/gor)
|
||||
[](https://github.com/buger/goreplay/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) [](https://houndci.com)
|
||||
|
||||

|
||||
|
||||
## https://goreplay.org/
|
||||
|
||||
GoReplay is an open-source network monitoring tool which can record your live traffic, and use it for shadowing, load testing, monitoring and detailed analysis.
|
||||
|
||||
## 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.
|
||||
As your application grows, the effort required to test it also grows exponentially. GoReplay offers you the simple idea of reusing your existing traffic for testing, which makes it incredibly powerful. Our state of art technique allows you to analyze and record your application traffic without affecting it. This eliminates the risks that come with putting a third party component in the critical path.
|
||||
|
||||
GoReplay increases your confidence in code deployments, configuration 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.
|
||||
GoReplay offers unique approach for shadowing. Instead of being a proxy, GoReplay in background listen for traffic on your network interface, requiring no changes in your production infrastructure, rather then running GoReplay daemon on the same machine as your service.
|
||||
|
||||

|
||||
|
||||

|
||||
Check [latest documentation](http://github.com/buger/goreplay/wiki).
|
||||
|
||||
## Installation
|
||||
Download latest binary from https://github.com/buger/goreplay/releases or [compile by yourself](https://github.com/buger/goreplay/wiki/Compilation).
|
||||
|
||||
## Examples
|
||||
## Getting started
|
||||
|
||||
### Capture traffic from port
|
||||
```bash
|
||||
# Run on servers where you want to catch traffic. You can run it on each `web` machine.
|
||||
sudo gor --input-raw :80 --output-tcp replay.local:28020
|
||||
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`.
|
||||
|
||||
# Replay server (replay.local).
|
||||
gor --input-tcp replay.local:28020 --output-http http://staging.com
|
||||
```
|
||||
See the our [documentation](https://github.com/buger/goreplay/wiki/) and [Getting started](https://github.com/buger/goreplay/wiki/Getting-Started) page for more info.
|
||||
|
||||
Since Gor use raw sockets to capture traffic it require `sudo` access. Alternatively you can allow access to raw sockets like this: `sudo setcap CAP_NET_RAW=ep gor`
|
||||
## Newsletter
|
||||
Subscribe to our [newsletter](https://www.getdrip.com/forms/89690474/submissions/new) to stay informed about the latest features and changes to Gor project.
|
||||
|
||||
### Using 1 Gor instance for both listening and replaying
|
||||
It's recommended to use separate server for replaying traffic, but if you have enough CPU resources you can use single Gor instance.
|
||||
|
||||
```
|
||||
sudo gor --input-raw :80 --output-http "http://staging.com"
|
||||
```
|
||||
## Want to Upgrade?
|
||||
|
||||
### Guarantee of replay and HTTP input
|
||||
Due to how traffic interception works, there is chance of missing requests. If you want guarantee that requests will be replayed you can use http input, but it will require changes in your app as well.
|
||||
We have created a [GoReplay PRO](https://goreplay.org/pro.html) extension which provides additional features such as support for binary protocols like Thrift or ProtocolBuffers, saving and replaying from cloud storage, TCP sessions replication, etc. The PRO version also includes a commercial-friendly license, dedicated support, and it also allows you to support high-quality open source development.
|
||||
|
||||
```
|
||||
sudo gor --input-http :28019 --output-http "http://staging.com"
|
||||
```
|
||||
|
||||
Then in your application you should send copy (e.g. like reverse proxy) all incoming requests to Gor http input.
|
||||
## Problems?
|
||||
If you have a problem, please review the [FAQ](https://github.com/buger/goreplay/wiki/FAQ) and [Troubleshooting](https://github.com/buger/goreplay/wiki/Troubleshooting) wiki pages. Searching the [issues](https://github.com/buger/goreplay/issues) for your problem is also a good idea.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Forward to multiple addresses
|
||||
|
||||
You can forward traffic to multiple endpoints. Just add multiple --output-* arguments.
|
||||
```
|
||||
gor --input-tcp :28020 --output-http "http://staging.com" --output-http "http://dev.com"
|
||||
```
|
||||
|
||||
#### Splitting traffic
|
||||
By default it will send same traffic to all outputs, but you have options to equally split it:
|
||||
|
||||
```
|
||||
gor --input-tcp :28020 --output-http "http://staging.com" --output-http "http://dev.com" --split-output true
|
||||
```
|
||||
|
||||
### HTTP output workers
|
||||
By default Gor creates a dynamic pool of workers: it starts with 10 and creates more http output workers when the http output queue length is greater than 10. The number of workers created (N) is equal to the queue length at the time which it is checked and found to have a length greater than 10. The queue length is checked every time a message is written to the http output queue. No more workers will be spawned until that request to spawn N workers is satisfied. If a dynamic worker cannot process a message at that time, it will sleep for 100 milliseconds. If a dynamic worker cannot process a message for 2 seconds it dies.
|
||||
You may specify fixed number of workers using `--output-http-workers=20` option.
|
||||
|
||||
### Follow redirects
|
||||
By default Gor will ignore all redirects since they are handled by clients using your app, but in scenarios where your replayed environment introduces new redirects, you can enable them like this:
|
||||
```
|
||||
gor --input-tcp replay.local:28020 --output-http http://staging.com --output-http-redirects 2
|
||||
```
|
||||
The given example will follow up to 2 redirects per request.
|
||||
|
||||
### HTTP timeouts
|
||||
By default http timeout for both request and response is 5 seconds. You can override it like this:
|
||||
```
|
||||
gor --input-tcp replay.local:28020 --output-http http://staging.com --output-http-timeout 30s
|
||||
```
|
||||
|
||||
### Rate limiting
|
||||
Rate limiting can be useful if you only want to forward parts of production traffic and not overload your staging environment. There are 2 strategies: dropping random requests or dropping fractions of requests based on Header or URL param value.
|
||||
|
||||
#### Dropping random requests
|
||||
Every input and output supports random rate limiting.
|
||||
There are 2 limiting algorithms: absolute or percentage based.
|
||||
|
||||
Absolute: If for current second it reached specified requests limit - disregard the rest, on next second counter reseted.
|
||||
|
||||
Percentage: For input-file it will slowdown or speedup request execution, for the rest it will use random generator to decide if request pass or not based on chance you specified.
|
||||
|
||||
You can specify your desired limit using the
|
||||
"|" operator after the server address:
|
||||
|
||||
#### Limiting replay using absolute number
|
||||
```
|
||||
# staging.server will not get more than 10 requests per second
|
||||
gor --input-tcp :28020 --output-http "http://staging.com|10"
|
||||
```
|
||||
|
||||
#### Limiting listener using percentage based limiter
|
||||
```
|
||||
# replay server will not get more than 10% of requests
|
||||
# useful for high-load environments
|
||||
gor --input-raw :80 --output-tcp "replay.local:28020|10%"
|
||||
```
|
||||
|
||||
#### Limiting based on Header or URL param value
|
||||
If you have unique user id (like API key) stored in header or URL you can consistently forward specified percent of traffic only for fraction of this users.
|
||||
Basic formula looks like this: `FNV32-1A_hashing(value) % 100 >= chance`. Examples:
|
||||
```
|
||||
# Limit based on header value
|
||||
gor --input-raw :80 --output-tcp "replay.local:28020|10%" --http-header-limiter "X-API-KEY: 10%"
|
||||
|
||||
# Limit based on header value
|
||||
gor --input-raw :80 --output-tcp "replay.local:28020|10%" --http-param-limiter "api_key: 10%"
|
||||
```
|
||||
Only percentage based limiting supported.
|
||||
|
||||
### Filtering
|
||||
|
||||
#### Allow url regexp
|
||||
```
|
||||
# only forward requests being sent to the /api endpoint
|
||||
gor --input-raw :8080 --output-http staging.com --http-allow-url /api
|
||||
```
|
||||
|
||||
#### Disallow url regexp
|
||||
```
|
||||
# only forward requests NOT being sent to the /api... endpoint
|
||||
gor --input-raw :8080 --output-http staging.com --http-disallow-url /api
|
||||
```
|
||||
#### Filter based on regexp of header
|
||||
|
||||
```
|
||||
# only forward requests with an api version of 1.0x
|
||||
gor --input-raw :8080 --output-http staging.com --http-allow-header api-version:^1\.0\d
|
||||
|
||||
# only forward requests NOT containing User-Agent header value "Replayed by Gor"
|
||||
gor --input-raw :8080 --output-http staging.com --http-disallow-header "User-Agent: Replayed by Gor"
|
||||
```
|
||||
|
||||
#### Filter based on http method
|
||||
Requests not matching a specified whitelist can be filtered out. For example to strip non-nullipotent requests:
|
||||
|
||||
```
|
||||
gor --input-raw :80 --output-http "http://staging.server" \
|
||||
--http-allow-method GET \
|
||||
--http-allow-method OPTIONS
|
||||
```
|
||||
|
||||
### Rewriting original request
|
||||
Gor supports some basic request rewriting support. For complex logic you can use middleware, see below.
|
||||
|
||||
#### Rewrite URL based on a mapping
|
||||
```
|
||||
# rewrite url to match the following
|
||||
gor --input-raw :8080 --output-http staging.com --http-rewrite-url /v1/user/([^\\/]+)/ping:/v2/user/$1/ping
|
||||
```
|
||||
|
||||
#### Set URL param
|
||||
Set request url param, if param already exists it will be overwritten
|
||||
```
|
||||
gor --input-raw :8080 --output-http staging.com --http-set-param api_key=1
|
||||
```
|
||||
|
||||
#### Set Header
|
||||
Set request header, if header already exists it will be overwritten. This may be useful if you need to identify requests generated by Gor or enable feature flagged functionality in an application:
|
||||
|
||||
```
|
||||
gor --input-raw :80 --output-http "http://staging.server" \
|
||||
--http-header "User-Agent: Replayed by Gor" \
|
||||
--http-header "Enable-Feature-X: true"
|
||||
```
|
||||
|
||||
#### Host header
|
||||
Host header gets special treatment. By default Host get set to the value specified in --output-http. If you manually set --http-header "Host: anonther.com", Gor will not override Host value.
|
||||
|
||||
If you app accepts traffic from multiple domain, and you want to keep original headers, there is specific `--http-original-host` with tells Gor do not touch Host header at all.
|
||||
|
||||
### Middleware
|
||||
Middleware is a program that accepts request and response payload at STDIN and emits modified requests at STDOUT. You can implement any custom logic like stripping private data, advanced rewriting, support for oAuth and etc.
|
||||
|
||||
```
|
||||
Original request +--------------+
|
||||
+-------------+----------STDIN---------->+ |
|
||||
| Gor input | | Middleware |
|
||||
+-------------+----------STDIN---------->+ |
|
||||
Original response +------+---+---+
|
||||
| ^
|
||||
+-------------+ Modified request v |
|
||||
| Gor output +<---------STDOUT-----------------+ |
|
||||
+-----+-------+ |
|
||||
| |
|
||||
| Replayed response |
|
||||
+------------------STDIN----------------->----+
|
||||
```
|
||||
|
||||
Middleware can be written in any language, see `examples/middleware` folder for examples.
|
||||
Middleware program should accept the fact that all communication with Gor is asynchronous, there is no guarantee that original request and response messages will come one after each other. Your app should take care of the state if logic depends on original or replayed response, see `examples/middleware/token_modifier.go` as example.
|
||||
|
||||
Simple bash echo middleware (returns same request) will look like this:
|
||||
```bash
|
||||
while read line; do
|
||||
echo $line
|
||||
end
|
||||
```
|
||||
|
||||
Middleware can be enabled using `--middleware` option, by specifying path to executable file:
|
||||
```
|
||||
gor --input-raw :80 --middleware "/opt/middleware_executable" --output-http "http://staging.server"
|
||||
```
|
||||
|
||||
#### Communication protocol
|
||||
All messages should be hex encoded, new line character specifieds the end of the message, eg. new message per line.
|
||||
|
||||
Decoded payload consist of 2 parts: header and HTTP payload, separated by new line character.
|
||||
|
||||
Example request payload:
|
||||
|
||||
```
|
||||
1 932079936fa4306fc308d67588178d17d823647c 1439818823587396305
|
||||
GET /a HTTP/1.1
|
||||
Host: 127.0.0.1
|
||||
|
||||
```
|
||||
|
||||
Example response payload:
|
||||
|
||||
```
|
||||
2 8e091765ae902fef8a2b7d9dd960e9d52222bd8c 2782013
|
||||
HTTP/1.1 200 OK
|
||||
Date: Mon, 17 Aug 2015 13:40:23 GMT
|
||||
Content-Length: 0
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
```
|
||||
|
||||
Header contains request meta information separated by spaces. First value is payload type, possible values: `1` - request, `2` - original response, `3` - replayed response.
|
||||
Next goes request id: unique among all requests (sha1 of time and Ack), but remain same for original and replayed response, so you can create associations between request and responses. Third argument varies depending on payload type: for request - start time, for responses - round-trip time.
|
||||
|
||||
HTTP payload is unmodified HTTP requests/responses intercepted from network. You can read more about request format [here](http://www.jmarshall.com/easy/http/), [here](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol) and [here](http://www.w3.org/Protocols/rfc2616/rfc2616.html). You can operate with payload as you want, add headers, change path, and etc. Basically you just editing a string, just ensure that it is RCF compliant.
|
||||
|
||||
At the end modified (or untouched) request should be emitted back to STDOUT, keeping original header, and hex-encoded. If you want to filter request, just not send it. Emitting responses back is required, even if you did not touch them.
|
||||
|
||||
#### Advanced example
|
||||
Imagine that you have 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 `examples/middleware/token_modifier.go` and `middleware_test.go#TestTokenMiddleware` as example of described scheme.
|
||||
|
||||
### Saving requests to file and replaying them
|
||||
You can save requests to file, and replay them later:
|
||||
```
|
||||
# write to file
|
||||
gor --input-raw :80 --output-file requests.gor
|
||||
|
||||
# read from file
|
||||
gor --input-file requests.gor --output-http "http://staging.com"
|
||||
```
|
||||
|
||||
**Note:** Replay will preserve the original time differences between requests.
|
||||
|
||||
### Load testing
|
||||
|
||||
Currently it supported only by `input-file` and only when using percentage based limiter. Unlike default limiter for `input-file` instead of dropping requests it will slowdown or speedup request emitting. Note that unlike examples above limiter is applied to input:
|
||||
|
||||
```
|
||||
# Replay from file on 2x speed
|
||||
gor --input-file "requests.gor|200%" --output-http "staging.com"
|
||||
```
|
||||
|
||||
### Basic Auth
|
||||
|
||||
If your development or staging environment is protected by Basic Authentication then those credentials can be injected in during the replay:
|
||||
|
||||
```
|
||||
gor --input-raw :80 --output-http "http://user:pass@staging .com"
|
||||
```
|
||||
|
||||
Note: This will overwrite any Authorization headers in the original request.
|
||||
|
||||
|
||||
## Stats
|
||||
|
||||
Gor can report stats on the `output-tcp` and `output-http` request queues. Stats are reported to the console every 5 seconds in the form `latest,mean,max,count,count/second` by using the `--output-http-stats` and `--output-tcp-stats` options.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
2014/04/23 21:17:50 output_tcp:latest,mean,max,count,count/second
|
||||
2014/04/23 21:17:50 output_tcp:0,0,0,0,0
|
||||
2014/04/23 21:17:55 output_tcp:1,1,2,68,13
|
||||
2014/04/23 21:18:00 output_tcp:1,1,2,92,18
|
||||
2014/04/23 21:18:05 output_tcp:1,1,2,119,23
|
||||
```
|
||||
|
||||
```
|
||||
Version: 0.8
|
||||
2014/04/23 21:19:46 output_http:latest,mean,max,count,count/second
|
||||
2014/04/23 21:19:46 output_http:0,0,0,0,0
|
||||
2014/04/23 21:19:51 output_http:0,0,0,0,0
|
||||
2014/04/23 21:19:56 output_http:0,0,0,0,0
|
||||
2014/04/23 21:20:01 output_http:1,0,1,50,10
|
||||
2014/04/23 21:20:06 output_http:1,1,4,72,14
|
||||
2014/04/23 21:20:11 output_http:1,0,1,179,35
|
||||
2014/04/23 21:20:16 output_http:1,0,1,148,29
|
||||
2014/04/23 21:20:21 output_http:1,1,2,91,18
|
||||
2014/04/23 21:20:26 output_http:1,1,2,150,30
|
||||
2014/04/23 21:18:15 output_http:100,99,100,70,14
|
||||
2014/04/23 21:18:21 output_http:100,99,100,55,11
|
||||
```
|
||||
|
||||
### How can I tell if I have bottlenecks?
|
||||
Key areas that sometimes experience bottlenecks are the output-tcp and output-http functions which have internal queues for requests. Each queue has an upper limit of 100. Enable stats reporting to see if any queues are experiencing bottleneck behavior.
|
||||
|
||||
#### output-http bottlenecks
|
||||
When running a Gor replay the output-http feature may bottleneck if:
|
||||
|
||||
* the replay has inadequate bandwidth. If the replay is receiving or sending more messages than its network adapter can handle the output-http-stats may report that the output-http queue is filling up. See if there is a way to upgrade the replay's bandwidth.
|
||||
* with `--output-http-workers` set to anything other than `-1` the `-output-http` target is unable to respond to messages in a timely manner. The http output workers which take messages off the output-http queue, process the request, and ensure that the request did not result in an error may not be able to keep up with the number of incoming requests. If the replay is not using dynamic worker scaling (`--output-http-workers=-1`) The optimal number of output-http-workers can be determined with the formula `output-workers = (Average number of requests per second)/(Average target response time per second)`.
|
||||
|
||||
#### output-tcp bottlenecks
|
||||
When using the Gor listener the output-tcp feature may bottleneck if:
|
||||
|
||||
* the replay is unable to accept and process more requests than the listener is able generate. Prior to troubleshooting the output-tcp bottleneck, ensure that the replay target is not experiencing any bottlenecks.
|
||||
* the replay target has inadequate bandwidth to handle all its incoming requests. If a replay target's incoming bandwidth is maxed out the output-tcp-stats may report that the output-tcp queue is filling up. See if there is a way to upgrade the replay's bandwidth.
|
||||
|
||||
### ElasticSearch
|
||||
For deep response analyze based on url, cookie, user-agent and etc. you can export response metadata to ElasticSearch. See [ELASTICSEARCH.md](ELASTICSEARCH.md) for more details.
|
||||
|
||||
```
|
||||
gor --input-tcp :80 --output-http "http://staging.com" --output-http-elasticsearch "es_host:api_port/index_name"
|
||||
```
|
||||
|
||||
## Additional help
|
||||
|
||||
Feel free to ask question directly by email or by creating github issue.
|
||||
|
||||
## Latest releases (including binaries)
|
||||
|
||||
https://github.com/buger/gor/releases
|
||||
|
||||
## Command line reference
|
||||
`gor -h` output:
|
||||
```
|
||||
-http-allow-header=[]: A regexp to match a specific header against. Requests with non-matching headers will be dropped:
|
||||
gor --input-raw :8080 --output-http staging.com --http-allow-header api-version:^v1
|
||||
-http-disallow-header=[]: A regexp to match a specific header against. Requests with matching headers will be dropped:
|
||||
gor --input-raw :8080 --output-http staging.com --http-disallow-header "User-Agent: Replayed by Gor"
|
||||
-http-allow-method=[]: Whitelist of HTTP methods to replay. Anything else will be dropped:
|
||||
gor --input-raw :8080 --output-http staging.com --http-allow-method GET --http-allow-method OPTIONS
|
||||
-http-allow-url=[]: A regexp to match requests against. Filter get matched agains full url with domain. Anything else will be dropped:
|
||||
gor --input-raw :8080 --output-http staging.com --http-allow-url ^www.
|
||||
-http-disallow-url=[]: A regexp to match requests against. Filter get matched agains full url with domain. Anything else will be forwarded:
|
||||
gor --input-raw :8080 --output-http staging.com --http-disallow-url ^www.
|
||||
-http-header-limiter=[]: Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific header:
|
||||
gor --input-raw :8080 --output-http staging.com --http-header-imiter user-id:25%
|
||||
-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:
|
||||
gor --input-raw :8080 --output-http staging.com --http-param-limiter user_id:25%
|
||||
-http-rewrite-url=[]: Rewrite the request url based on a mapping:
|
||||
gor --input-raw :8080 --output-http staging.com --http-rewrite-url /v1/user/([^\/]+)/ping:/v2/user/$1/ping
|
||||
-http-set-header=[]: Inject additional headers to http reqest:
|
||||
gor --input-raw :8080 --output-http staging.com --http-set-header 'User-Agent: Gor'
|
||||
-http-set-param=[]: Set request url param, if param already exists it will be overwritten:
|
||||
gor --input-raw :8080 --output-http staging.com --http-set-param api_key=1
|
||||
-input-dummy=[]: Used for testing outputs. Emits 'Get /' request every 1s
|
||||
-input-file=[]: Read requests from file:
|
||||
gor --input-file ./requests.gor --output-http staging.com
|
||||
-input-http=[]: Read requests from HTTP, should be explicitly sent from your application:
|
||||
# Listen for http on 9000
|
||||
gor --input-http :9000 --output-http staging.com
|
||||
-input-raw=[]: Capture traffic from given port (use RAW sockets and require *sudo* access):
|
||||
# Capture traffic from 8080 port
|
||||
gor --input-raw :8080 --output-http staging.com
|
||||
-input-tcp=[]: Used for internal communication between Gor instances. Example:
|
||||
# Receive requests from other Gor instances on 28020 port, and redirect output to staging
|
||||
gor --input-tcp :28020 --output-http staging.com
|
||||
-memprofile="": write memory profile to this file
|
||||
-output-dummy=[]: Used for testing inputs. Just prints data coming from inputs.
|
||||
-output-file=[]: Write incoming requests to file:
|
||||
gor --input-raw :80 --output-file ./requests.gor
|
||||
-output-http=[]: Forwards incoming requests to given http address.
|
||||
# Redirect all incoming requests to staging.com address
|
||||
gor --input-raw :80 --output-http http://staging.com
|
||||
-output-http-elasticsearch="": Send request and response stats to ElasticSearch:
|
||||
gor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'
|
||||
-output-http-header-filter=[]: WARNING: `--output-http-header-filter` DEPRECATED, use `--http-allow-header` instead
|
||||
-output-http-redirects=0: Enable how often redirects should be followed.
|
||||
-output-http-stats=false: Report http output queue stats to console every 5 seconds. Remember to include also `--stats`
|
||||
-output-http-workers=0: Gor uses dynamic worker scaling by default. Enter a number to run a set number of workers.
|
||||
-output-tcp=[]: Used for internal communication between Gor instances. Example:
|
||||
# Listen for requests on 80 port and forward them to other Gor instance on 28020 port
|
||||
gor --input-raw :80 --output-tcp replay.local:28020
|
||||
-output-tcp-stats=false: Report TCP output queue stats to console every 5 seconds. Remember to include also `--stats`
|
||||
-split-output=false: By default each output gets same traffic. If set to `true` it splits traffic equally among all outputs.
|
||||
-stats=false: Turn on queue stats output. Use in combination with the other *-stats flags.
|
||||
-verbose=false: Turn on verbose/debug output
|
||||
```
|
||||
|
||||
## Building from source
|
||||
|
||||
1. Setup standard Go environment http://golang.org/doc/code.html and ensure that $GOPATH environment variable properly set.
|
||||
2. `go get github.com/buger/gor`.
|
||||
3. `cd $GOPATH/src/github.com/buger/gor`
|
||||
4. `go build` to get binary, or `go test` to run tests
|
||||
|
||||
## Development
|
||||
Project contains Docker environment.
|
||||
|
||||
1. Build container: `make dbuild`
|
||||
2. Run all tests: `make dtest`. Run specific test: `make dtest ARGS=-test.run=**regexp**`
|
||||
3. Bash access to container: `make dbash`. Inside container you have python to run simple web server `python -m SimpleHTTPServer 8080` and `curl` to make http requests.
|
||||
|
||||
|
||||
## Questions and support
|
||||
|
||||
All bug-reports and suggestions should go though Github Issues or our [Google Group](https://groups.google.com/forum/#!forum/gor-users). Or you can just send email to gor-users@googlegroups.com
|
||||
|
||||
If you have some private questions you can send direct mail to leonsbox@gmail.com
|
||||
|
||||
## FAQ
|
||||
|
||||
### What OS are supported?
|
||||
For now only Linux based. *BSD (including MacOS is not supported yet, check https://github.com/buger/gor/issues/22 for details)
|
||||
|
||||
### Why does the `--input-raw` requires sudo or root access?
|
||||
Listener works by sniffing traffic from a given port. It's accessible
|
||||
only by using sudo or root access.
|
||||
|
||||
### How do you deal with user session to replay the traffic correctly?
|
||||
You can rewrite session related headers/params to match your staging environment. If you require custom logic (e.g random token based auth) follow this discussion: https://github.com/buger/gor/issues/154
|
||||
|
||||
### Can i use Gor to intercept SSL traffic?
|
||||
Basic idea is that SSL was made to protect itself from traffic interception. There 2 options:
|
||||
1. Move SSL handling to proxy like Nginx or Amazon ELB. And allow Gor to listen on upstreams.
|
||||
2. Use `--input-http` so you can duplicate request payload directly from your app to Gor, but it will require your app modifications.
|
||||
|
||||
More can be find here: https://github.com/buger/gor/issues/85
|
||||
|
||||
### Is there a limit for size of HTTP request when using output-http?
|
||||
Due to the fact that Gor can't guarantee interception of all packets, for large payloads > 200kb there is chance of missing some packets and corrupting body. Treat it as a feature and chance to test broken bodies handling :)
|
||||
The only way to guarantee delivery is using `--input-http`, but you will miss some features.
|
||||
|
||||
### I'm getting 'too many open files' error
|
||||
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
|
||||
|
||||
More about ulimit: http://www.thecodingmachine.com/solving-the-too-many-open-files-exception-in-red5-or-any-other-application/
|
||||
|
||||
### The CPU average across my load-balanced targets is higher than the source
|
||||
If you are replaying traffic from multiple listeners to a load-balanced target and you use sticky sessions, you may observe that the target servers have a higher CPU load than the listener servers. This may be because the sticky session cookie of the original load balancer is not honored by the target load balancer thus resulting in requests that would normally hit the same target server hitting different servers on the backend thus reducing some caching benefits gained via the load balancing. Try running just one listener against one replay target and see if the CPU utilization comparison is more accurate.
|
||||
|
||||
## Tuning
|
||||
|
||||
To achieve the top most performance you should tune the source server system limits:
|
||||
|
||||
net.ipv4.tcp_max_tw_buckets = 65536
|
||||
net.ipv4.tcp_tw_recycle = 1
|
||||
net.ipv4.tcp_tw_reuse = 0
|
||||
net.ipv4.tcp_max_syn_backlog = 131072
|
||||
net.ipv4.tcp_syn_retries = 3
|
||||
net.ipv4.tcp_synack_retries = 3
|
||||
net.ipv4.tcp_retries1 = 3
|
||||
net.ipv4.tcp_retries2 = 8
|
||||
net.ipv4.tcp_rmem = 16384 174760 349520
|
||||
net.ipv4.tcp_wmem = 16384 131072 262144
|
||||
net.ipv4.tcp_mem = 262144 524288 1048576
|
||||
net.ipv4.tcp_max_orphans = 65536
|
||||
net.ipv4.tcp_fin_timeout = 10
|
||||
net.ipv4.tcp_low_latency = 1
|
||||
net.ipv4.tcp_syncookies = 0
|
||||
All bug-reports and suggestions should go through 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.
|
||||
|
||||
|
||||
## Contributing
|
||||
@@ -478,11 +55,19 @@ To achieve the top most performance you should tune the source server system lim
|
||||
|
||||
## Companies using Gor
|
||||
|
||||
* [Granify](http://granify.com)
|
||||
* [GOV.UK](https://www.gov.uk) ([Government Digital Service](http://digital.cabinetoffice.gov.uk/))
|
||||
* [theguardian.com](http://theguardian.com)
|
||||
* [TomTom](http://www.tomtom.com/)
|
||||
* [3SCALE](http://www.3scale.net/)
|
||||
* [Optionlab](http://www.opinionlab.com)
|
||||
* [TubeMogul] (http://tubemogul.com)
|
||||
* To add your company drop me a line to github.com/buger or leonsbox@gmail.com
|
||||
* [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 to add you to the list and share your story, just write to: hello@goreplay.org
|
||||
|
||||
## Author
|
||||
|
||||
Leonid Bugaev, [@buger](https://twitter.com/buger), https://leonsbox.com
|
||||
|
||||
@@ -29,12 +29,15 @@ func Replace(a []byte, from, to int, new []byte) []byte {
|
||||
copy(a[from:from+len(new)], new)
|
||||
|
||||
return a
|
||||
} else if lenDiff < 0 {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// same size
|
||||
copy(a[from:], new)
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -0,0 +1,904 @@
|
||||
/*
|
||||
Package capture provides traffic sniffier using RAW sockets.
|
||||
Capture traffic from socket using RAW_SOCKET's http://en.wikipedia.org/wiki/Raw_socket
|
||||
RAW_SOCKET allows you to listen for traffic from 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 capture
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/google/gopacket/pcap"
|
||||
)
|
||||
|
||||
type packet struct {
|
||||
srcIP []byte
|
||||
data []byte
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// Listener handle traffic capture
|
||||
type Listener struct {
|
||||
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 *packet
|
||||
|
||||
// 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
|
||||
|
||||
bpfFilter string
|
||||
timestampType string
|
||||
overrideSnapLen bool
|
||||
immediateMode bool
|
||||
|
||||
bufferSize int64
|
||||
|
||||
conn net.PacketConn
|
||||
pcapHandles []*pcap.Handle
|
||||
|
||||
quit chan bool
|
||||
ready bool
|
||||
|
||||
protocol TCPProtocol
|
||||
}
|
||||
|
||||
type request struct {
|
||||
id tcpID
|
||||
start time.Time
|
||||
ack uint32
|
||||
}
|
||||
|
||||
// Available engines for intercepting traffic
|
||||
const (
|
||||
EngineRawSocket = 1 << iota
|
||||
EnginePcap
|
||||
EnginePcapFile
|
||||
)
|
||||
|
||||
// NewListener creates and initializes new Listener object
|
||||
func NewListener(addr string, port string, engine int, trackResponse bool, expire time.Duration, protocol TCPProtocol, bpfFilter string, timestampType string, bufferSize int64, overrideSnapLen bool, immediateMode bool) (l *Listener) {
|
||||
l = &Listener{}
|
||||
|
||||
l.packetsChan = make(chan *packet, 10000)
|
||||
l.messagesChan = make(chan *TCPMessage, 10000)
|
||||
l.quit = make(chan bool)
|
||||
|
||||
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.protocol = protocol
|
||||
l.bpfFilter = bpfFilter
|
||||
l.timestampType = timestampType
|
||||
l.immediateMode = immediateMode
|
||||
l.bufferSize = bufferSize
|
||||
l.overrideSnapLen = overrideSnapLen
|
||||
|
||||
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 EnginePcap:
|
||||
go l.readPcap()
|
||||
case EnginePcapFile:
|
||||
go l.readPcapFile()
|
||||
case EngineRawSocket:
|
||||
go l.readRAWSocket()
|
||||
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 packet := <-t.packetsChan:
|
||||
tcpPacket := ParseTCPPacket(packet.srcIP, packet.data, packet.timestamp)
|
||||
t.processTCPPacket(tcpPacket)
|
||||
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)
|
||||
|
||||
if t.protocol == ProtocolHTTP && !message.complete {
|
||||
if !message.IsIncoming {
|
||||
delete(t.respAliases, message.Ack)
|
||||
delete(t.respWithoutReq, message.Ack)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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.setAssocMessage(message)
|
||||
message.setAssocMessage(resp)
|
||||
|
||||
if resp.complete {
|
||||
defer t.dispatchMessage(resp)
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
if resp, ok := t.messages[message.ResponseID]; ok {
|
||||
resp.setAssocMessage(message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if message.AssocMessage == nil {
|
||||
if responseRequest, ok := t.respAliases[message.Ack]; ok {
|
||||
message.setAssocMessage(responseRequest)
|
||||
responseRequest.setAssocMessage(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/goreplay/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 isLoopback(device pcap.Interface) bool {
|
||||
if len(device.Addresses) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch device.Addresses[0].IP.String() {
|
||||
case "127.0.0.1", "::1":
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func listenAllInterfaces(addr string) bool {
|
||||
switch addr {
|
||||
case "", "0.0.0.0", "[::]", "::":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func findPcapDevices(addr string) (interfaces []pcap.Interface, err error) {
|
||||
devices, err := pcap.FindAllDevs()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, device := range devices {
|
||||
if listenAllInterfaces(addr) && len(device.Addresses) > 0 || isLoopback(device) {
|
||||
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}
|
||||
}
|
||||
|
||||
return interfaces, nil
|
||||
}
|
||||
|
||||
func (t *Listener) readPcap() {
|
||||
devices, err := findPcapDevices(t.addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
const BPFSupported = runtime.GOOS != "darwin"
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(devices))
|
||||
|
||||
for _, d := range devices {
|
||||
go func(device pcap.Interface) {
|
||||
inactive, err := pcap.NewInactiveHandle(device.Name)
|
||||
if err != nil {
|
||||
inactive.CleanUp()
|
||||
log.Println("Pcap Error while opening device", device.Name, err)
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
if t.timestampType != "" {
|
||||
if tt, terr := pcap.TimestampSourceFromString(t.timestampType); terr != nil {
|
||||
log.Println("Supported timestamp types: ", inactive.SupportedTimestamps(), device.Name)
|
||||
} else if terr := inactive.SetTimestampSource(tt); terr != nil {
|
||||
log.Println("Supported timestamp types: ", inactive.SupportedTimestamps(), device.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if it, err := net.InterfaceByName(device.Name); err == nil && !t.overrideSnapLen {
|
||||
// Auto-guess max length of packet to capture
|
||||
inactive.SetSnapLen(it.MTU + 68*2)
|
||||
} else {
|
||||
inactive.SetSnapLen(65536)
|
||||
}
|
||||
inactive.SetTimeout(t.messageExpire)
|
||||
inactive.SetPromisc(true)
|
||||
inactive.SetImmediateMode(t.immediateMode)
|
||||
if t.immediateMode {
|
||||
log.Println("Setting immediate mode")
|
||||
}
|
||||
if t.bufferSize > 0 {
|
||||
inactive.SetBufferSize(int(t.bufferSize))
|
||||
}
|
||||
|
||||
handle, herr := inactive.Activate()
|
||||
if herr != nil {
|
||||
log.Printf("PCAP Activate device '%s' error: %s\n", device.Name, herr)
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
defer handle.Close()
|
||||
|
||||
t.Lock()
|
||||
t.pcapHandles = append(t.pcapHandles, handle)
|
||||
|
||||
var bpfDstHost, bpfSrcHost string
|
||||
var loopback = isLoopback(device)
|
||||
|
||||
if loopback {
|
||||
var allAddr []string
|
||||
for _, dc := range devices {
|
||||
for _, addr := range dc.Addresses {
|
||||
allAddr = append(allAddr, "(dst host "+addr.IP.String()+" and src host "+addr.IP.String()+")")
|
||||
}
|
||||
}
|
||||
|
||||
bpfDstHost = strings.Join(allAddr, " or ")
|
||||
bpfSrcHost = bpfDstHost
|
||||
} else {
|
||||
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.bpfFilter != "" {
|
||||
bpf = t.bpfFilter
|
||||
} else {
|
||||
if t.trackResponse {
|
||||
bpf = fmt.Sprintf("(tcp dst port %d and (%s)) or (tcp src port %d and (%s))", t.port, bpfDstHost, t.port, bpfSrcHost)
|
||||
} else {
|
||||
bpf = fmt.Sprintf("(tcp dst port %d and (%s))", t.port, bpfDstHost)
|
||||
}
|
||||
}
|
||||
|
||||
if err := handle.SetBPFFilter(bpf); err != nil {
|
||||
log.Println("BPF filter error:", err, "Device:", device.Name, bpf)
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
t.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, layers.LayerTypeIPv4:
|
||||
of = 0
|
||||
case layers.LinkTypeLinuxSLL:
|
||||
of = 16
|
||||
default:
|
||||
log.Println("Unknown packet layer", decoder, packet)
|
||||
break
|
||||
}
|
||||
data = packet.Data()[of:]
|
||||
|
||||
version := uint8(data[0]) >> 4
|
||||
ipLength := int(binary.BigEndian.Uint16(data[2: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]
|
||||
|
||||
// Too small IP packet
|
||||
if ipLength < 20 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Invalid length
|
||||
if int(ihl*4) > ipLength {
|
||||
continue
|
||||
}
|
||||
|
||||
if cmp := len(data) - ipLength; cmp > 0 {
|
||||
data = data[:ipLength]
|
||||
} else if cmp < 0 {
|
||||
// Truncated packet
|
||||
continue
|
||||
}
|
||||
|
||||
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] >> 4
|
||||
isFIN := data[13]&0x01 != 0
|
||||
|
||||
// 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) || isFIN {
|
||||
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
|
||||
|
||||
if loopback {
|
||||
for _, dc := range devices {
|
||||
if addrMatched {
|
||||
break
|
||||
}
|
||||
for _, a := range dc.Addresses {
|
||||
if a.IP.Equal(net.IP(addrCheck)) {
|
||||
addrMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
addrMatched = true
|
||||
} else {
|
||||
for _, a := range device.Addresses {
|
||||
if a.IP.Equal(net.IP(addrCheck)) {
|
||||
addrMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !addrMatched {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
t.packetsChan <- t.buildPacket(srcIP, data, packet.Metadata().Timestamp)
|
||||
}
|
||||
}
|
||||
}(d)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
t.Lock()
|
||||
t.ready = true
|
||||
t.Unlock()
|
||||
}
|
||||
|
||||
func (t *Listener) readPcapFile() {
|
||||
if handle, err := pcap.OpenOffline(t.addr); err != nil {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
if t.bpfFilter != "" {
|
||||
if err := handle.SetBPFFilter(t.bpfFilter); err != nil {
|
||||
log.Println("BPF filter error:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.Lock()
|
||||
t.ready = true
|
||||
t.Unlock()
|
||||
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
|
||||
|
||||
for {
|
||||
packet, err := packetSource.NextPacket()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
log.Println("Error:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var addr, data []byte
|
||||
|
||||
if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil {
|
||||
tcp, _ := tcpLayer.(*layers.TCP)
|
||||
data = append(tcp.LayerContents(), tcp.LayerPayload()...)
|
||||
|
||||
if uint16(tcp.DstPort) == t.port {
|
||||
copy(data[0:2], []byte{byte(tcp.SrcPort >> 8), byte(tcp.SrcPort)})
|
||||
copy(data[2:4], []byte{byte(tcp.DstPort >> 8), byte(tcp.DstPort)})
|
||||
} else {
|
||||
copy(data[0:2], []byte{byte(tcp.DstPort >> 8), byte(tcp.DstPort)})
|
||||
copy(data[2:4], []byte{byte(tcp.SrcPort >> 8), byte(tcp.SrcPort)})
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer != nil {
|
||||
ip, _ := ipLayer.(*layers.IPv4)
|
||||
addr = ip.SrcIP
|
||||
} else if ipLayer = packet.Layer(layers.LayerTypeIPv6); ipLayer != nil {
|
||||
ip, _ := ipLayer.(*layers.IPv6)
|
||||
addr = ip.SrcIP
|
||||
} else {
|
||||
// log.Println("Can't find IP layer", packet)
|
||||
continue
|
||||
}
|
||||
|
||||
dataOffset := data[12] >> 4
|
||||
isFIN := data[13]&0x01 != 0
|
||||
|
||||
// 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) && !isFIN {
|
||||
continue
|
||||
}
|
||||
|
||||
t.packetsChan <- t.buildPacket(addr, data, packet.Metadata().Timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
type RSPacket struct {
|
||||
buf []byte
|
||||
addr net.Addr
|
||||
err error
|
||||
n int
|
||||
}
|
||||
var bufChan = make(chan *RSPacket, 1000)
|
||||
t.Lock()
|
||||
t.ready = true
|
||||
t.Unlock()
|
||||
go func() {
|
||||
for {
|
||||
// Re-allocate data object to avoid data collision
|
||||
var buf [64 * 104 * 1024]byte
|
||||
// Note: ReadFrom receive messages without IP header
|
||||
n, addr, err := t.conn.ReadFrom(buf[:])
|
||||
bufChan <- &RSPacket{buf[:], addr, err, n}
|
||||
}
|
||||
}()
|
||||
for {
|
||||
packet := <-bufChan
|
||||
if packet.err != nil {
|
||||
if strings.HasSuffix(packet.err.Error(), "closed network connection") {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if packet.n > 0 {
|
||||
if t.isValidPacket(packet.buf[:packet.n]) {
|
||||
t.packetsChan <- t.buildPacket([]byte(packet.addr.(*net.IPAddr).IP), packet.buf[:packet.n], time.Now())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Listener) buildPacket(packetSrcIP []byte, packetData []byte, timestamp time.Time) *packet {
|
||||
return &packet{
|
||||
srcIP: packetSrcIP,
|
||||
data: packetData,
|
||||
timestamp: timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
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] >> 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) {
|
||||
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()))
|
||||
}
|
||||
}()
|
||||
|
||||
var responseRequest *TCPMessage
|
||||
var message *TCPMessage
|
||||
|
||||
isIncoming := packet.DestPort == t.port
|
||||
|
||||
if t.protocol == ProtocolHTTP {
|
||||
if !isIncoming {
|
||||
responseRequest, _ = t.respAliases[packet.Ack]
|
||||
}
|
||||
|
||||
// Seek for 100-expect chunks
|
||||
// `packet.Ack != parentAck` is protection for clients who send data without ignoring server 100-continue response, e.g have data chunks have same Ack
|
||||
if parentAck, ok := t.seqWithData[packet.Seq]; ok && packet.Ack != parentAck {
|
||||
// Skip zero-length chunks https://github.com/buger/goreplay/issues/496
|
||||
if len(packet.Data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 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.setAssocMessage(nil)
|
||||
m.setAssocMessage(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 isIncoming && packet.IsFIN {
|
||||
if ma, ok := t.respAliases[packet.Seq]; ok {
|
||||
if ma.packets[0].SrcPort == packet.SrcPort {
|
||||
packet.UpdateAck(ma.Ack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if alias, ok := t.ackAliases[packet.Ack]; ok {
|
||||
packet.UpdateAck(alias)
|
||||
}
|
||||
|
||||
message, ok := t.messages[packet.ID]
|
||||
|
||||
if !ok {
|
||||
message = NewTCPMessage(packet.Seq, packet.Ack, isIncoming, t.protocol, packet.timestamp)
|
||||
t.messages[packet.ID] = message
|
||||
|
||||
if !isIncoming {
|
||||
if responseRequest != nil {
|
||||
message.setAssocMessage(responseRequest)
|
||||
responseRequest.setAssocMessage(message)
|
||||
} else {
|
||||
t.respWithoutReq[packet.Ack] = packet.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adding packet to message
|
||||
message.AddPacket(packet)
|
||||
|
||||
// Handling Expect: 100-continue requests
|
||||
if t.protocol == ProtocolHTTP && message.expectType == httpExpect100Continue && len(message.packets) == message.headerPacket+1 {
|
||||
seq := packet.Seq + uint32(len(packet.Data))
|
||||
t.seqWithData[seq] = packet.Ack
|
||||
|
||||
message.DataSeq = seq
|
||||
message.complete = false
|
||||
|
||||
// In case if sequence packet came first
|
||||
for _, m := range t.messages {
|
||||
if m.Seq == seq {
|
||||
t.deleteMessage(m)
|
||||
if m.AssocMessage != nil {
|
||||
message.setAssocMessage(m.AssocMessage)
|
||||
m.AssocMessage.setAssocMessage(nil)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// If client do sends Expect: 100-continue but do not respect server response
|
||||
if message.expectType == httpExpect100Continue && (message.headerPacket != -1 && len(message.packets) > message.headerPacket+1) {
|
||||
delete(t.seqWithData, message.DataSeq)
|
||||
seq := packet.Seq + uint32(len(packet.Data))
|
||||
t.seqWithData[seq] = packet.Ack
|
||||
message.DataSeq = seq
|
||||
}
|
||||
|
||||
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.complete {
|
||||
// log.Println("COMPLETE!", isIncoming, message)
|
||||
if isIncoming {
|
||||
if t.trackResponse {
|
||||
// log.Println("Found response!", message.ResponseID, t.messages)
|
||||
|
||||
if resp, ok := t.messages[message.ResponseID]; ok {
|
||||
if resp.complete {
|
||||
t.dispatchMessage(resp)
|
||||
}
|
||||
|
||||
t.dispatchMessage(message)
|
||||
}
|
||||
} else {
|
||||
t.dispatchMessage(message)
|
||||
}
|
||||
} else {
|
||||
if message.AssocMessage == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req, ok := t.messages[message.AssocMessage.ID()]; ok {
|
||||
if req.complete {
|
||||
t.dispatchMessage(req)
|
||||
t.dispatchMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Receiver TCP messages from the listener channel
|
||||
func (t *Listener) Receiver() chan *TCPMessage {
|
||||
return t.messagesChan
|
||||
}
|
||||
|
||||
// Close tcp listener
|
||||
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,617 @@
|
||||
package capture
|
||||
|
||||
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, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket := buildPacket(true, 1, 1, []byte("GET / HTTP/1.1\r\n\r\n"), time.Now())
|
||||
|
||||
respAck := reqPacket.Seq + uint32(len(reqPacket.Data))
|
||||
respPacket := buildPacket(false, respAck, reqPacket.Seq+1, []byte("HTTP/1.1 200 OK\r\n\r\n"), time.Now())
|
||||
|
||||
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 firstPacket(payload []byte) *TCPPacket {
|
||||
return buildPacket(
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
payload,
|
||||
time.Now(),
|
||||
)
|
||||
}
|
||||
|
||||
func nextPacket(prev *TCPPacket, payload []byte) *TCPPacket {
|
||||
return buildPacket(
|
||||
prev.SrcPort == 1,
|
||||
prev.Ack,
|
||||
prev.Seq+uint32(len(prev.Data)),
|
||||
payload,
|
||||
prev.timestamp.Add(time.Millisecond),
|
||||
)
|
||||
}
|
||||
|
||||
func responsePacket(prev *TCPPacket, payload []byte) *TCPPacket {
|
||||
return buildPacket(
|
||||
!(prev.SrcPort == 1),
|
||||
prev.Seq+uint32(len(prev.Data)),
|
||||
prev.Ack,
|
||||
payload,
|
||||
prev.timestamp.Add(time.Millisecond),
|
||||
)
|
||||
}
|
||||
|
||||
func TestHEADRequestNoBody(t *testing.T) {
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket := firstPacket([]byte("HEAD / HTTP/1.1\r\nContent-Length: 0\r\n\r\n"))
|
||||
respPacket := responsePacket(reqPacket, []byte("HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n"))
|
||||
|
||||
listener.packetsChan <- reqPacket.dump()
|
||||
listener.packetsChan <- respPacket.dump()
|
||||
|
||||
var req, resp *TCPMessage
|
||||
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 TestSingleAck100Continue(t *testing.T) {
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket1 := firstPacket([]byte("POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 4\r\n\r\n"))
|
||||
respPacket1 := responsePacket(reqPacket1, []byte(""))
|
||||
respPacket2 := responsePacket(reqPacket1, []byte("HTTP/1.1 100 Continue\r\n"))
|
||||
reqPacket2 := responsePacket(respPacket2, []byte("DATA"))
|
||||
respPacket3 := responsePacket(reqPacket2, []byte("HTTP/1.1 200 OK\r\n\r\n"))
|
||||
|
||||
result := []byte("POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nDATA")
|
||||
|
||||
testRawListener100Continue(t, listener, result,
|
||||
reqPacket1,
|
||||
respPacket1, respPacket2,
|
||||
reqPacket2,
|
||||
respPacket3)
|
||||
}
|
||||
|
||||
func Test100ContinueWithoutWaiting(t *testing.T) {
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
req1 := firstPacket([]byte("POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 4\r\n\r\n"))
|
||||
req2 := nextPacket(req1, []byte("DATA"))
|
||||
resp1 := responsePacket(req1, []byte("HTTP/1.1 100 Continue\r\n"))
|
||||
resp2 := responsePacket(req2, []byte("HTTP/1.1 200 OK\r\n\r\n"))
|
||||
|
||||
result := []byte("POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nDATA")
|
||||
|
||||
testRawListener100Continue(t, listener, result,
|
||||
req1, req2, resp1, resp2)
|
||||
}
|
||||
|
||||
// Client first sends data without waiting 100-continue, but once response received, generate packets based on Ack payload
|
||||
func Test100ContinueMixed(t *testing.T) {
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
req1 := firstPacket([]byte("POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 12\r\n\r\n"))
|
||||
req2 := nextPacket(req1, []byte("DAT1"))
|
||||
resp1 := responsePacket(req1, []byte("HTTP/1.1 100 Continue\r\n\r\n"))
|
||||
req3 := responsePacket(resp1, []byte("DAT2"))
|
||||
req3.Seq = req2.Seq + uint32(len(req2.Data))
|
||||
req4 := nextPacket(req3, []byte("DAT3"))
|
||||
resp2 := responsePacket(req4, []byte("HTTP/1.1 200 OK\r\n\r\n"))
|
||||
|
||||
result := []byte("POST / HTTP/1.1\r\nContent-Length: 12\r\n\r\nDAT1DAT2DAT3")
|
||||
|
||||
testRawListener100Continue(t, listener, result,
|
||||
req1, req2, req3, req4, resp1, resp2)
|
||||
}
|
||||
|
||||
func TestDoubleAck100Continue(t *testing.T) {
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket1 := firstPacket([]byte("POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 4\r\n\r\n"))
|
||||
|
||||
respPacket1 := responsePacket(reqPacket1, []byte(""))
|
||||
respPacket2 := responsePacket(reqPacket1, []byte("HTTP/1.1 100 Continue\r\n"))
|
||||
reqPacket2 := responsePacket(respPacket2, []byte(""))
|
||||
reqPacket3 := responsePacket(respPacket2, []byte("DATA"))
|
||||
respPacket3 := responsePacket(reqPacket3, []byte("HTTP/1.1 200 OK\r\n\r\n"))
|
||||
|
||||
result := []byte("POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nDATA")
|
||||
|
||||
testRawListener100Continue(t, listener, result,
|
||||
reqPacket1,
|
||||
respPacket1, respPacket2,
|
||||
reqPacket2, reqPacket3,
|
||||
respPacket3)
|
||||
}
|
||||
|
||||
func TestRawListenerInputResponseByClose(t *testing.T) {
|
||||
var req, resp *TCPMessage
|
||||
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket := buildPacket(true, 1, 1, []byte("GET / HTTP/1.1\r\n\r\n"), time.Now())
|
||||
|
||||
respAck := reqPacket.Seq + uint32(len(reqPacket.Data))
|
||||
respPacket := buildPacket(false, respAck, reqPacket.Seq+1, []byte("HTTP/1.1 200 OK\r\nConnection: close\r\n\r\nasd"), time.Now())
|
||||
finPacket := buildPacket(false, respAck, reqPacket.Seq+2, []byte(""), time.Now())
|
||||
finPacket.IsFIN = true
|
||||
|
||||
listener.packetsChan <- reqPacket.dump()
|
||||
listener.packetsChan <- respPacket.dump()
|
||||
listener.packetsChan <- finPacket.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, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket := buildPacket(true, 1, 1, []byte("GET / HTTP/1.1\r\n\r\n"), time.Now())
|
||||
|
||||
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, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket := firstPacket([]byte("GET / HTTP/1.1\r\n\r\n"))
|
||||
respPacket := responsePacket(reqPacket, []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 request 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 get100ContinuePackets() (req []*TCPPacket, resp []*TCPPacket) {
|
||||
req1 := firstPacket([]byte("POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 2\r\n\r\n"))
|
||||
resp1 := responsePacket(req1, []byte("HTTP/1.1 100 Continue\r\n"))
|
||||
req2 := responsePacket(resp1, []byte("a"))
|
||||
req3 := nextPacket(req2, []byte("b"))
|
||||
resp2 := responsePacket(req3, []byte("HTTP/1.1 200 OK\r\n\r\n"))
|
||||
|
||||
return []*TCPPacket{req1, req2, req3}, []*TCPPacket{resp1, resp2}
|
||||
}
|
||||
|
||||
func TestShort100Continue(t *testing.T) {
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
req, resp := get100ContinuePackets()
|
||||
|
||||
result := []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\nab")
|
||||
|
||||
testRawListener100Continue(t, listener, result, req[0], req[1], req[2], resp[0], resp[1])
|
||||
}
|
||||
|
||||
// Response comes before Request
|
||||
func Test100ContinueWrongOrder(t *testing.T) {
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
req, resp := get100ContinuePackets()
|
||||
|
||||
result := []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\nab")
|
||||
|
||||
testRawListener100Continue(t, listener, result, resp[0], resp[1], req[0], req[1], req[2])
|
||||
}
|
||||
|
||||
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.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)
|
||||
}
|
||||
}
|
||||
|
||||
// permutation using heap algorithm https://en.wikipedia.org/wiki/Heap%27s_algorithm
|
||||
func permutation(a []*TCPPacket, f func([]*TCPPacket)) {
|
||||
n := len(a)
|
||||
c := make([]int, n)
|
||||
f(a)
|
||||
i := 0
|
||||
for i < n {
|
||||
if c[i] < i {
|
||||
if i&1 != 1 {
|
||||
a[0], a[i] = a[i], a[0]
|
||||
} else {
|
||||
a[c[i]], a[i] = a[i], a[c[i]]
|
||||
}
|
||||
f(a)
|
||||
c[i]++
|
||||
i = 0
|
||||
} else {
|
||||
c[i] = 0
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Response comes before Request
|
||||
func TestRawListenerChunkedWrongOrder(t *testing.T) {
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket1 := firstPacket([]byte("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nExpect: 100-continue\r\n\r\n"))
|
||||
|
||||
respPacket1 := responsePacket(reqPacket1, []byte("HTTP/1.1 100 Continue\r\n"))
|
||||
reqPacket2 := responsePacket(respPacket1, []byte("1\r\na\r\n"))
|
||||
reqPacket3 := nextPacket(reqPacket2, []byte("1\r\nb\r\n"))
|
||||
reqPacket4 := nextPacket(reqPacket3, []byte("0\r\n\r\n"))
|
||||
|
||||
respPacket2 := responsePacket(reqPacket4, []byte("HTTP/1.1 200 OK\r\n\r\n"))
|
||||
|
||||
f := func(p []*TCPPacket) {
|
||||
testChunkedSequence(t, listener, p...)
|
||||
}
|
||||
// Should re-construct message from all possible combinations
|
||||
permutation([]*TCPPacket{reqPacket1, reqPacket2, reqPacket3, reqPacket4, respPacket1, respPacket2}, f)
|
||||
}
|
||||
|
||||
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"), time.Now())
|
||||
// Packet with data have different Seq
|
||||
reqPacket2 := buildPacket(true, ack, seq+47, []byte("1\r\na\r\n"), time.Now())
|
||||
reqPacket3 := buildPacket(true, ack, reqPacket2.Seq+5, []byte("1\r\nb\r\n"), time.Now())
|
||||
reqPacket4 := buildPacket(true, ack, reqPacket3.Seq+5, []byte("0\r\n\r\n"), time.Now())
|
||||
|
||||
respPacket := buildPacket(false, reqPacket4.Seq+5 /* len of data */, ack, []byte("HTTP/1.1 200 OK\r\n\r\n"), time.Now())
|
||||
|
||||
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, time.Now()),
|
||||
buildPacket(false, seq+uint32(len(data)), seq2, []byte("HTTP/1.1 200 OK\r\n\r\n"), time.Now()),
|
||||
}
|
||||
}
|
||||
|
||||
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"), time.Now()),
|
||||
buildPacket(false, seq+18, seq2, []byte("HTTP/1.1 200 OK\r\n\r\n"), time.Now()),
|
||||
}
|
||||
}
|
||||
|
||||
// Response comes before Request
|
||||
func TestRawListenerBench(t *testing.T) {
|
||||
l := NewListener("", "0", EnginePcap, true, 200*time.Millisecond, ProtocolHTTP, "", "", 0, false, false)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseZeroContentLength(t *testing.T) {
|
||||
var req, resp *TCPMessage
|
||||
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond, ProtocolHTTP, "", "", 0, false, false)
|
||||
defer listener.Close()
|
||||
|
||||
reqPacket := firstPacket([]byte("POST /api/setup/install HTTP/1.1\r\nHost: localhost:22936\r\nUser-Agent: curl/7.57.0\r\nAccept: */*\r\nContent-Length: 0\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\n"))
|
||||
respPacket := responsePacket(reqPacket, []byte("HTTP/1.1 200\r\nDate: Fri, 11 May 2018 15:09:10 GMT\r\nServer: Kestrel\r\nCache-Control: no-cache\r\nTransfer-Encoding: chunked\r\n\r\n"))
|
||||
respPacket2 := nextPacket(respPacket, []byte("0\r\n\r\n"))
|
||||
|
||||
// If response packet comes before request
|
||||
listener.packetsChan <- reqPacket.dump()
|
||||
listener.packetsChan <- respPacket.dump()
|
||||
listener.packetsChan <- respPacket2.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(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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
// TCPProtocol is a number to indicate type of protocol
|
||||
type TCPProtocol uint8
|
||||
|
||||
const (
|
||||
// ProtocolHTTP ...
|
||||
ProtocolHTTP TCPProtocol = 0
|
||||
// ProtocolBinary ...
|
||||
ProtocolBinary TCPProtocol = 1
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
protocol TCPProtocol
|
||||
|
||||
/* HTTP specific variables */
|
||||
methodType httpMethodType
|
||||
bodyType httpBodyType
|
||||
expectType httpExpectType
|
||||
seqMissing bool
|
||||
headerPacket int
|
||||
contentLength int
|
||||
complete bool
|
||||
}
|
||||
|
||||
// NewTCPMessage pointer created from a Acknowledgment number and a channel of messages readuy to be deleted
|
||||
func NewTCPMessage(Seq, Ack uint32, IsIncoming bool, protocol TCPProtocol, timestamp time.Time) (msg *TCPMessage) {
|
||||
msg = &TCPMessage{Seq: Seq, Ack: Ack, IsIncoming: IsIncoming, protocol: protocol, Start: timestamp}
|
||||
msg.Start = time.Now()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *TCPMessage) packetsData() (d [][]byte) {
|
||||
d = make([][]byte, len(t.packets))
|
||||
for i, p := range t.packets {
|
||||
d[i] = p.Data
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Bytes return message content
|
||||
func (t *TCPMessage) Bytes() (output []byte) {
|
||||
for _, p := range t.packets {
|
||||
output = append(output, p.Data...)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// BodySize returns total body size
|
||||
func (t *TCPMessage) BodySize() (size int) {
|
||||
if len(t.packets) == 0 || t.headerPacket == -1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
size += len(proto.Body(t.packets[t.headerPacket].Data))
|
||||
|
||||
for _, p := range t.packets[t.headerPacket+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) {
|
||||
for _, pkt := range t.packets {
|
||||
if packet.Seq == pkt.Seq {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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 packet.OrigAck != 0 {
|
||||
t.DataAck = packet.OrigAck
|
||||
}
|
||||
|
||||
if packet.timestamp.Before(t.Start) || t.Start.IsZero() {
|
||||
t.Start = packet.timestamp
|
||||
}
|
||||
|
||||
if packet.timestamp.After(t.End) || t.End.IsZero() {
|
||||
t.End = packet.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
t.checkSeqIntegrity()
|
||||
|
||||
if t.protocol == ProtocolHTTP {
|
||||
t.updateHeadersPacket()
|
||||
t.updateMethodType()
|
||||
t.updateBodyType()
|
||||
t.check100Continue()
|
||||
t.checkIfComplete()
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is missing packet
|
||||
func (t *TCPMessage) checkSeqIntegrity() {
|
||||
if len(t.packets) == 1 {
|
||||
t.seqMissing = false
|
||||
}
|
||||
|
||||
offset := len(t.packets) - 1
|
||||
|
||||
if t.packets[offset].IsFIN {
|
||||
offset--
|
||||
|
||||
if offset < 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for i, p := range t.packets[:offset] {
|
||||
if p.IsFIN {
|
||||
continue
|
||||
}
|
||||
|
||||
// If final packet
|
||||
if len(t.packets) == i+1 {
|
||||
t.seqMissing = false
|
||||
return
|
||||
}
|
||||
np := t.packets[i+1]
|
||||
|
||||
nextSeq := p.Seq + uint32(len(p.Data))
|
||||
|
||||
if np.Seq != nextSeq {
|
||||
if t.protocol == ProtocolHTTP && t.expectType == httpExpect100Continue {
|
||||
if np.Seq != nextSeq+22 {
|
||||
t.seqMissing = true
|
||||
return
|
||||
}
|
||||
} else {
|
||||
t.seqMissing = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.seqMissing = false
|
||||
}
|
||||
|
||||
var bEmptyLine = []byte("\r\n\r\n")
|
||||
var bBR = []byte("\r\n")
|
||||
|
||||
// last-chunk always is 0\r\n\r\n\. More info https://tools.ietf.org/html/rfc2616#section-3.6.1
|
||||
var bChunkEnd = []byte("0\r\n\r\n")
|
||||
|
||||
func (t *TCPMessage) updateHeadersPacket() {
|
||||
if len(t.packets) == 1 {
|
||||
t.headerPacket = -1
|
||||
}
|
||||
|
||||
if t.headerPacket != -1 {
|
||||
return
|
||||
}
|
||||
|
||||
if t.seqMissing {
|
||||
return
|
||||
}
|
||||
|
||||
for i, p := range t.packets {
|
||||
if len(p.Data) >= len(bEmptyLine) {
|
||||
if bytes.LastIndex(p.Data, bEmptyLine) != -1 {
|
||||
t.headerPacket = i
|
||||
return
|
||||
}
|
||||
} else if i > 0 && bytes.Equal(p.Data, bBR) {
|
||||
idx := bytes.LastIndex(t.packets[i-1].Data, bBR)
|
||||
if idx != -1 && idx == len(t.packets[i-1].Data)-len(bBR) {
|
||||
t.headerPacket = i
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// checkIfComplete returns true if all of the packets that compse the message arrived.
|
||||
func (t *TCPMessage) checkIfComplete() {
|
||||
if t.seqMissing || t.headerPacket == -1 {
|
||||
// log.Println("Seq missing", t.seqMissing, t.packets)
|
||||
return
|
||||
}
|
||||
|
||||
if t.methodType == httpMethodNotFound {
|
||||
// log.Println("Method missing", t.methodType, t.packets)
|
||||
return
|
||||
}
|
||||
|
||||
// Responses can be emitted only if we found request
|
||||
if !t.IsIncoming && t.AssocMessage == nil {
|
||||
// log.Println("Assoc not found", t)
|
||||
return
|
||||
}
|
||||
|
||||
// log.Println("Found?", t)
|
||||
|
||||
switch t.bodyType {
|
||||
case httpBodyEmpty:
|
||||
t.complete = true
|
||||
case httpBodyContentLength:
|
||||
if t.contentLength == 0 || t.contentLength == t.BodySize() {
|
||||
t.complete = true
|
||||
}
|
||||
case httpBodyChunked:
|
||||
lastPacket := t.packets[len(t.packets)-1]
|
||||
if bytes.LastIndex(lastPacket.Data, bChunkEnd) != -1 {
|
||||
t.complete = true
|
||||
}
|
||||
default:
|
||||
if len(t.packets) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
last := t.packets[len(t.packets)-1]
|
||||
if last.IsFIN {
|
||||
t.complete = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type httpMethodType uint8
|
||||
|
||||
const (
|
||||
httpMethodNotSet httpMethodType = 0
|
||||
httpMethodKnown httpMethodType = 1
|
||||
httpMethodNotFound httpMethodType = 2
|
||||
)
|
||||
|
||||
func (t *TCPMessage) updateMethodType() {
|
||||
// if there is cache
|
||||
if t.methodType != httpMethodNotSet && t.methodType != httpMethodNotFound {
|
||||
return
|
||||
}
|
||||
|
||||
d := t.packets[0].Data
|
||||
|
||||
// Minimum length fo request: GET / HTTP/1.1\r\n
|
||||
|
||||
if len(d) < 16 {
|
||||
t.methodType = httpMethodNotFound
|
||||
return
|
||||
}
|
||||
|
||||
if t.IsIncoming {
|
||||
if mIdx := bytes.IndexByte(d[:8], ' '); mIdx != -1 {
|
||||
// Check that after method we have absolute or relative path
|
||||
switch d[mIdx+1] {
|
||||
case '/', 'h', '*':
|
||||
default:
|
||||
t.methodType = httpMethodNotFound
|
||||
return
|
||||
}
|
||||
} else {
|
||||
t.methodType = httpMethodNotFound
|
||||
return
|
||||
}
|
||||
|
||||
t.methodType = httpMethodKnown
|
||||
} else {
|
||||
if !bytes.Equal(d[:6], []byte("HTTP/1")) {
|
||||
t.methodType = httpMethodNotFound
|
||||
return
|
||||
}
|
||||
|
||||
t.methodType = httpMethodKnown
|
||||
}
|
||||
}
|
||||
|
||||
type httpBodyType uint8
|
||||
|
||||
const (
|
||||
httpBodyNotSet httpBodyType = 0
|
||||
httpBodyEmpty httpBodyType = 1
|
||||
httpBodyContentLength httpBodyType = 2
|
||||
httpBodyChunked httpBodyType = 3
|
||||
httpBodyConnectionClose httpBodyType = 4
|
||||
)
|
||||
|
||||
func (t *TCPMessage) updateBodyType() {
|
||||
// if there is cache
|
||||
if t.bodyType != httpBodyNotSet {
|
||||
return
|
||||
}
|
||||
|
||||
// Headers not received
|
||||
if t.headerPacket == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
var lengthB, encB, connB []byte
|
||||
|
||||
proto.ParseHeaders(t.packetsData(), func(header, value []byte) bool {
|
||||
if proto.HeadersEqual(header, []byte("Content-Length")) {
|
||||
lengthB = value
|
||||
return false
|
||||
}
|
||||
|
||||
if proto.HeadersEqual(header, []byte("Transfer-Encoding")) {
|
||||
encB = value
|
||||
return false
|
||||
}
|
||||
|
||||
if proto.HeadersEqual(header, []byte("Connection")) {
|
||||
connB = value
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
switch t.methodType {
|
||||
case httpMethodNotFound:
|
||||
return
|
||||
case httpMethodKnown:
|
||||
|
||||
if !t.IsIncoming &&
|
||||
t.AssocMessage != nil &&
|
||||
bytes.IndexByte(t.AssocMessage.Bytes(), ' ') > -1 &&
|
||||
bytes.Equal([]byte("HEAD"), proto.Method(t.AssocMessage.Bytes())) {
|
||||
// Need to check if this is a response to a head request,
|
||||
// in which case the body has to be empty regardless.
|
||||
t.bodyType = httpBodyEmpty
|
||||
return
|
||||
}
|
||||
|
||||
if len(lengthB) > 0 {
|
||||
t.contentLength, _ = strconv.Atoi(string(lengthB))
|
||||
if t.contentLength == 0 {
|
||||
t.bodyType = httpBodyEmpty
|
||||
} else {
|
||||
t.bodyType = httpBodyContentLength
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(encB) > 0 {
|
||||
t.bodyType = httpBodyChunked
|
||||
return
|
||||
}
|
||||
|
||||
if len(connB) > 0 && bytes.Equal(connB, []byte("close")) {
|
||||
t.bodyType = httpBodyConnectionClose
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.bodyType = httpBodyEmpty
|
||||
}
|
||||
|
||||
type httpExpectType uint8
|
||||
|
||||
const (
|
||||
httpExpectNotSet httpExpectType = 0
|
||||
httpExpectEmpty httpExpectType = 1
|
||||
httpExpect100Continue httpExpectType = 2
|
||||
)
|
||||
|
||||
var bExpectHeader = []byte("Expect")
|
||||
var bExpect100Value = []byte("100-continue")
|
||||
|
||||
func (t *TCPMessage) check100Continue() {
|
||||
if t.expectType != httpExpectNotSet || len(t.packets[0].Data) < 25 {
|
||||
return
|
||||
}
|
||||
|
||||
if t.seqMissing || t.headerPacket == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
last := t.packets[len(t.packets)-1]
|
||||
// reading last 4 bytes for double CRLF
|
||||
if !bytes.HasSuffix(last.Data, bEmptyLine) {
|
||||
return
|
||||
}
|
||||
|
||||
var expectB []byte
|
||||
proto.ParseHeaders(t.packetsData(), func(header, value []byte) bool {
|
||||
if proto.HeadersEqual(header, bExpectHeader) {
|
||||
expectB = value
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if len(expectB) > 0 && bytes.Equal(bExpect100Value, expectB) {
|
||||
t.expectType = httpExpect100Continue
|
||||
return
|
||||
}
|
||||
|
||||
t.expectType = httpExpectEmpty
|
||||
}
|
||||
|
||||
func (t *TCPMessage) setAssocMessage(m *TCPMessage) {
|
||||
t.AssocMessage = m
|
||||
t.checkIfComplete()
|
||||
}
|
||||
|
||||
// UpdateResponseAck should be called after packet is added
|
||||
func (t *TCPMessage) UpdateResponseAck() uint32 {
|
||||
lastPacket := t.packets[len(t.packets)-1]
|
||||
if lastPacket.IsFIN && len(t.packets) > 1 {
|
||||
lastPacket = t.packets[len(t.packets)-2]
|
||||
}
|
||||
|
||||
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) 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
|
||||
}
|
||||
|
||||
func (t *TCPMessage) ID() tcpID {
|
||||
return t.packets[0].ID
|
||||
}
|
||||
|
||||
func (t *TCPMessage) IP() net.IP {
|
||||
return net.IP(t.packets[0].Addr)
|
||||
}
|
||||
|
||||
func (t *TCPMessage) String() string {
|
||||
return strings.Join([]string{
|
||||
"Len packets: " + strconv.Itoa(len(t.packets)),
|
||||
"Data size:" + strconv.Itoa(len(t.Bytes())),
|
||||
"Data:" + string(t.Bytes()),
|
||||
}, "\n")
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
_ "log"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func buildPacket(isIncoming bool, Ack, Seq uint32, Data []byte, timestamp time.Time) (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, timestamp)
|
||||
|
||||
return packet
|
||||
}
|
||||
|
||||
func buildMessage(p *TCPPacket) *TCPMessage {
|
||||
isIncoming := false
|
||||
if p.SrcPort == 1 {
|
||||
isIncoming = true
|
||||
}
|
||||
m := NewTCPMessage(p.Seq, p.Ack, isIncoming, ProtocolHTTP, p.timestamp)
|
||||
m.AddPacket(p)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func TestTCPMessagePacketsOrder(t *testing.T) {
|
||||
msg := buildMessage(buildPacket(true, 1, 1, []byte("a"), time.Now()))
|
||||
msg.AddPacket(buildPacket(true, 1, 2, []byte("b"), time.Now()))
|
||||
|
||||
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"), time.Now()))
|
||||
msg.AddPacket(buildPacket(true, 1, 1, []byte("a"), time.Now()))
|
||||
|
||||
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"), time.Now()))
|
||||
msg.AddPacket(buildPacket(true, 1, 1, []byte("a"), time.Now()))
|
||||
|
||||
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"), time.Now()))
|
||||
msg.AddPacket(buildPacket(true, 1, 2, []byte("b"), time.Now()))
|
||||
|
||||
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 TestTCPMessageIsComplete(t *testing.T) {
|
||||
testCases := []struct {
|
||||
direction bool
|
||||
payload string
|
||||
assocMessage bool
|
||||
expectedCompleted bool
|
||||
}{
|
||||
{true, "GET / HTTP/1.1\r\n\r\n", false, true},
|
||||
{true, "HEAD / HTTP/1.1\r\n\r\n", false, true},
|
||||
{false, "HTTP/1.1 200 OK\r\n\r\n", true, true},
|
||||
{true, "POST / HTTP/1.1\r\nContent-Length: 1\r\n\r\na", false, true},
|
||||
{true, "PUT / HTTP/1.1\r\nContent-Length: 1\r\n\r\na", false, true},
|
||||
{false, "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", true, true},
|
||||
{false, "HTTP/1.1 200 OK\r\nContent-Length: 1\r\n\r\na", true, true},
|
||||
{false, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n", true, true},
|
||||
|
||||
// chunked not finished
|
||||
{false, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n", true, false},
|
||||
|
||||
// content-length != actual length
|
||||
{true, "POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\na", false, false},
|
||||
{false, "HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\na", true, false},
|
||||
// non-valid http request
|
||||
{true, "UNKNOWN asd HTTP/1.1\r\n\r\n", false, false},
|
||||
|
||||
// response without associated request
|
||||
{false, "HTTP/1.1 200 OK\r\n\r\n", false, false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
msg := buildMessage(buildPacket(tc.direction, 1, 1, []byte(tc.payload), time.Now()))
|
||||
if tc.assocMessage {
|
||||
msg.AssocMessage = &TCPMessage{}
|
||||
}
|
||||
msg.checkIfComplete()
|
||||
|
||||
if msg.complete != tc.expectedCompleted {
|
||||
t.Errorf("Payload %s: Expected %t, got %t.", tc.payload, tc.expectedCompleted, msg.complete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPMessageIsSeqMissing(t *testing.T) {
|
||||
p1 := buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\n"), time.Now())
|
||||
p2 := buildPacket(false, 1, p1.Seq+uint32(len(p1.Data)), []byte("Content-Length: 10\r\n\r\n"), time.Now())
|
||||
p3 := buildPacket(false, 1, p2.Seq+uint32(len(p2.Data)), []byte("a"), time.Now())
|
||||
|
||||
msg := buildMessage(p1)
|
||||
if msg.seqMissing {
|
||||
t.Error("Should be complete if have only 1 packet")
|
||||
}
|
||||
|
||||
msg.AddPacket(p3)
|
||||
if !msg.seqMissing {
|
||||
t.Error("Should be incomplete because missing middle component")
|
||||
}
|
||||
|
||||
msg.AddPacket(p2)
|
||||
if msg.seqMissing {
|
||||
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\r\n"), time.Now())
|
||||
p2 := buildPacket(false, 1, p1.Seq+uint32(len(p1.Data)), []byte("Content-Length: 10\r\n\r\n"), time.Now())
|
||||
|
||||
msg := buildMessage(p1)
|
||||
if msg.headerPacket == -1 {
|
||||
t.Error("Should be complete if have only 1 packet", msg.headerPacket)
|
||||
}
|
||||
|
||||
msg.AddPacket(p2)
|
||||
if msg.headerPacket == -1 {
|
||||
t.Error("Should found double new line: headers received")
|
||||
}
|
||||
|
||||
msg = buildMessage(buildPacket(true, 1, 1, []byte("GET / HTTP/1.1\r\nContent-Length: 1\r\n"), time.Now()))
|
||||
if msg.headerPacket != -1 {
|
||||
t.Error("Should not find headers end")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPMessageMethodType(t *testing.T) {
|
||||
testCases := []struct {
|
||||
direction bool
|
||||
payload string
|
||||
expectedMethodType httpMethodType
|
||||
}{
|
||||
{true, "GET / HTTP/1.1\r\n\r\n", httpMethodKnown},
|
||||
{true, "GET * HTTP/1.1\r\n\r\n", httpMethodKnown},
|
||||
{true, "UNKNOWN / HTTP/1.1\r\n\r\n", httpMethodKnown},
|
||||
{true, "GET http://example.com HTTP/1.1\r\n\r\n", httpMethodKnown},
|
||||
{true, "POST / HTTP/1.1\r\n\r\n", httpMethodKnown},
|
||||
{true, "PUT / HTTP/1.1\r\n\r\n", httpMethodKnown},
|
||||
{true, "GET zxc HTTP/1.1\r\n\r\n", httpMethodNotFound},
|
||||
{true, "GET / HTTP\r\n\r\n", httpMethodNotFound},
|
||||
{true, "VERYLONGMETHOD / HTTP/1.1\r\n\r\n", httpMethodNotFound},
|
||||
{false, "HTTP/1.1 200 OK\r\n\r\n", httpMethodKnown},
|
||||
{false, "HTTP /1.1 200 OK\r\n\r\n", httpMethodNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
msg := buildMessage(buildPacket(tc.direction, 1, 1, []byte(tc.payload), time.Now()))
|
||||
|
||||
if msg.methodType != tc.expectedMethodType {
|
||||
t.Errorf("Expected %d, got %d", tc.expectedMethodType, msg.methodType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPMessageBodyType(t *testing.T) {
|
||||
testCases := []struct {
|
||||
direction bool
|
||||
payload string
|
||||
expectedBodyType httpBodyType
|
||||
}{
|
||||
{true, "GET / HTTP/1.1\r\n\r\n", httpBodyEmpty},
|
||||
{true, "GET / HTTP/1.1\r\nContent-Length: 2\r\n\r\nab", httpBodyContentLength},
|
||||
{true, "POST / HTTP/1.1\r\n\r\n", httpBodyEmpty},
|
||||
{true, "POST / HTTP/1.1\r\nUser-Agent: zxc\r\n\r\n", httpBodyEmpty},
|
||||
{false, "HTTP/1.1 200 OK\r\n\r\n", httpBodyEmpty},
|
||||
{true, "POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\nab", httpBodyContentLength},
|
||||
{false, "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nab", httpBodyContentLength},
|
||||
{true, "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nab\r\n0\r\n\r\n", httpBodyChunked},
|
||||
{false, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nab\r\n0\r\n\r\n", httpBodyChunked},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
msg := buildMessage(buildPacket(tc.direction, 1, 1, []byte(tc.payload), time.Now()))
|
||||
|
||||
if msg.bodyType != tc.expectedBodyType {
|
||||
t.Errorf("Expected %d, got %d", tc.expectedBodyType, msg.bodyType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPMessageBodySize(t *testing.T) {
|
||||
testCases := []struct {
|
||||
direction bool
|
||||
payloads []string
|
||||
expectedSize int
|
||||
}{
|
||||
{true, []string{"GET / HTTP/1.1\r\n\r\n"}, 0},
|
||||
{true, []string{"POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\nab"}, 2},
|
||||
{true, []string{"GET / HTTP/1.1\r\n", "Content-Length: 2\r\n\r\nab"}, 2},
|
||||
{true, []string{"GET / HTTP/1.1\r\n", "Content-Length: 2\r\n\r\n", "ab"}, 2},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
msg := buildMessage(buildPacket(tc.direction, 1, 1, []byte(tc.payloads[0]), time.Now()))
|
||||
|
||||
if len(tc.payloads) > 1 {
|
||||
for _, p := range tc.payloads[1:] {
|
||||
seq := uint32(1 + msg.Size())
|
||||
msg.AddPacket(buildPacket(tc.direction, 1, seq, []byte(p), time.Now()))
|
||||
}
|
||||
}
|
||||
|
||||
if msg.BodySize() != tc.expectedSize {
|
||||
t.Errorf("Expected %d, got %d", tc.expectedSize, msg.BodySize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTcpMessageStart(t *testing.T) {
|
||||
start := time.Now().Add(-1 * time.Second)
|
||||
|
||||
msg := buildMessage(buildPacket(true, 1, 2, []byte("b"), time.Now()))
|
||||
msg.AddPacket(buildPacket(true, 1, 1, []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\na"), start))
|
||||
|
||||
if msg.Start != start {
|
||||
t.Error("Message timestamp should be equal to the lowest related packet timestamp", start, msg.Start)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
|
||||
IsFIN bool
|
||||
|
||||
Raw []byte
|
||||
Data []byte
|
||||
Addr []byte
|
||||
timestamp time.Time
|
||||
ID tcpID
|
||||
}
|
||||
|
||||
// ParseTCPPacket takes address and tcp payload and returns parsed TCPPacket
|
||||
func ParseTCPPacket(addr []byte, data []byte, timestamp time.Time) (p *TCPPacket) {
|
||||
p = &TCPPacket{Raw: data}
|
||||
p.ParseBasic()
|
||||
p.Addr = addr
|
||||
p.timestamp = timestamp
|
||||
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
|
||||
t.IsFIN = t.Raw[13]&0x01 != 0
|
||||
|
||||
if len(t.Raw) >= int(t.DataOffset*4) {
|
||||
t.Data = t.Raw[t.DataOffset*4:]
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TCPPacket) dump() *packet {
|
||||
|
||||
packetSrcIP := make([]byte, 16)
|
||||
packetData := make([]byte, len(t.Data)+16)
|
||||
|
||||
copy(packetSrcIP, t.Addr)
|
||||
|
||||
binary.BigEndian.PutUint16(packetData[0:2], t.SrcPort)
|
||||
binary.BigEndian.PutUint16(packetData[2:4], t.DestPort)
|
||||
|
||||
binary.BigEndian.PutUint32(packetData[4:8], t.Seq)
|
||||
binary.BigEndian.PutUint32(packetData[8:12], t.Ack)
|
||||
|
||||
packetData[12] = 64
|
||||
|
||||
if t.IsFIN {
|
||||
packetData[13] = packetData[13] | 0x01
|
||||
}
|
||||
|
||||
copy(packetData[16:], t.Data)
|
||||
return &packet{
|
||||
srcIP: packetSrcIP,
|
||||
data: packetData,
|
||||
timestamp: t.timestamp,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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)),
|
||||
"FIN:" + strconv.FormatBool(t.IsFIN),
|
||||
|
||||
"Data size:" + strconv.Itoa(len(t.Data)),
|
||||
"Data:" + string(t.Data[:maxLen]),
|
||||
}, "\n")
|
||||
}
|
||||
@@ -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 @@
|
||||
docs.goreplay.org
|
||||
@@ -0,0 +1,49 @@
|
||||
Think about Gor more like a network analyzer or tcpdump on steroids, it is not a proxy and does not affect your app anyhow. You specify application port, and it will capture and replay incoming data.
|
||||
|
||||
Simplest setup will be:
|
||||
```bash
|
||||
# Run on servers where you want to catch traffic. You can run it on every `web` machine.
|
||||
sudo gor --input-raw :80 --output-http http://staging.com
|
||||
```
|
||||
It will record and replay traffic from the same machine. However, it is possible to use [[Aggregator-forwarder setup]], when Gor on your web machines forward traffic to Gor aggregator instance running on the separate server.
|
||||
|
||||
> You may notice that it require `sudo`: to analyze network Gor need permissions which available only to root users. However, it is possible to configure Gor [beign run for non-root users](Running as a non-root user).
|
||||
|
||||
|
||||
### Forwarding to multiple addresses
|
||||
|
||||
You can forward traffic to multiple endpoints.
|
||||
```
|
||||
gor --input-tcp :28020 --output-http "http://staging.com" --output-http "http://dev.com"
|
||||
```
|
||||
|
||||
### Splitting traffic
|
||||
By default, it will send same traffic to all outputs, but you have options to equally split it (round-robin) using `--split-output` option.
|
||||
|
||||
```
|
||||
gor --input-raw :80 --output-http "http://staging.com" --output-http "http://dev.com" --split-output true
|
||||
```
|
||||
|
||||
### Tracking responses
|
||||
By default `input-raw` does not intercept responses, only requests. You can turn response tracking using `--input-raw-track-response` option. When enable you will be able to access response information in middleware and `output-file`.
|
||||
|
||||
|
||||
### Traffic interception engine
|
||||
By default, Gor will use `libpcap` for intercepting traffic, it should work in most cases. If you have any troubles with it, you may try alternative engine: `raw_socket`.
|
||||
|
||||
```
|
||||
sudo gor --input-raw :80 --input-raw-engine "raw_socket" --output-http "http://staging.com"
|
||||
```
|
||||
|
||||
You can read more about [[Replaying HTTP traffic]].
|
||||
|
||||
|
||||
### Tracking original IP addresses
|
||||
You can use `--input-raw-realip-header` option to specify header name: 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`, but you can specify any name.
|
||||
|
||||
`gor --input-raw :80 --input-raw-realip-header "X-Real-IP" ...`
|
||||
|
||||
|
||||
***
|
||||
|
||||
Also you may want to know about [[Rate limiting]], [[Request rewriting]] and [[Request filtering]]
|
||||
@@ -0,0 +1,30 @@
|
||||
We provide pre-compiled binaries for Mac and Linux, but you are free to compile Gor by yourself.
|
||||
|
||||
Gor is written using Go, so first you need to download it from here https://golang.org/, use the latest stable version.
|
||||
|
||||
The only Gor dependency is [libpcap](https://github.com/the-tcpdump-group/libpcap), which is the interface to various kernel packet capture mechanisms, and https://github.com/google/gopacket, which is a Go wrapper around libpcap. Latest libpcap version can be obtained at http://www.tcpdump.org/release/. Libpcap itself depend on `flex` and `bison` packages, many operating systems already have them installed.
|
||||
|
||||
```bash
|
||||
# Fetch libpcap dependencies. Depending on your OS, instead of `apt` you will use `yum` or `rpm`, or `brew` on Mac.
|
||||
sudo apt-get install flex bison -y
|
||||
|
||||
# Download latest stable release, compile and install it
|
||||
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
|
||||
|
||||
|
||||
# Lets fetch Gor source code
|
||||
mkdir $HOME/gocode
|
||||
# See more information about GOPATH https://github.com/golang/go/wiki/GOPATH
|
||||
export GOPATH=$HOME/gocode
|
||||
# Fetch code from the Github
|
||||
go get github.com/buger/gor
|
||||
|
||||
# Compile from source
|
||||
cd $HOME/gocode/src/github.com/buger/gor
|
||||
go build LDFLAGS = -ldflags "-extldflags \"-static\""
|
||||
```
|
||||
|
||||
After you finished, you should see `gor` binary in current directory.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
## STEP 1: Install Docker
|
||||
For local development we recommend to use Docker.
|
||||
|
||||
If you don’t have it you can read how to install it here:
|
||||
https://docs.docker.com/engine/getstarted/step_one/#step-3-verify-your-installation
|
||||
|
||||
## STEP 2: Download repository
|
||||
|
||||
`git clone git@github.com:buger/goreplay.git`
|
||||
|
||||
|
||||
## STEP 3: Setup container
|
||||
|
||||
```
|
||||
cd ./goreplay
|
||||
make build
|
||||
|
||||
```
|
||||
|
||||
## Testing
|
||||
To run tests execute next command:
|
||||
|
||||
```
|
||||
make test
|
||||
```
|
||||
|
||||
You can copy the command that is produced and modify it. For example, if you need to run one test copy the command and add `-run TestName`, e.g.:
|
||||
|
||||
```
|
||||
docker run -v `pwd`:/go/src/github.com/buger/gor/ -p 0.0.0.0:8000:8000 -t -i gor:go go test ./. -run TestEmitterFiltered -timeout 60s -ldflags "-X main.VERSION=DEV-1482398347 -extldflags \"-static\"" -v
|
||||
```
|
||||
|
||||
|
||||
## Building
|
||||
To get a binary file run
|
||||
|
||||
```
|
||||
make release-bin
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
Sometimes it makes sense to use separate Gor instance for replaying traffic and performing things like load testing, so your production machines do not spend precious resources. It is possible to configure Gor on your web machines forward traffic to Gor aggregator instance running on the separate server.
|
||||
|
||||
```bash
|
||||
# Run on servers where you want to catch traffic. You can run it on each `web` machine.
|
||||
sudo gor --input-raw :80 --output-tcp replay.local:28020
|
||||
|
||||
# Replay server (replay.local).
|
||||
gor --input-tcp replay.local:28020 --output-http http://staging.com
|
||||
```
|
||||
|
||||
If you have multiple replay machines you can split traffic among them using `--split-output` option: it will equally split all incoming traffic to all outputs using round robin algorithm.
|
||||
```
|
||||
gor --input-raw :80 --split-output --output-tcp replay1.local:28020 --output-tcp replay2.local:28020
|
||||
```
|
||||
|
||||
[GoReplay PRO](https://goreplay.org/pro.html) support accurate recording and replaying of tcp sessions, and when `--recognize-tcp-sessions` option is passed, instead of round-robin it will use a smarter algorithm which ensures that same sessions will be sent to the same replay instance.
|
||||
|
||||
|
||||
In case if you are planning a large load testing, you may consider use separate master instance which will control Gor slaves which actually replay traffic. For example:
|
||||
```
|
||||
# This command will read multiple log files, replay them on 10x speed and loop them if needed for 30 seconds, and will distributed traffic (tcp session aware) among multiple workers
|
||||
gor --input-file logs_from_multiple_machines.*|1000% --input-file-loop --exit-after 30s --recognize-tcp-sessions --split-output --output-tcp worker1.local --output-tcp worker2.local:27017 --output-tcp worker3.local:27017 ... --output-tcp workerN.local:27017
|
||||
|
||||
# worker
|
||||
gor --input-tcp :27017 --ouput-http load_test.target
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
Gor can export requests and replayed response data to ElasticSearch:
|
||||
|
||||
```
|
||||
./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.
|
||||
|
||||
### Format
|
||||
|
||||
Following structure represents ES format:
|
||||
|
||||
```
|
||||
type ESRequestResponse struct {
|
||||
ReqURL string `json:"Req_URL"`
|
||||
ReqMethod string `json:"Req_Method"`
|
||||
ReqUserAgent string `json:"Req_User-Agent"`
|
||||
ReqAcceptLanguage string `json:"Req_Accept-Language,omitempty"`
|
||||
ReqAccept string `json:"Req_Accept,omitempty"`
|
||||
ReqAcceptEncoding string `json:"Req_Accept-Encoding,omitempty"`
|
||||
ReqIfModifiedSince string `json:"Req_If-Modified-Since,omitempty"`
|
||||
ReqConnection string `json:"Req_Connection,omitempty"`
|
||||
ReqCookies string `json:"Req_Cookies,omitempty"`
|
||||
RespStatus string `json:"Resp_Status"`
|
||||
RespStatusCode string `json:"Resp_Status-Code"`
|
||||
RespProto string `json:"Resp_Proto,omitempty"`
|
||||
RespContentLength string `json:"Resp_Content-Length,omitempty"`
|
||||
RespContentType string `json:"Resp_Content-Type,omitempty"`
|
||||
RespTransferEncoding string `json:"Resp_Transfer-Encoding,omitempty"`
|
||||
RespContentEncoding string `json:"Resp_Content-Encoding,omitempty"`
|
||||
RespExpires string `json:"Resp_Expires,omitempty"`
|
||||
RespCacheControl string `json:"Resp_Cache-Control,omitempty"`
|
||||
RespVary string `json:"Resp_Vary,omitempty"`
|
||||
RespSetCookie string `json:"Resp_Set-Cookie,omitempty"`
|
||||
Rtt int64 `json:"RTT"`
|
||||
Timestamp time.Time
|
||||
}
|
||||
```
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
### What OS are supported?
|
||||
Gor will run everywhere where [libpcap](http://www.tcpdump.org/) works, and it works on most of the platforms. However, currently, we test it on Linux and Mac. See more about [[Compilation]].
|
||||
|
||||
### Why does the `--input-raw` requires sudo or root access?
|
||||
Listener works by sniffing traffic from a given port. It's accessible
|
||||
only by using sudo or root access. But it is possible to [[Running as non root user]].
|
||||
|
||||
### How do you deal with user session to replay the traffic correctly?
|
||||
You can rewrite session related headers/params to match your staging environment. If you require custom logic (e.g random token based auth) follow this discussion: https://github.com/buger/gor/issues/154
|
||||
|
||||
### Can I use Gor to intercept SSL traffic?
|
||||
Basic idea is that SSL was made to protect itself from traffic interception. There 2 options:
|
||||
1. Move SSL handling to proxy like Nginx or Amazon ELB. And allow Gor to listen on upstreams.
|
||||
2. Use `--input-http` so you can duplicate request payload directly from your app to Gor, but it will require your app modifications.
|
||||
|
||||
More can be find here: https://github.com/buger/gor/issues/85
|
||||
|
||||
### Is there a limit for size of HTTP request when using output-http?
|
||||
Due to the fact that Gor can't guarantee interception of all packets, for large payloads > 200kb there is chance of missing some packets and corrupting body. Treat it as a feature and chance to test broken bodies handling :)
|
||||
The only way to guarantee delivery is using `--input-http`, but you will miss some features.
|
||||
|
||||
### I'm getting 'too many open files' error
|
||||
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
|
||||
|
||||
More about ulimit: http://www.thecodingmachine.com/solving-the-too-many-open-files-exception-in-red5-or-any-other-application/
|
||||
|
||||
### The CPU average across my load-balanced targets is higher than the source
|
||||
If you are replaying traffic from multiple listeners to a load-balanced target and you use sticky sessions, you may observe that the target servers have a higher CPU load than the listener servers. This may be because the sticky session cookie of the original load balancer is not honored by the target load balancer thus resulting in requests that would normally hit the same target server hitting different servers on the backend thus reducing some caching benefits gained via the load balancing. Try running just one listener against one replay target and see if the CPU utilization comparison is more accurate.
|
||||
|
||||
Also see [[Troubleshooting]].
|
||||
@@ -0,0 +1,74 @@
|
||||
#### Overview
|
||||
Middleware is a program that accepts request and response payload at STDIN and emits modified requests at STDOUT. You can implement any custom logic like stripping private data, advanced rewriting, support for oAuth and etc. Check examples [included into our repo](https://github.com/buger/gor/tree/master/examples/middleware).
|
||||
|
||||
|
||||
```
|
||||
Original request +--------------+
|
||||
+-------------+----------STDIN---------->+ |
|
||||
| Gor input | | Middleware |
|
||||
+-------------+----------STDIN---------->+ |
|
||||
Original response (1) +------+---+---+
|
||||
| ^
|
||||
+-------------+ Modified request v |
|
||||
| Gor output +<---------STDOUT-----------------+ |
|
||||
+-----+-------+ |
|
||||
| |
|
||||
| Replayed response |
|
||||
+------------------STDIN----------------->----+
|
||||
```
|
||||
|
||||
(1): Original responses will only be sent to the middleware if the `--input-raw-track-response` option is specified.
|
||||
|
||||
Middleware can be written in any language, see `examples/middleware` folder for examples.
|
||||
Middleware program should accept the fact that all communication with Gor is asynchronous, there is no guarantee that original request and response messages will come one after each other. Your app should take care of the state if logic depends on original or replayed response, see `examples/middleware/token_modifier.go` as example.
|
||||
|
||||
Simple bash echo middleware (returns same request) will look like this:
|
||||
```bash
|
||||
while read line; do
|
||||
echo $line
|
||||
done
|
||||
```
|
||||
|
||||
Middleware can be enabled using `--middleware` option, by specifying path to executable file:
|
||||
```
|
||||
gor --input-raw :80 --middleware "/opt/middleware_executable" --output-http "http://staging.server"
|
||||
```
|
||||
|
||||
#### Communication protocol
|
||||
All messages should be hex encoded, new line character specifieds the end of the message, eg. new message per line.
|
||||
|
||||
Decoded payload consist of 2 parts: header and HTTP payload, separated by new line character.
|
||||
|
||||
Example request payload:
|
||||
|
||||
```
|
||||
1 932079936fa4306fc308d67588178d17d823647c 1439818823587396305
|
||||
GET /a HTTP/1.1
|
||||
Host: 127.0.0.1
|
||||
|
||||
```
|
||||
|
||||
Example response payload (note: you will only receive this if you specify `--input-raw-track-response`)
|
||||
|
||||
```
|
||||
2 8e091765ae902fef8a2b7d9dd960e9d52222bd8c 1439818823587996305 2782013
|
||||
HTTP/1.1 200 OK
|
||||
Date: Mon, 17 Aug 2015 13:40:23 GMT
|
||||
Content-Length: 0
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
```
|
||||
|
||||
Header contains request meta information separated by spaces. First value is payload type, possible values: `1` - request, `2` - original response, `3` - replayed response.
|
||||
Next goes request id: unique among all requests (sha1 of time and Ack), but remain same for original and replayed response, so you can create associations between request and responses. The third argument is the time when request/response was initiated/received. Forth argument is populated only for responses and means latency.
|
||||
|
||||
HTTP payload is unmodified HTTP requests/responses intercepted from network. You can read more about request format [here](http://www.jmarshall.com/easy/http/), [here](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol) and [here](http://www.w3.org/Protocols/rfc2616/rfc2616.html). You can operate with payload as you want, add headers, change path, and etc. Basically you just editing a string, just ensure that it is RCF compliant.
|
||||
|
||||
At the end modified (or untouched) request should be emitted back to STDOUT, keeping original header, and hex-encoded. If you want to filter request, just not send it. Emitting responses back is required, even if you did not touch them.
|
||||
|
||||
#### Advanced example
|
||||
Imagine that you have 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 [examples/middleware/token_modifier.go](https://github.com/buger/gor/tree/master/examples/middleware/token_modifier.go) and [middleware_test.go#TestTokenMiddleware](https://github.com/buger/gor/tree/master/middleware_test.go) as example of described scheme.
|
||||
|
||||
***
|
||||
|
||||
You may also read about [[Request filtering]], [[Rate limiting]] and [[Request rewriting]].
|
||||
@@ -0,0 +1,37 @@
|
||||
Rate limiting can be useful if you only want to forward parts of incoming traffic, for example, to not overload your test environment. There are two strategies: dropping random requests or dropping fractions of requests based on Header or URL param value.
|
||||
|
||||
### Dropping random requests
|
||||
Every input and output support random rate limiting.
|
||||
There are two limiting algorithms: absolute or percentage based.
|
||||
|
||||
**Absolute**: If for current second it reached specified requests limit - disregard the rest, on next second counter reset.
|
||||
|
||||
**Percentage**: For input-file it will slowdown or speedup request execution, for the rest it will use the random generator to decide if request pass or not based on the chance you specified.
|
||||
|
||||
You can specify your desired limit using the "|" operator after the server address, see examples below.
|
||||
|
||||
#### Limiting replay using absolute number
|
||||
```
|
||||
# staging.server will not get more than ten requests per second
|
||||
gor --input-tcp :28020 --output-http "http://staging.com|10"
|
||||
```
|
||||
|
||||
#### Limiting listener using percentage based limiter
|
||||
```
|
||||
# replay server will not get more than 10% of requests
|
||||
# useful for high-load environments
|
||||
gor --input-raw :80 --output-tcp "replay.local:28020|10%"
|
||||
```
|
||||
|
||||
### Consistent limiting based on Header or URL param value
|
||||
If you have unique user id (like API key) stored in header or URL you can consistently forward specified percent of traffic only for the fraction of this users.
|
||||
Basic formula looks like this: `FNV32-1A_hashing(value) % 100 >= chance`. Examples:
|
||||
```
|
||||
# Limit based on header value
|
||||
gor --input-raw :80 --output-tcp "replay.local:28020|10%" --http-header-limiter "X-API-KEY: 10%"
|
||||
|
||||
# Limit based on header value
|
||||
gor --input-raw :80 --output-tcp "replay.local:28020|10%" --http-param-limiter "api_key: 10%"
|
||||
```
|
||||
|
||||
When limiting based on header or param only percentage based limiting supported.
|
||||
@@ -0,0 +1,46 @@
|
||||
Gor can replay HTTP traffic using `--output-http` option:
|
||||
|
||||
```bash
|
||||
sudo ./gor --input-raw :8000 --output-http="http://staging.env"
|
||||
```
|
||||
|
||||
You can [filter](Request filtering), [rate limit](Rate limiting) and [rewrite](Request rewriting) requests on the fly.
|
||||
|
||||
### HTTP output workers
|
||||
By default Gor creates a dynamic pool of workers: it starts with 10 and creates more HTTP output workers when the HTTP output queue length is greater than 10. The number of workers created (N) is equal to the queue length at the time which it is checked and found to have a length greater than 10. The queue length is checked every time a message is written to the HTTP output queue. No more workers will be spawned until that request to spawn N workers is satisfied. If a dynamic worker cannot process a message at that time, it will sleep for 100 milliseconds. If a dynamic worker cannot process a message for 2 seconds it dies.
|
||||
You may specify fixed number of workers using `--output-http-workers=20` option.
|
||||
|
||||
### Following redirects
|
||||
By default Gor will ignore all redirects since they are handled by clients using your app, but in scenarios where your replayed environment introduces new redirects, you can enable them like this:
|
||||
```
|
||||
gor --input-tcp replay.local:28020 --output-http http://staging.com --output-http-redirects 2
|
||||
```
|
||||
The given example will follow up to 2 redirects per request.
|
||||
|
||||
### HTTP timeouts
|
||||
By default http timeout for both request and response is 5 seconds. You can override it like this:
|
||||
```
|
||||
gor --input-tcp replay.local:28020 --output-http http://staging.com --output-http-timeout 30s
|
||||
```
|
||||
|
||||
### Response buffer
|
||||
By default, to reduce memory consumption, internal HTTP client will fetch max 200kb of the response body (used if you use middleware), by you can increase limit using `--output-http-response-buffer` option (accepts number of bytes).
|
||||
|
||||
### Basic Auth
|
||||
|
||||
If your development or staging environment is protected by Basic Authentication then those credentials can be injected in during the replay:
|
||||
|
||||
```
|
||||
gor --input-raw :80 --output-http "http://user:pass@staging.com"
|
||||
```
|
||||
|
||||
Note: This will overwrite any Authorization headers in the original request.
|
||||
|
||||
|
||||
### Multiple domains support
|
||||
|
||||
If you app accepts traffic from multiple domains, and you want to keep original headers, there is specific `--http-original-host` with tells Gor do not touch Host header at all.
|
||||
|
||||
|
||||
***
|
||||
You may also read about [[Saving and Replaying from file]]
|
||||
@@ -0,0 +1,35 @@
|
||||
Filtering is useful when you need to capture only specific part of traffic, like API requests. It is possible to filter by URL, HTTP header or HTTP method.
|
||||
|
||||
#### Allow url regexp
|
||||
```
|
||||
# only forward requests being sent to the /api endpoint
|
||||
gor --input-raw :8080 --output-http staging.com --http-allow-url /api
|
||||
```
|
||||
|
||||
#### Disallow url regexp
|
||||
```
|
||||
# only forward requests NOT being sent to the /api... endpoint
|
||||
gor --input-raw :8080 --output-http staging.com --http-disallow-url /api
|
||||
```
|
||||
#### Filter based on regexp of header
|
||||
|
||||
```
|
||||
# only forward requests with an api version of 1.0x
|
||||
gor --input-raw :8080 --output-http staging.com --http-allow-header api-version:^1\.0\d
|
||||
|
||||
# only forward requests NOT containing User-Agent header value "Replayed by Gor"
|
||||
gor --input-raw :8080 --output-http staging.com --http-disallow-header "User-Agent: Replayed by Gor"
|
||||
```
|
||||
|
||||
#### Filter based on HTTP method
|
||||
Requests not matching a specified whitelist can be filtered out. For example to strip non-nullipotent requests:
|
||||
|
||||
```
|
||||
gor --input-raw :80 --output-http "http://staging.server" \
|
||||
--http-allow-method GET \
|
||||
--http-allow-method OPTIONS
|
||||
```
|
||||
|
||||
|
||||
-----
|
||||
You may also read about [[Request rewriting]], [[Rate limiting]] and [[Middleware]]
|
||||
@@ -0,0 +1,38 @@
|
||||
Gor supports rewriting of URLs, URL params and headers, see below.
|
||||
|
||||
Rewriting may be useful if you test environment does not have the same data as your production, and you want to perform all actions in the context of `test` user: for example rewrite all API tokens to some test value. Other possible use cases are toggling features on/off using custom headers or rewriting URL's if they changed in the new environment.
|
||||
|
||||
For more complex logic you can use [Middleware](middleware.md).
|
||||
|
||||
#### Rewrite URL based on a mapping
|
||||
`--http-rewrite-url` expects value in "<search>:<replace>" format: ":" is a dilimiter. In `<replace>` section you may use captured regexp group values. This works similar to `replace` method in Javascript or `gsub` in Ruby.
|
||||
|
||||
```
|
||||
# Rewrites all `/v1/user/<user_id>/ping` requests to `/v2/user/<user_id>/ping`
|
||||
gor --input-raw :8080 --output-http staging.com --http-rewrite-url /v1/user/([^\\/]+)/ping:/v2/user/$1/ping
|
||||
```
|
||||
|
||||
#### Set URL param
|
||||
Set request url param, if param already exists it will be overwritten.
|
||||
```
|
||||
gor --input-raw :8080 --output-http staging.com --http-set-param api_key=1
|
||||
```
|
||||
|
||||
#### Set Header
|
||||
Set request header, if header already exists it will be overwritten. May be useful if you need to identify requests generated by Gor or enable feature flagged functionality in an application:
|
||||
|
||||
```
|
||||
gor --input-raw :80 --output-http "http://staging.server" \
|
||||
--http-set-header "User-Agent: Replayed by Gor" \
|
||||
--http-set-header "Enable-Feature-X: true"
|
||||
```
|
||||
|
||||
#### Host header
|
||||
Host header gets special treatment. By default Host get set to the value specified in --output-http. If you manually set --http-set-header "Host: anonther.com", Gor will not override Host value.
|
||||
|
||||
If you app accepts traffic from multiple domains, and you want to keep original headers, there is specific `--http-original-host` with tells Gor do not touch Host header at all.
|
||||
|
||||
|
||||
***
|
||||
|
||||
You may also read about [[Request filtering]], [[Rate limiting]] and [[Middleware]]
|
||||
@@ -0,0 +1,17 @@
|
||||
You can enable Gor for non-root users in a secure method by using the following commands
|
||||
|
||||
```
|
||||
# Following commands assume that you put `gor` binary to /usr/local/bin
|
||||
add gor
|
||||
addgroup <username> gor
|
||||
chgrp gor /usr/local/bin/gor
|
||||
chmod 0750 /usr/local/bin/gor
|
||||
setcap "cap_net_raw,cap_net_admin+eip" /usr/local/bin/gor
|
||||
```
|
||||
|
||||
As a brief explanation of the above.
|
||||
* We create a group called gor.
|
||||
* We then add the user you want to the new group so they will be able to use gor without sudo
|
||||
* We then change the user/group of gor binary the new group.
|
||||
* We then make sure the permissions are set on gor binary so that members of the group can execute it but other normal users cannot.
|
||||
* We then use `setcap` to give the CAP_NET_RAW and CAP_NET_ADMIN privilege to the executable when it runs. This is so that Gor can open its raw socket which is not normally permitted unless you are root.
|
||||
@@ -0,0 +1,105 @@
|
||||
You can save requests to file, and replay them later. While replaying it will preserve the original time differences between requests. If you apply [percentage based limiting](Rate Limiting) timing between requests will be reduced or increased appropriately: this approach opens possibilities like load testing, see below.
|
||||
|
||||
```bash
|
||||
# write to file
|
||||
gor --input-raw :80 --output-file requests.log
|
||||
|
||||
# read from file
|
||||
gor --input-file requests.gor --output-http "http://staging.com"
|
||||
```
|
||||
|
||||
By default Gor writes files in chunks. This configurable using `--output-file-append` option: the flushed chunk is appended to existence file or not. The default is **false**. By default, `--output-file` flushes each chunk to a different path.
|
||||
|
||||
```bash
|
||||
gor ... --output-file %Y%m%d.log
|
||||
# append false
|
||||
20140608_0.log
|
||||
20140608_1.log
|
||||
20140609_0.log
|
||||
20140609_1.log
|
||||
```
|
||||
|
||||
This makes parallel file processing easy. But if you want to disable this behavior, you can disable it by adding `--output-file-append` option:
|
||||
|
||||
```bash
|
||||
gor ... --output-file %Y%m%d.log --output-file-append
|
||||
# append true
|
||||
20140608.log
|
||||
20140609.log
|
||||
```
|
||||
|
||||
If you run gor multiple times, and it finds existing files, it will continue from last known index.
|
||||
|
||||
### Chunk size
|
||||
|
||||
You can set chunk limits using `--output-file-size-limit` and `--output-file-queue-limit` options.
|
||||
The length of the chunk queue and the size of each chunk, respectively. The default values are 256 and 32mb, respectively. The suffixes “k” (KB), “m” (MB), and “g” (GB) can be used for `output-file-size-limit`.
|
||||
If you want to have only size constraint, you can set `--output-file-queue-limit` to 0, and vice versa.
|
||||
|
||||
```bash
|
||||
gor --input-raw :80 --output-file %Y-%m-%d.gz --output-file-size-limit 256m --output-file-queue-limit 0
|
||||
```
|
||||
|
||||
### Using date variables in file names
|
||||
For example, you can tell to create new file each hour: `--output-file /mnt/logs/requests-%Y-%m-%d-%H.log`
|
||||
It will create new file for each hour: requests-2016-06-01-12.log, requests-2016-06-01-13.log, ...
|
||||
|
||||
The time format used as part of the file name. The following characters are replaced with actual values when the file is created:
|
||||
|
||||
* `%Y`: year including the century (at least 4 digits)
|
||||
* `%m`: month of the year (01..12)
|
||||
* `%d`: Day of the month (01..31)
|
||||
* `%H`: Hour of the day, 24-hour clock (00..23)
|
||||
* `%M`: Minute of the hour (00..59)
|
||||
* `%S`: Second of the minute (00..60)
|
||||
|
||||
The default format is `%Y%m%d%H`, which creates one file per hour.
|
||||
|
||||
|
||||
### GZIP compression
|
||||
To read or write GZIP compressed files ensure that file extension ends with ".gz": `--output-file log.gz`
|
||||
|
||||
### Replaying from multiple files
|
||||
|
||||
`--input-file` accepts file pattern, for example: `--input-file logs-2016-05-*`: it will replay all the files, sorting them in lexicographical order.
|
||||
|
||||
### Buffered file output
|
||||
Gor has memory buffer when it writes to file, and continuously flush changes to the file. Flushing to file happens if the buffer is filled, forced flush every 1 second, or if Gor is closed. You can change it using `--output-file-flush-interval` option. It most cases it should not be touched.
|
||||
|
||||
### File format
|
||||
HTTP requests stored as it is, plain text: headers and bodies. Requests separated by `\n🐵🙈🙉\n` line (using such sequence for uniqueness and fun). Before each request goes single line with meta information containing payload type (1 - request, 2 - response, 3 - replayed response), unique request ID (request and response have the same) and timestamp when request was made. An example of 2 requests:
|
||||
|
||||
```
|
||||
1 d7123dasd913jfd21312dasdhas31 127345969\n
|
||||
GET / HTTP/1.1\r\n
|
||||
\r\n
|
||||
\n
|
||||
🐵🙈🙉
|
||||
\n
|
||||
POST /upload HTTP/1.1\r\n
|
||||
Content-Length: 7\r\n
|
||||
Host: www.w3.org\r\n
|
||||
\r\n
|
||||
a=1&b=2
|
||||
```
|
||||
Note that technically \r and \n symbols are invisible, and indicate new lines. I made them visible in example just to show how it looks on byte level.
|
||||
|
||||
Making it text friendly allows writing simple parsers and use console tools like `grep` to do an analysis. You can even edit them manually, but be sure that your file editor does not change line endings.
|
||||
|
||||
## Performance testing
|
||||
|
||||
Currently, this functionality supported only by `input-file` and only when using percentage based limiter. Unlike default limiter for `input-file` instead of dropping requests it will slowdown or speedup request emitting. Note that **limiter is applied to input**:
|
||||
|
||||
```
|
||||
# Replay from file on 2x speed
|
||||
gor --input-file "requests.gor|200%" --output-http "staging.com"
|
||||
```
|
||||
|
||||
Use `--stats --output-http-stats` to see latency stats.
|
||||
|
||||
### Looping files for replaying indefinitely
|
||||
You can loop the same set of files, so when the last one replays all the requests, it will not stop, and will start from first one again. Having the only small amount of requests you can do extensive performance testing.
|
||||
Pass `--input-file-loop` to make it work.
|
||||
|
||||
***
|
||||
You may also read about [[Capturing and replaying traffic]] and [[Rate limiting]]
|
||||
@@ -0,0 +1,90 @@
|
||||
Gor can report stats on the `output-tcp` and `output-http` request queues. Stats are reported to the console every 5 seconds in the form `latest,mean,max,count,count/second` by using the `--output-http-stats` and `--output-tcp-stats` options.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
2014/04/23 21:17:50 output_tcp:latest,mean,max,count,count/second
|
||||
2014/04/23 21:17:50 output_tcp:0,0,0,0,0
|
||||
2014/04/23 21:17:55 output_tcp:1,1,2,68,13
|
||||
2014/04/23 21:18:00 output_tcp:1,1,2,92,18
|
||||
2014/04/23 21:18:05 output_tcp:1,1,2,119,23
|
||||
```
|
||||
|
||||
```
|
||||
2014/04/23 21:19:46 output_http:latest,mean,max,count,count/second
|
||||
2014/04/23 21:19:46 output_http:0,0,0,0,0
|
||||
2014/04/23 21:19:51 output_http:0,0,0,0,0
|
||||
2014/04/23 21:19:56 output_http:0,0,0,0,0
|
||||
2014/04/23 21:20:01 output_http:1,0,1,50,10
|
||||
2014/04/23 21:20:06 output_http:1,1,4,72,14
|
||||
2014/04/23 21:20:11 output_http:1,0,1,179,35
|
||||
2014/04/23 21:20:16 output_http:1,0,1,148,29
|
||||
2014/04/23 21:20:21 output_http:1,1,2,91,18
|
||||
2014/04/23 21:20:26 output_http:1,1,2,150,30
|
||||
2014/04/23 21:18:15 output_http:100,99,100,70,14
|
||||
2014/04/23 21:18:21 output_http:100,99,100,55,11
|
||||
```
|
||||
|
||||
### How can I tell if I have bottlenecks?
|
||||
Key areas that sometimes experience bottlenecks are the output-tcp and output-http functions which have internal queues for requests. Each queue has an upper limit of 100. Enable stats reporting to see if any queues are experiencing bottleneck behavior.
|
||||
|
||||
#### Output HTTP bottlenecks
|
||||
When running a Gor replay the output-http feature may bottleneck if:
|
||||
|
||||
* the replay has inadequate bandwidth. If the replay is receiving or sending more messages than its network adapter can handle the output-http-stats may report that the output-http queue is filling up. See if there is a way to upgrade the replay's bandwidth.
|
||||
* with `--output-http-workers` set to anything other than `-1` the `-output-http` target is unable to respond to messages in a timely manner. The http output workers which take messages off the output-http queue, process the request, and ensure that the request did not result in an error may not be able to keep up with the number of incoming requests. If the replay is not using dynamic worker scaling (`--output-http-workers=-1`) The optimal number of output-http-workers can be determined with the formula `output-workers = (Average number of requests per second)/(Average target response time per second)`.
|
||||
|
||||
#### Output TCP bottlenecks
|
||||
When using the Gor listener the output-tcp feature may bottleneck if:
|
||||
|
||||
* the replay is unable to accept and process more requests than the listener is able generate. Prior to troubleshooting the output-tcp bottleneck, ensure that the replay target is not experiencing any bottlenecks.
|
||||
* the replay target has inadequate bandwidth to handle all its incoming requests. If a replay target's incoming bandwidth is maxed out the output-tcp-stats may report that the output-tcp queue is filling up. See if there is a way to upgrade the replay's bandwidth.
|
||||
|
||||
|
||||
#### Tuning
|
||||
|
||||
To achieve the top most performance you should tune the source server system limits:
|
||||
|
||||
net.ipv4.tcp_max_tw_buckets = 65536
|
||||
net.ipv4.tcp_tw_recycle = 1
|
||||
net.ipv4.tcp_tw_reuse = 0
|
||||
net.ipv4.tcp_max_syn_backlog = 131072
|
||||
net.ipv4.tcp_syn_retries = 3
|
||||
net.ipv4.tcp_synack_retries = 3
|
||||
net.ipv4.tcp_retries1 = 3
|
||||
net.ipv4.tcp_retries2 = 8
|
||||
net.ipv4.tcp_rmem = 16384 174760 349520
|
||||
net.ipv4.tcp_wmem = 16384 131072 262144
|
||||
net.ipv4.tcp_mem = 262144 524288 1048576
|
||||
net.ipv4.tcp_max_orphans = 65536
|
||||
net.ipv4.tcp_fin_timeout = 10
|
||||
net.ipv4.tcp_low_latency = 1
|
||||
net.ipv4.tcp_syncookies = 0
|
||||
***
|
||||
|
||||
### Gor is crashing with following stacktrace
|
||||
```
|
||||
fatal error: unexpected signal during runtime execution
|
||||
[signal 0xb code=0x1 addr=0x63 pc=0x7ffcdfdf8b2c]
|
||||
|
||||
runtime stack:
|
||||
runtime.throw(0xad8380, 0x2a)
|
||||
/usr/local/go/src/runtime/panic.go:547 +0x90
|
||||
runtime.sigpanic()
|
||||
/usr/local/go/src/runtime/sigpanic_unix.go:12 +0x5a
|
||||
|
||||
goroutine 103 [syscall, locked to thread]:
|
||||
runtime.cgocall(0x7b35a0, 0xc82121f1e8, 0x0)
|
||||
/usr/local/go/src/runtime/cgocall.go:123 +0x11b fp=0xc82121f188 sp=0xc82121f158
|
||||
net._C2func_getaddrinfo(0x7ffcec0008c0, 0x0, 0xc821b221e0, 0xc8217b2b18, 0x0, 0x0, 0x0)
|
||||
??:0 +0x55 fp=0xc82121f1e8 sp=0xc82121f188
|
||||
net.cgoLookupIPCNAME(0x7fffb17208ab, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb17200)
|
||||
```
|
||||
|
||||
There is a chance that you hit Go bug. The crash comes from the CGO version of DNS resolver.
|
||||
By default Go based version used, but ins some cases [it switches to CGO based](https://golang.org/pkg/net/#hdr-Name_Resolution). It is possible to force Go based DNS resolver using GODEBUG environment variable:
|
||||
`sudo GODEBUG="netdns=go" ./gor --input-raw :80 --output-http staging.env`
|
||||
|
||||
|
||||
|
||||
Also, see [[FAQ]]
|
||||
@@ -0,0 +1 @@
|
||||
[Website](https://goreplay.org) | [PRO version](https://goreplay.org/pro.html) | [[Getting started]] | [[FAQ]] | [Join newsletter](https://www.getdrip.com/forms/89690474/submissions/new)
|
||||
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-cayman
|
||||
@@ -0,0 +1,19 @@
|
||||
Collaboration is difficult with commercial closed source but I do want to keep as much of the OSS ethos as possible available to customers who want to fix it themselves.
|
||||
|
||||
## Legal
|
||||
|
||||
In order to unambiguously own and sell Gor commercial products, I must have the copyright associated with the entire codebase. Any code you create which is merged must be owned by me. That's not me trying to be a jerk, that's just the way it works.
|
||||
|
||||
## Application
|
||||
|
||||
If you wish access to the product repository so you can send a PR, just open a new Gor issue and include the following info:
|
||||
|
||||
1. the email address that bought the license, a max of one collaborator per license
|
||||
1. the following statement "I assign all rights, including copyright, to any future Gor work by myself to Leonid Bugaev"
|
||||
|
||||
You should be granted access to the private repo soon after.
|
||||
|
||||
## Notes
|
||||
|
||||
1. You should **never** work on the master branch. Only I may merge changes.
|
||||
1. I may revoke access for any reason at any time. Access is not guaranteed with purchase.
|
||||
@@ -0,0 +1,58 @@
|
||||
### What are GoReplay PRO and GoReplay Enterprise?
|
||||
|
||||
[GoReplay PRO](https://goreplay.org/pro.html) and GoReplay Enterprise are extensions to GoReplay which add more functionality and provide additional support options for customers.
|
||||
|
||||
### Is there a trial version?
|
||||
|
||||
There's no free trial but we do offer a 14 day period with full refund if it does not work for you.
|
||||
|
||||
### What is the license?
|
||||
|
||||
See [COMM-LICENSE](https://github.com/buger/gor/blob/master/COMM-LICENSE) in the root of the GoReplay repo.
|
||||
|
||||
### How does PRO licensing work?
|
||||
|
||||
Every organization running GoReplay Pro on its own servers must have a license. There's no limit to the amount of servers or environments used by that organization.
|
||||
|
||||
### How does Enterprise licensing work?
|
||||
|
||||
Every organization running Gor Enterprise on its own servers must have a license. There's **no limit** to the amount of servers or environments used by that organization.
|
||||
|
||||
### What happens if my subscription lapses?
|
||||
|
||||
You must have an active subscription to run GoReplay Pro or Enterprise. After a one week grace period, you'll lose access to binaries and priority support. You won't get any more updates or bug fixes.
|
||||
|
||||
### How do I buy GoReplay Enterprise?
|
||||
|
||||
Send email to [support@gortool.com](mailto:support@gortool.com) with your info. A PDF quote will be emailed to you with the price. Reply to that email with your purchase order or just "Sounds good" and we will send an invoice which can be paid with a credit card, ACH bank transfer or a paper check.
|
||||
|
||||
### Can I upgrade from GoReplay Pro?
|
||||
|
||||
Yes! Current subscribers can upgrade by [requesting a quote](mailto:support@gortool.com). **Please note that you are an existing Pro subscriber.** We will add a one-time discount on your first invoice to reflect any remaining Pro subscription credit. If you purchased GoReplay Pro 6 months ago for $950, you'll get a $475 discount.
|
||||
|
||||
### Can I distribute GoReplay PRO or Enterprise to my customers?
|
||||
|
||||
This is a common requirement for "on-site installs" or "appliances" sold to large corporations.
|
||||
|
||||
The standard license is appropriate for SaaS usage as it does not allow distribution. GoReplay PRO and Enterprise have an Appliance license option which **does** allow you to distribute them. The Appliance license is $9,500/yr for Pro and $19,500/yr for Enterprise. It allows you to distribute the Pro or Enterprise binaries as part of your application and each of your customers to run GoReplay Pro or Enterprise. Email [support@gortool.com](mailto:support@gortool.com) to purchase.
|
||||
|
||||
### Can you transfer a license?
|
||||
|
||||
Licenses are **not** transferable to another company. We will transfer the license from a user-specific email to a group email address (e.g. john_smith@example.com -> tech@example.com) but only for **the same domain**. It is strongly recommended that you buy the license using a group email address so the license is not attached to any one employee's email address.
|
||||
|
||||
### What does the license require me to do?
|
||||
|
||||
Your purchase gets you a unique access URL for downloading the Pro and/or Enterprise binaries. The license agreement requires you to keep this access URL private. If we find your access URL is ever publicized:
|
||||
|
||||
1. We'll send you a warning email with details. You need to remove the content and send a new email address so we can generate a new access URL for you. The old access URL will stop working immediately so you'll need to update your apps.
|
||||
2. If your access URL is publicized a second time, we reserve the right to permanently remove access.
|
||||
|
||||
### Can I get a refund?
|
||||
|
||||
Yes, up to two weeks after purchase. Let us know the reason and maybe we can help but either way it's not a problem. Email [support@gortool.com](mailto:support@gortool.com).
|
||||
|
||||
### How do I update my credit card info?
|
||||
|
||||
If you purchased GoReplay Enterprise, there's nothing to do. Each annual invoice is paid separately.
|
||||
|
||||
If you purchased GoReplay PRO, log into [Gumroad](https://gumroad.com) with your email address, click the Billing tab and enter your new card. I can't provide support for the Gumroad website and don't have the ability to edit customer info - if you can't log in or change your credit card, you can always let your current subscription expire and purchase a new subscription.
|
||||
@@ -0,0 +1,11 @@
|
||||
Gor offers only community support. Gor Pro and Enterprise offer priority support via email.
|
||||
|
||||
## Priority Support
|
||||
|
||||
Covers 1 incident per quarter, with a max response time of 2 working days. Scope is limited to Gor and Gor Pro and Enterprise features and APIs, not the application or infrastructure. For support, email **support** AT **gortool.com**. Please email using the same domain as the original license email or explain your connection to the licensed company.
|
||||
|
||||
More aggressive support contracts (phone, quicker response time) are available separately, email with your needs.
|
||||
|
||||
## Onboarding
|
||||
|
||||
Enterprise customers may request a one hour video chat session with @buger to discuss their application(s), requirements and how best to leverage the various Gor features. Contact support to set up your session.
|
||||
@@ -0,0 +1,39 @@
|
||||
.wy-breadcrumbs li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.wy-breadcrumbs li.wy-breadcrumbs-aside {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.wy-breadcrumbs li a {
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.wy-breadcrumbs li a:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.wy-breadcrumbs-extra {
|
||||
margin-bottom: 0;
|
||||
color: #b3b3b3;
|
||||
font-size: 80%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.wy-breadcrumbs-extra {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wy-breadcrumbs li.wy-breadcrumbs-aside {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.wy-breadcrumbs li.wy-breadcrumbs-aside {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
.codeblock-example {
|
||||
border: 1px solid #e1e4e5;
|
||||
border-bottom: none;
|
||||
padding: 24px;
|
||||
padding-top: 48px;
|
||||
font-weight: 500;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.codeblock-example:after {
|
||||
content: "Example";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: #9B59B6;
|
||||
color: #fff;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.codeblock-example.prettyprint-example-only {
|
||||
border: 1px solid #e1e4e5;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.codeblock,
|
||||
pre.literal-block,
|
||||
.rst-content .literal-block,
|
||||
.rst-content pre.literal-block,
|
||||
div[class^='highlight'] {
|
||||
border: 1px solid #e1e4e5;
|
||||
padding: 0;
|
||||
overflow-x: auto;
|
||||
background: #fff;
|
||||
margin: 1px 0 24px;
|
||||
}
|
||||
|
||||
.codeblock div[class^='highlight'],
|
||||
pre.literal-block div[class^='highlight'],
|
||||
.rst-content .literal-block div[class^='highlight'],
|
||||
div[class^='highlight'] div[class^='highlight'] {
|
||||
border: none;
|
||||
background: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div[class^='highlight'] td.code {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.linenodiv pre {
|
||||
border-right: solid 1px #e6e9ea;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
font-family: "Source Code Pro","Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #d9d9d9;
|
||||
}
|
||||
|
||||
div[class^='highlight'] pre {
|
||||
white-space: pre;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
font-family: "Source Code Pro","Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
color: #404040;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.codeblock,
|
||||
pre.literal-block,
|
||||
.rst-content .literal-block,
|
||||
.rst-content pre.literal-block,
|
||||
div[class^='highlight'],
|
||||
div[class^='highlight'] pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hll {
|
||||
background-color: #ffc;
|
||||
margin: 0 -12px;
|
||||
padding: 0 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.c {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.err {
|
||||
color: #a61717;
|
||||
background-color: #e3d2d2;
|
||||
}
|
||||
|
||||
.k {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.o {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cm {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cp {
|
||||
color: #999;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cs {
|
||||
color: #999;
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.gd {
|
||||
color: #000;
|
||||
background-color: #fdd;
|
||||
}
|
||||
|
||||
.gd .x {
|
||||
color: #000;
|
||||
background-color: #faa;
|
||||
}
|
||||
|
||||
.ge {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.gr {
|
||||
color: #a00
|
||||
}
|
||||
|
||||
.gh {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.gi {
|
||||
color: #000;
|
||||
background-color: #dfd;
|
||||
}
|
||||
|
||||
.gi .x {
|
||||
color: #000;
|
||||
background-color: #afa;
|
||||
}
|
||||
|
||||
.go {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.gp {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.gs {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gu {
|
||||
color: purple;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gt {
|
||||
color: #a00;
|
||||
}
|
||||
|
||||
.kc {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.kd {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.kn {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.kp {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.kr {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.kt {
|
||||
color: #458;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.m {
|
||||
color: #099;
|
||||
}
|
||||
|
||||
.s {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.n {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.na {
|
||||
color: teal;
|
||||
}
|
||||
|
||||
.nb {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.nc {
|
||||
color: #458;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.no {
|
||||
color: teal;
|
||||
}
|
||||
|
||||
.ni {
|
||||
color: purple;
|
||||
}
|
||||
|
||||
.ne {
|
||||
color: #900;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nf {
|
||||
color: #900;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nn {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.nt {
|
||||
color: navy;
|
||||
}
|
||||
|
||||
.nv {
|
||||
color: teal;
|
||||
}
|
||||
|
||||
.ow {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.w {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.mf {
|
||||
color: #099;
|
||||
}
|
||||
|
||||
.mh {
|
||||
color: #099;
|
||||
}
|
||||
|
||||
.mi {
|
||||
color: #099;
|
||||
}
|
||||
|
||||
.mo {
|
||||
color: #099;
|
||||
}
|
||||
|
||||
.sb {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.sc {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.sd {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.s2 {
|
||||
color: #d14
|
||||
}
|
||||
|
||||
.se {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.sh {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.si {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.sx {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.sr {
|
||||
color: #009926;
|
||||
}
|
||||
|
||||
.s1 {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.ss {
|
||||
color: #990073;
|
||||
}
|
||||
|
||||
.bp {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.vc {
|
||||
color: teal;
|
||||
}
|
||||
|
||||
.vg {
|
||||
color: teal;
|
||||
}
|
||||
|
||||
.vi {
|
||||
color: teal;
|
||||
}
|
||||
|
||||
.il {
|
||||
color: #099;
|
||||
}
|
||||
|
||||
.gc {
|
||||
color: #999;
|
||||
background-color: #EAF2F5;
|
||||
}
|
||||
+3442
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,171 @@
|
||||
code {
|
||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace !important;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
background-color: #f7f7f7 !important;
|
||||
color: #333 !important;
|
||||
font-size: 14px !important;
|
||||
border: none !important;
|
||||
padding: 5px !important;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
line-height: 20px;
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
color: #777 !important;
|
||||
border-left: 4px solid rgb(221, 221, 221);
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
margin-left: 0px !important;
|
||||
}
|
||||
|
||||
/* Visited links purple is not a good idea */
|
||||
a:visited {
|
||||
color: #008BF3;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #008BF3;
|
||||
}
|
||||
|
||||
h3, h4, h5, h6 {
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
.rst-versions {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* The next and previous button we don't need */
|
||||
.rst-footer-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Foldable parts */
|
||||
details > * {
|
||||
margin-left: 20px;
|
||||
}
|
||||
details > summary {
|
||||
margin-left: 0px; /* To overwrite the margin from above */
|
||||
|
||||
font-size: 120%;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
/* Fix the design not having the correct spacing */
|
||||
.wy-menu-vertical .subnav li.current > a {
|
||||
padding: 0.4045em 2.427em;
|
||||
}
|
||||
|
||||
.toctree-l3 {
|
||||
padding-left: 1.0em;
|
||||
}
|
||||
|
||||
footer .fastlane {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
footer .fastlane iframe {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Custom Syntax highlighting to look more like GitHub.com */
|
||||
.hljs-symbol {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-keyword {
|
||||
font-weight: normal;
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.hljs-string {
|
||||
color: #183691;
|
||||
}
|
||||
|
||||
/* Header Anchors */
|
||||
/* From https://github.com/facelessuser/pymdown-extensions/blob/bf18d0635e9d91b0f98eacdcaa10f26e0ace0f20/doc_theme/css/theme_custom.css#L322-L395 */
|
||||
.rst-content .headeranchor-link {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
padding-right: 6px;
|
||||
padding-left: 30px;
|
||||
margin-left: -30px;
|
||||
}
|
||||
|
||||
.rst-content .headeranchor-link:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.rst-content h1,
|
||||
.rst-content h2,
|
||||
.rst-content h3,
|
||||
.rst-content h4,
|
||||
.rst-content h5,
|
||||
.rst-content h6 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rst-content h1 .headeranchor,
|
||||
.rst-content h2 .headeranchor,
|
||||
.rst-content h3 .headeranchor,
|
||||
.rst-content h4 .headeranchor,
|
||||
.rst-content h5 .headeranchor,
|
||||
.rst-content h6 .headeranchor {
|
||||
display: none;
|
||||
color: #000;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.rst-content h1:hover .headeranchor-link,
|
||||
.rst-content h2:hover .headeranchor-link,
|
||||
.rst-content h3:hover .headeranchor-link,
|
||||
.rst-content h4:hover .headeranchor-link,
|
||||
.rst-content h5:hover .headeranchor-link,
|
||||
.rst-content h6:hover .headeranchor-link {
|
||||
height: 1em;
|
||||
padding-left: 8px;
|
||||
margin-left: -30px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.rst-content h1:hover .headeranchor-link .headeranchor,
|
||||
.rst-content h2:hover .headeranchor-link .headeranchor,
|
||||
.rst-content h3:hover .headeranchor-link .headeranchor,
|
||||
.rst-content h4:hover .headeranchor-link .headeranchor,
|
||||
.rst-content h5:hover .headeranchor-link .headeranchor,
|
||||
.rst-content h6:hover .headeranchor-link .headeranchor {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.rst-content .headeranchor {
|
||||
font: normal normal 16px FontAwesome;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.rst-content .headeranchor:before {
|
||||
content: '\f0c1';
|
||||
}
|
||||
|
||||
/* index.md badges */
|
||||
@media screen and (max-width: 768px) {
|
||||
.badge img {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
.wy-affix {
|
||||
position: fixed;
|
||||
top: 1.618em;
|
||||
}
|
||||
|
||||
.wy-menu a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wy-menu-vertical header,
|
||||
.wy-menu-vertical p.caption {
|
||||
height: 32px;
|
||||
display: inline-block;
|
||||
line-height: 32px;
|
||||
padding: 0 1.618em;
|
||||
margin-bottom: 0;
|
||||
margin-top: 14px;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
font-size: 85%;
|
||||
color: #ccc;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wy-menu-vertical span {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.wy-menu-vertical ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.divide-top {
|
||||
border-top: solid 1px #404040;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.divide-bottom {
|
||||
border-bottom: solid 1px #404040;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.current {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.current a {
|
||||
color: rgba(0, 93, 255, 0.7);
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.current a:hover {
|
||||
color: rgba(0, 93, 255, 0.9);
|
||||
}
|
||||
|
||||
.wy-menu-vertical li code,
|
||||
.wy-menu-vertical li .rst-content tt,
|
||||
.rst-content .wy-menu-vertical li tt {
|
||||
border: none;
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
padding-left: 0;
|
||||
padding-right: 0
|
||||
}
|
||||
|
||||
.wy-menu-vertical li span.toctree-expand {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-left: -1.2em;
|
||||
font-size: .8em;
|
||||
line-height: 1.6em;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.on a,
|
||||
.wy-menu-vertical li.current>a {
|
||||
color: rgba(0, 93, 255, 0.9);
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
background: #fafafa;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/*.wy-menu-vertical li.on a:hover span.toctree-expand,
|
||||
.wy-menu-vertical li.current>a:hover span.toctree-expand {
|
||||
color: gray;
|
||||
}*/
|
||||
|
||||
.wy-menu-vertical li.on a span.toctree-expand,
|
||||
.wy-menu-vertical li.current>a span.toctree-expand {
|
||||
display: block;
|
||||
font-size: .8em;
|
||||
line-height: 1.6em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul,
|
||||
.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul,
|
||||
.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a {
|
||||
display: block;
|
||||
padding: .4045em 4.045em;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l2 span.toctree-expand {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l3 {
|
||||
background-color: #eee;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l3.current>a {
|
||||
padding: .4045em 4.045em;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a {
|
||||
display: block;
|
||||
padding: .4045em 5.663em;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand {
|
||||
color: rgba(0, 93, 255, 0.9);
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l3 span.toctree-expand {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l4 {
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.current ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wy-menu-vertical .local-toc li ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li ul li a {
|
||||
margin-bottom: 0;
|
||||
color: rgba(0, 93, 255, 0.7);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.wy-menu-vertical a {
|
||||
display: inline-block;
|
||||
line-height: 18px;
|
||||
padding: .4045em 1.618em;
|
||||
display: block;
|
||||
position: relative;
|
||||
font-size: 90%;
|
||||
color: rgba(0, 93, 255, 0.7);
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.on a:hover,
|
||||
.wy-menu-vertical li.current>a:hover {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.wy-menu-vertical a:hover {
|
||||
color: rgba(0, 93, 255, 0.9);
|
||||
cursor: pointer;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.wy-menu-vertical a:hover span.toctree-expand {
|
||||
color: rgba(0, 93, 255, 0.5);
|
||||
}
|
||||
|
||||
.wy-menu-vertical a:active span.toctree-expand {
|
||||
color: rgba(0, 93, 255, 0.7);
|
||||
}
|
||||
|
||||
/* Search */
|
||||
|
||||
.wy-side-nav-search {
|
||||
z-index: 200;
|
||||
background-color: #fafafa;
|
||||
border-bottom: #333;
|
||||
text-align: center;
|
||||
padding: .809em;
|
||||
display: block;
|
||||
color: #333;
|
||||
margin-bottom: .809em
|
||||
}
|
||||
|
||||
.wy-side-nav-search input[type=text] {
|
||||
width: 100%;
|
||||
color: #333;
|
||||
border-radius: 3px;
|
||||
outline: 0;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
border: solid 1px #6d6d6d;
|
||||
box-shadow: none
|
||||
}
|
||||
|
||||
.wy-side-nav-search img {
|
||||
display: block;
|
||||
margin: auto auto .809em;
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
background-color: #2980B9;
|
||||
padding: 5px;
|
||||
border-radius: 100%
|
||||
}
|
||||
|
||||
.wy-side-nav-search>a,
|
||||
.wy-side-nav-search .wy-dropdown>a {
|
||||
color: #333;
|
||||
font-size: 100%;
|
||||
font-weight: 700;
|
||||
display: inline-block;
|
||||
padding: 4px 6px;
|
||||
margin-bottom: .809em
|
||||
}
|
||||
|
||||
.wy-side-nav-search>a:hover,
|
||||
.wy-side-nav-search .wy-dropdown>a:hover {
|
||||
background: rgba(255,255,255,0.1)
|
||||
}
|
||||
|
||||
.wy-side-nav-search>a img.logo,
|
||||
.wy-side-nav-search .wy-dropdown>a img.logo {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
height: auto;
|
||||
width: auto;
|
||||
border-radius: 0;
|
||||
max-width: 100%;
|
||||
background: transparent
|
||||
}
|
||||
|
||||
.wy-side-nav-search>a.icon img.logo,
|
||||
.wy-side-nav-search .wy-dropdown>a.icon img.logo {
|
||||
margin-top: .85em
|
||||
}
|
||||
|
||||
.wy-nav .wy-menu-vertical header {
|
||||
color: #2980B9
|
||||
}
|
||||
|
||||
.wy-nav .wy-menu-vertical a {
|
||||
color: #b3b3b3
|
||||
}
|
||||
|
||||
.wy-nav .wy-menu-vertical a:hover {
|
||||
background-color: #2980B9;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
[data-menu-wrap] {
|
||||
-webkit-transition: all .2s ease-in;
|
||||
-moz-transition: all .2s ease-in;
|
||||
transition: all .2s ease-in;
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
[data-menu-wrap].move-center {
|
||||
left: 0;
|
||||
right: auto;
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
[data-menu-wrap].move-left {
|
||||
right: auto;
|
||||
left: -100%;
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
[data-menu-wrap].move-right {
|
||||
right: -100%;
|
||||
left: auto;
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
.wy-body-for-nav {
|
||||
background: left repeat-y #fcfcfc;
|
||||
background-image: url(data:image/png;
|
||||
base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC);
|
||||
background-size: 300px 1px
|
||||
}
|
||||
|
||||
.wy-grid-for-nav {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.wy-nav-side {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding-bottom: 2em;
|
||||
width: 300px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
min-height: 100%;
|
||||
background-color: #fafafa;
|
||||
z-index: 200;
|
||||
border-right: 2px solid #eee;
|
||||
}
|
||||
|
||||
.wy-nav-top {
|
||||
display: none;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
padding: .4045em .809em;
|
||||
position: relative;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
font-size: 100%;
|
||||
*zoom: 1
|
||||
}
|
||||
|
||||
.wy-nav-top:before,
|
||||
.wy-nav-top:after {
|
||||
display: table;
|
||||
content: ""
|
||||
}
|
||||
|
||||
.wy-nav-top:after {
|
||||
clear: both
|
||||
}
|
||||
|
||||
.wy-nav-top a {
|
||||
color: #fff;
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.wy-nav-top img {
|
||||
margin-right: 12px;
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
background-color: #2980B9;
|
||||
padding: 5px;
|
||||
border-radius: 100%
|
||||
}
|
||||
|
||||
.wy-nav-top i {
|
||||
font-size: 30px;
|
||||
line-height: 50px;
|
||||
float: left;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.wy-nav-content-wrap {
|
||||
margin-left: 300px;
|
||||
background: #fcfcfc;
|
||||
min-height: 100%
|
||||
}
|
||||
|
||||
.wy-nav-content {
|
||||
padding: 1.618em 3.236em;
|
||||
height: 100%;
|
||||
max-width: 1100px;
|
||||
margin: auto
|
||||
}
|
||||
|
||||
.wy-body-mask {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.2);
|
||||
display: none;
|
||||
z-index: 499
|
||||
}
|
||||
|
||||
.wy-body-mask.on {
|
||||
display: block
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.wy-body-for-nav {
|
||||
background: #fcfcfc
|
||||
}
|
||||
|
||||
.wy-nav-top {
|
||||
display: block
|
||||
}
|
||||
|
||||
.wy-nav-side {
|
||||
left: -300px
|
||||
}
|
||||
|
||||
.wy-nav-side.shift {
|
||||
width: 85%;
|
||||
left: 0
|
||||
}
|
||||
|
||||
.wy-nav-content-wrap {
|
||||
margin-left: 0
|
||||
}
|
||||
|
||||
.wy-nav-content-wrap .wy-nav-content {
|
||||
padding: 1.618em
|
||||
}
|
||||
|
||||
.wy-nav-content-wrap.shift {
|
||||
position: fixed;
|
||||
min-width: 100%;
|
||||
left: 85%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1400px) {
|
||||
.wy-nav-content {
|
||||
margin: 0;
|
||||
background: #fcfcfc
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
### Overview
|
||||
Gor architecture tries to follow UNIX philosophy: everything made of pipes, various inputs multiplexing data to outputs.
|
||||
|
||||
You can [rate limit](/rate-limiting.md), [filter](request-filtering.md), [rewrite](request-rewriting.md) requests or even use your own [middleware](middleware.md) to implement custom logic. Also, it is possible to replay requests at the higher rate for [load testing](saving-and-replaying-from-file.md).
|
||||
|
||||
### Available inputs
|
||||
|
||||
* `--input-raw` - used to capture HTTP traffic, you should specify IP address or interface and application port. More about [[Capturing and replaying traffic]].
|
||||
* `--input-file` - accepts file which previously was recorded using ` --output-file`. More about [[Saving and Replaying from file]]
|
||||
* `--input-tcp` - used by Gor aggregation instance if you decided forward traffic from multiple forwarder Gor instances to it. Read about using [[Aggregator-forwarder setup]].
|
||||
|
||||
### Available outputs
|
||||
|
||||
* `--output-http` - replay HTTP traffic to given endpoint, accepts base url. Read [more about it](Replaying HTTP traffic)
|
||||
* `--output-file` - records incoming traffic to the file. More about [[Saving and Replaying from file]]
|
||||
* `--output-tcp` - forward incoming data to another Gor instance, used in conjunction with `--input-tcp`. Read more about [[Aggregator-forwarder setup]].
|
||||
* `--output-stdout` - used for debugging, outputs all data to stdout.
|
||||
@@ -0,0 +1,44 @@
|
||||
### Dependencies
|
||||
To start working with Gor, you need to have a web server running on your machine, and a terminal to run commands. If you are just poking around, you can quickly start the server by calling `gor file-server :8000`, this will start a simple file server of the current directory on port `8000`.
|
||||
|
||||
### Installing Gor
|
||||
Download the latest Gor binary from https://github.com/buger/gor/releases (we provide precompiled binaries for Windows, Linux x64 and Mac OS), or you can compile by yourself [[Compilation]].
|
||||
|
||||
Once the archive is downloaded and uncompressed, you can run Gor from the current directory, or you may want to copy binary to your PATH (for Linux and Mac OS it can be `/usr/local/bin`).
|
||||
|
||||
### Capturing web traffic
|
||||
Now run this command in terminal: `sudo ./gor --input-raw :8000 --output-stdout`
|
||||
|
||||
This command says to listen for all network activity happening on port 8000 and log it to stdout.
|
||||
If you are familiar with `tcpdump`, we are going to implement similar functionality.
|
||||
|
||||
> You may notice that it uses `sudo` and asks for the password: to analyze network, Gor needs permissions which are available only to super users.
|
||||
> However, it is possible to configure Gor [being run for non-root users](Running-as-non-root-user).
|
||||
|
||||
|
||||
Make a few requests by opening `http://localhost:8000` in your browser, or just by calling curl in terminal `curl http://localhost:8000`. You should see that `gor` outputs all the HTTP requests and responses right to the terminal window where it is running.
|
||||
|
||||
|
||||
**Gor is not a proxy:** you do not need to put 3-rd party tool to your critical path. Instead Gor just silently analyzes the traffic of your application and does not affect it anyhow.
|
||||
|
||||
### Replaying
|
||||
|
||||
Now it's time to replay your original traffic to another environment. Let's start the same file web server but on a different port: `gor file-server :8001`.
|
||||
|
||||
Instead of `--output-stdout` we will use `--output-http` and provide URL of second server: `sudo ./gor --input-raw :8000 --output-http="http://localhost:8001"`
|
||||
|
||||
Make few requests to first server. You should see them replicated to the second one, voila!
|
||||
|
||||
### Saving requests to file and replaying them later
|
||||
Sometimes it's not possible to replay requests in real time; Gor allows you to save requests to the file and replay them later.
|
||||
|
||||
First use `--output-file` to save them: `sudo ./gor --input-raw :8000 --output-file=requests.gor`. This will create new file and continuously write all captured requests to it.
|
||||
|
||||
Let's re-run Gor, but now to replay requests from file: `./gor --input-file requests.gor --output-http="http://localhost:8001"`. You should see all the recorded requests coming to the second server, and they will be replayed in the same order and with exactly same timing as they were recorded.
|
||||
|
||||
Next: [[The Basics]]
|
||||
|
||||
### Watch an overview:
|
||||
|
||||
|
||||

|
||||
@@ -0,0 +1,29 @@
|
||||
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.
|
||||
|
||||
Read for more info:
|
||||
|
||||
* [[Getting Started]]
|
||||
* [[The Basics]]
|
||||
* [[Capturing and replaying traffic]]
|
||||
* [[Replaying HTTP traffic]]
|
||||
* [[[PRO] Replaying Binary protocols]]
|
||||
* [[[PRO] Recording and replaying keep alive TCP sessions]]
|
||||
* [[Saving and Replaying from file]]
|
||||
* [Performance testing](https://github.com/buger/gor/wiki/Saving-and-Replaying-from-file#performance-testing)
|
||||
* [[Rate limiting]]
|
||||
* [[Request filtering]]
|
||||
* [[Request rewriting]]
|
||||
* [[Middleware]]
|
||||
* [[Distributed configuration]]
|
||||
* [[Exporting to ElasticSearch]]
|
||||
* [[FAQ]]
|
||||
* [[Troubleshooting]]
|
||||
|
||||
## Commercial Aspects
|
||||
|
||||
* [[Commercial Support]]
|
||||
* [[Commercial FAQ]]
|
||||
* [[Commercial collaboration]]
|
||||
|
||||
|
||||
Next: [Getting Started](Getting-Started)
|
||||
@@ -0,0 +1,7 @@
|
||||
$(document).ready(function () {
|
||||
$('img[alt="YOUTUBE"]').each(function () {
|
||||
var id = $(this).attr('src').split('/')[$(this).attr('src').split('/').length - 1].split("=")[1];
|
||||
var video = '<iframe style="width: 100%;height: 450px;" src="https://www.youtube.com/embed/' + id + '" frameborder="0" allowfullscreen></iframe>';
|
||||
$(this).replaceWith(video);
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
> **This feature available only in PRO version. See https://goreplay.org/pro.html for details.**
|
||||
|
||||
By default, GoReplay does not guarantee that when you record keep-alive TCP session, it will be replayed in the same TCP connection as well. This is ok for most of the cases, but it does not give an accurate number of TCP sessions while replaying, also may cause issues if your application state depends on TCP session (do not mess with HTTP session).
|
||||
|
||||
[GoReplay PRO](https://goreplay.org/pro.html) extension adds support for accurate recording and replaying of keep-alive TCP sessions. Separate connection to your server is created per original session and it makes benchmarks and tests incredibly accurate. To enable session recognition you just need to pass `--recognize-tcp-sessions` option.
|
||||
|
||||
```
|
||||
gor --input-raw :80 --recognize-tcp-sessions --output-http http://test.target
|
||||
```
|
||||
|
||||
Note that enabling this option also change algorithm of distributing traffic when using `--split-output`, see [Distributed configuration].
|
||||
@@ -0,0 +1,14 @@
|
||||
> **This feature available only in PRO version. See https://goreplay.org/pro.html for details.**
|
||||
|
||||
Gor includes basic support for working with binary formats like `thrift` or `protocol-buffers`. To start set `--input-raw-protocol` to 'binary' (by default 'http'). For replaying, you should use `--output-binary`, example:
|
||||
|
||||
```
|
||||
gor --input-raw :80 --input-raw-protocol binary --output-binary staging:8081
|
||||
```
|
||||
|
||||
While working with `--input-raw` you may notice a 2-second delay before messages are emitted to the outputs. This behaviour is expected and happening because for general binary protocol it is impossible to know when TCP message ends, so Gor has to set inactivity timeout. Each protocol has own rules (for example write message length as first bytes), and require individual handling to know message length. We consider improving detailed protocol support for `thrift`, `protocol-buffer` and etc.
|
||||
|
||||
Note that you can use all load testing features for binary protocols. For example, the following command will loop and replay recorded payload on 10x speed for 30 seconds:
|
||||
```
|
||||
gor --input-file './binary*.gor|1000%' --output-binary staging:9091 --input-file-loop --exit-after 30s
|
||||
```
|
||||
+83
-65
@@ -2,67 +2,81 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/buger/elastigo/api"
|
||||
"github.com/buger/elastigo/core"
|
||||
"github.com/buger/gor/proto"
|
||||
"log"
|
||||
"regexp"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
|
||||
elastigo "github.com/mattbaird/elastigo/lib"
|
||||
)
|
||||
|
||||
type ESUriErorr struct{}
|
||||
|
||||
func (e *ESUriErorr) Error() string {
|
||||
return "Wrong ElasticSearch URL format. Expected to be: host:port/index_name"
|
||||
return "Wrong ElasticSearch URL format. Expected to be: scheme://host/index_name"
|
||||
}
|
||||
|
||||
type ESPlugin struct {
|
||||
Active bool
|
||||
ApiPort string
|
||||
eConn *elastigo.Conn
|
||||
Host string
|
||||
Index string
|
||||
indexor *core.BulkIndexer
|
||||
indexor *elastigo.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"`
|
||||
ReqURL string `json:"Req_URL"`
|
||||
ReqMethod string `json:"Req_Method"`
|
||||
ReqUserAgent string `json:"Req_User-Agent"`
|
||||
ReqAcceptLanguage string `json:"Req_Accept-Language,omitempty"`
|
||||
ReqAccept string `json:"Req_Accept,omitempty"`
|
||||
ReqAcceptEncoding string `json:"Req_Accept-Encoding,omitempty"`
|
||||
ReqIfModifiedSince string `json:"Req_If-Modified-Since,omitempty"`
|
||||
ReqConnection string `json:"Req_Connection,omitempty"`
|
||||
ReqCookies string `json:"Req_Cookies,omitempty"`
|
||||
RespStatus string `json:"Resp_Status"`
|
||||
RespStatusCode string `json:"Resp_Status-Code"`
|
||||
RespProto string `json:"Resp_Proto,omitempty"`
|
||||
RespContentLength string `json:"Resp_Content-Length,omitempty"`
|
||||
RespContentType string `json:"Resp_Content-Type,omitempty"`
|
||||
RespTransferEncoding string `json:"Resp_Transfer-Encoding,omitempty"`
|
||||
RespContentEncoding string `json:"Resp_Content-Encoding,omitempty"`
|
||||
RespExpires string `json:"Resp_Expires,omitempty"`
|
||||
RespCacheControl string `json:"Resp_Cache-Control,omitempty"`
|
||||
RespVary string `json:"Resp_Vary,omitempty"`
|
||||
RespSetCookie string `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)
|
||||
// Proper format is: scheme://[userinfo@]host/index_name
|
||||
// userinfo is: user[:password]
|
||||
// net/url.Parse() does not fail if scheme is not provided but actualy does not
|
||||
// handle URI properly.
|
||||
// So we must 'validate' URI format to match requirements to use net/url.Parse()
|
||||
func parseURI(URI string) (err error, index string) {
|
||||
|
||||
if len(match) == 0 {
|
||||
parsedUrl, parseErr := url.Parse(URI)
|
||||
|
||||
if parseErr != nil {
|
||||
err = new(ESUriErorr)
|
||||
return
|
||||
}
|
||||
|
||||
// check URL validity by extracting host and undex values.
|
||||
host := parsedUrl.Host
|
||||
urlPathParts := strings.Split(parsedUrl.Path, "/")
|
||||
index = urlPathParts[len(urlPathParts)-1]
|
||||
|
||||
// force index specification in uri : ie no implicit index
|
||||
if host == "" || index == "" {
|
||||
err = new(ESUriErorr)
|
||||
} else {
|
||||
host = match[0][1]
|
||||
port = match[0][2]
|
||||
index = match[0][3]
|
||||
}
|
||||
|
||||
return
|
||||
@@ -71,29 +85,32 @@ func parseURI(URI string) (err error, host string, port string, index string) {
|
||||
func (p *ESPlugin) Init(URI string) {
|
||||
var err error
|
||||
|
||||
err, p.Host, p.ApiPort, p.Index = parseURI(URI)
|
||||
err, p.Index = parseURI(URI)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Can't initialize ElasticSearch plugin.", err)
|
||||
}
|
||||
|
||||
api.Domain = p.Host
|
||||
api.Port = p.ApiPort
|
||||
p.eConn = elastigo.NewConn()
|
||||
|
||||
p.indexor = core.NewBulkIndexerErrors(50, 60)
|
||||
p.eConn.SetFromUrl(URI)
|
||||
|
||||
p.indexor = p.eConn.NewBulkIndexerErrors(50, 60)
|
||||
p.done = make(chan bool)
|
||||
p.indexor.Run(p.done)
|
||||
p.indexor.Start()
|
||||
|
||||
// Only start the ErrorHandler goroutine when in verbose mode
|
||||
// no need to burn ressources otherwise
|
||||
// go p.ErrorHandler()
|
||||
if Settings.Verbose {
|
||||
// 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
|
||||
p.indexor.Stop()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -118,28 +135,29 @@ func (p *ESPlugin) ResponseAnalyze(req, resp []byte, start, stop time.Time) {
|
||||
}
|
||||
t := time.Now()
|
||||
rtt := p.RttDurationToMs(stop.Sub(start))
|
||||
req = payloadBody(req)
|
||||
|
||||
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")),
|
||||
ReqURL: string(proto.Path(req)),
|
||||
ReqMethod: string(proto.Method(req)),
|
||||
ReqUserAgent: string(proto.Header(req, []byte("User-Agent"))),
|
||||
ReqAcceptLanguage: string(proto.Header(req, []byte("Accept-Language"))),
|
||||
ReqAccept: string(proto.Header(req, []byte("Accept"))),
|
||||
ReqAcceptEncoding: string(proto.Header(req, []byte("Accept-Encoding"))),
|
||||
ReqIfModifiedSince: string(proto.Header(req, []byte("If-Modified-Since"))),
|
||||
ReqConnection: string(proto.Header(req, []byte("Connection"))),
|
||||
ReqCookies: string(proto.Header(req, []byte("Cookie"))),
|
||||
RespStatus: string(proto.Status(resp)),
|
||||
RespStatusCode: string(proto.Status(resp)),
|
||||
RespProto: string(proto.Method(resp)),
|
||||
RespContentLength: string(proto.Header(resp, []byte("Content-Length"))),
|
||||
RespContentType: string(proto.Header(resp, []byte("Content-Type"))),
|
||||
RespTransferEncoding: string(proto.Header(resp, []byte("Transfer-Encoding"))),
|
||||
RespContentEncoding: string(proto.Header(resp, []byte("Content-Encoding"))),
|
||||
RespExpires: string(proto.Header(resp, []byte("Expires"))),
|
||||
RespCacheControl: string(proto.Header(resp, []byte("Cache-Control"))),
|
||||
RespVary: string(proto.Header(resp, []byte("Vary"))),
|
||||
RespSetCookie: string(proto.Header(resp, []byte("Set-Cookie"))),
|
||||
Rtt: rtt,
|
||||
Timestamp: t,
|
||||
}
|
||||
@@ -147,7 +165,7 @@ func (p *ESPlugin) ResponseAnalyze(req, resp []byte, start, stop time.Time) {
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
p.indexor.Index(p.Index, "RequestResponse", "", "", &t, j, true)
|
||||
p.indexor.Index(p.Index, "RequestResponse", "", "", "", &t, j)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
const expectedIndex = "gor"
|
||||
|
||||
func assertExpectedGorIndex(index string, t *testing.T) {
|
||||
if expectedIndex != index {
|
||||
t.Fatalf("Expected index %s but got %s", expectedIndex, index)
|
||||
}
|
||||
}
|
||||
|
||||
func assertExpectedIndex(expectedIndex string, index string, t *testing.T) {
|
||||
if expectedIndex != index {
|
||||
t.Fatalf("Expected index %s but got %s", expectedIndex, index)
|
||||
}
|
||||
}
|
||||
|
||||
func assertExpectedError(returnedError error, t *testing.T) {
|
||||
expectedError := new(ESUriErorr)
|
||||
|
||||
if expectedError != returnedError {
|
||||
t.Errorf("Expected err %s but got %s", expectedError, returnedError)
|
||||
}
|
||||
}
|
||||
|
||||
func assertNoError(returnedError error, t *testing.T) {
|
||||
if nil != returnedError {
|
||||
t.Errorf("Expected no err but got %s", returnedError)
|
||||
}
|
||||
}
|
||||
|
||||
// Argument host:port/index_name
|
||||
// i.e : localhost:9200/gor
|
||||
// Fail because scheme is mandatory
|
||||
func TestElasticConnectionBuildFailWithoutScheme(t *testing.T) {
|
||||
uri := "localhost:9200/" + expectedIndex
|
||||
|
||||
err, _ := parseURI(uri)
|
||||
assertExpectedError(err, t)
|
||||
}
|
||||
|
||||
// Argument scheme://host:port
|
||||
// i.e : http://localhost:9200
|
||||
// Fail : explicit index is required
|
||||
func TestElasticConnectionBuildFailWithoutIndex(t *testing.T) {
|
||||
uri := "http://localhost:9200"
|
||||
|
||||
err, index := parseURI(uri)
|
||||
|
||||
assertExpectedIndex("", index, t)
|
||||
|
||||
assertExpectedError(err, t)
|
||||
}
|
||||
|
||||
// Argument scheme://host/index_name
|
||||
// i.e : http://localhost/gor
|
||||
func TestElasticConnectionBuildFailWithoutPort(t *testing.T) {
|
||||
uri := "http://localhost/" + expectedIndex
|
||||
|
||||
err, index := parseURI(uri)
|
||||
|
||||
assertNoError(err, t)
|
||||
|
||||
assertExpectedGorIndex(index, t)
|
||||
}
|
||||
|
||||
// Argument scheme://host:port/index_name
|
||||
// i.e : http://localhost:9200/gor
|
||||
func TestElasticLocalConnectionBuild(t *testing.T) {
|
||||
uri := "http://localhost:9200/" + expectedIndex
|
||||
|
||||
err, index := parseURI(uri)
|
||||
|
||||
assertNoError(err, t)
|
||||
|
||||
assertExpectedGorIndex(index, t)
|
||||
}
|
||||
|
||||
// Argument scheme://host:port/index_name
|
||||
// i.e : http://localhost.local:9200/gor or https://localhost.local:9200/gor
|
||||
func TestElasticSimpleLocalWithSchemeConnectionBuild(t *testing.T) {
|
||||
uri := "http://localhost.local:9200/" + expectedIndex
|
||||
|
||||
err, index := parseURI(uri)
|
||||
|
||||
assertNoError(err, t)
|
||||
|
||||
assertExpectedGorIndex(index, t)
|
||||
}
|
||||
|
||||
// Argument scheme://host:port/index_name
|
||||
// i.e : http://localhost.local:9200/gor or https://localhost.local:9200/gor
|
||||
func TestElasticSimpleLocalWithHTTPSConnectionBuild(t *testing.T) {
|
||||
uri := "https://localhost.local:9200/" + expectedIndex
|
||||
|
||||
err, index := parseURI(uri)
|
||||
|
||||
assertNoError(err, t)
|
||||
|
||||
assertExpectedGorIndex(index, t)
|
||||
}
|
||||
|
||||
// Argument scheme://host:port/index_name
|
||||
// i.e : localhost.local:9200/pathtoElastic/gor
|
||||
func TestElasticLongPathConnectionBuild(t *testing.T) {
|
||||
uri := "http://localhost.local:9200/pathtoElastic/" + expectedIndex
|
||||
|
||||
err, index := parseURI(uri)
|
||||
|
||||
assertNoError(err, t)
|
||||
|
||||
assertExpectedGorIndex(index, t)
|
||||
}
|
||||
|
||||
// Argument scheme://host:userinfo@port/index_name
|
||||
// i.e : http://user:password@localhost.local:9200/gor
|
||||
func TestElasticBasicAuthConnectionBuild(t *testing.T) {
|
||||
uri := "http://user:password@localhost.local:9200/" + expectedIndex
|
||||
|
||||
err, index := parseURI(uri)
|
||||
|
||||
assertNoError(err, t)
|
||||
|
||||
assertExpectedGorIndex(index, t)
|
||||
}
|
||||
|
||||
// Argument scheme://host:port/path/index_name
|
||||
// i.e : http://localhost.local:9200/path/gor or https://localhost.local:9200/path/gor
|
||||
func TestElasticComplexPathConnectionBuild(t *testing.T) {
|
||||
uri := "http://localhost.local:9200/path/" + expectedIndex
|
||||
|
||||
err, index := parseURI(uri)
|
||||
|
||||
assertNoError(err, t)
|
||||
|
||||
assertExpectedGorIndex(index, t)
|
||||
}
|
||||
+186
-49
@@ -2,106 +2,243 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Start initialize loop for sending data from inputs to outputs
|
||||
func Start(stop chan int) {
|
||||
if Settings.middleware != "" {
|
||||
middleware := NewMiddleware(Settings.middleware)
|
||||
type emitter struct {
|
||||
sync.WaitGroup
|
||||
quit chan int
|
||||
}
|
||||
|
||||
for _, in := range Plugins.Inputs {
|
||||
// NewEmitter creates and initializes new `emitter` object.
|
||||
func NewEmitter(quit chan int) *emitter {
|
||||
return &emitter{
|
||||
quit: quit,
|
||||
}
|
||||
}
|
||||
|
||||
// Start initialize loop for sending data from inputs to outputs
|
||||
func (e *emitter) Start(plugins *InOutPlugins, middlewareCmd string) {
|
||||
e.Add(1)
|
||||
defer e.Done()
|
||||
|
||||
if middlewareCmd != "" {
|
||||
middleware := NewMiddleware(middlewareCmd)
|
||||
|
||||
for _, in := range plugins.Inputs {
|
||||
middleware.ReadFrom(in)
|
||||
}
|
||||
|
||||
// We going only to read responses, so using same ReadFrom method
|
||||
for _, out := range Plugins.Outputs {
|
||||
// We are 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...)
|
||||
e.Add(1)
|
||||
go func() {
|
||||
defer e.Done()
|
||||
if err := CopyMulty(e.quit, middleware, plugins.Outputs...); err != nil {
|
||||
log.Println("Error during copy: ", err)
|
||||
e.close()
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-e.quit:
|
||||
middleware.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
for _, in := range Plugins.Inputs {
|
||||
go CopyMulty(in, Plugins.Outputs...)
|
||||
for _, in := range plugins.Inputs {
|
||||
e.Add(1)
|
||||
go func(in io.Reader) {
|
||||
defer e.Done()
|
||||
if err := CopyMulty(e.quit, in, plugins.Outputs...); err != nil {
|
||||
log.Println("Error during copy: ", err)
|
||||
e.close()
|
||||
}
|
||||
}(in)
|
||||
}
|
||||
|
||||
for _, out := range plugins.Outputs {
|
||||
if r, ok := out.(io.Reader); ok {
|
||||
e.Add(1)
|
||||
go func(r io.Reader) {
|
||||
defer e.Done()
|
||||
if err := CopyMulty(e.quit, r, plugins.Outputs...); err != nil {
|
||||
log.Println("Error during copy: ", err)
|
||||
e.close()
|
||||
}
|
||||
}(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
case <-e.quit:
|
||||
finalize(plugins)
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *emitter) close() {
|
||||
select {
|
||||
case <-e.quit:
|
||||
default:
|
||||
close(e.quit)
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes all the goroutine and waits for it to finish.
|
||||
func (e *emitter) Close() {
|
||||
e.close()
|
||||
e.Wait()
|
||||
}
|
||||
|
||||
// CopyMulty copies from 1 reader to multiple writers
|
||||
func CopyMulty(src io.Reader, writers ...io.Writer) (err error) {
|
||||
buf := make([]byte, 5*1024*1024)
|
||||
func CopyMulty(stop chan int, src io.Reader, writers ...io.Writer) error {
|
||||
buf := make([]byte, Settings.CopyBufferSize)
|
||||
wIndex := 0
|
||||
modifier := NewHTTPModifier(&Settings.modifierConfig)
|
||||
filteredRequests := make(map[string]time.Time)
|
||||
filteredRequestsLastCleanTime := time.Now()
|
||||
|
||||
i := 0
|
||||
for {
|
||||
nr, er := src.Read(buf)
|
||||
var nr int
|
||||
nr, err := src.Read(buf)
|
||||
|
||||
select {
|
||||
case <-stop:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
if err == io.EOF || err == ErrorStopped {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_maxN := nr
|
||||
if nr > 500 {
|
||||
_maxN = 500
|
||||
}
|
||||
if nr > 0 && len(buf) > nr {
|
||||
payload := buf[:nr]
|
||||
meta := payloadMeta(payload)
|
||||
if len(meta) < 3 {
|
||||
if Settings.Debug {
|
||||
Debug("[EMITTER] Found malformed record", string(payload[0:_maxN]), nr, "from:", src)
|
||||
}
|
||||
continue
|
||||
}
|
||||
requestID := string(meta[1])
|
||||
|
||||
_maxN := nr
|
||||
if nr > 500 {
|
||||
_maxN = 500
|
||||
if nr >= 5*1024*1024 {
|
||||
log.Println("INFO: Large packet... We received ", len(payload), " bytes from ", src)
|
||||
}
|
||||
|
||||
if Settings.debug {
|
||||
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 != nil {
|
||||
if 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 modifier tells to skip request
|
||||
if len(body) == 0 {
|
||||
filteredRequests[requestID] = time.Now()
|
||||
continue
|
||||
}
|
||||
|
||||
if originalBodyLen != len(body) {
|
||||
payload = append(payload[:headSize], body...)
|
||||
}
|
||||
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.Debug {
|
||||
Debug("[EMITTER] Rewritten input:", len(payload), "First 500 bytes:", string(payload[0:_maxN]))
|
||||
}
|
||||
} else {
|
||||
if _, ok := filteredRequests[requestID]; ok {
|
||||
delete(filteredRequests, requestID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if Settings.splitOutput {
|
||||
// Simple round robin
|
||||
writers[wIndex].Write(payload)
|
||||
if Settings.PrettifyHTTP {
|
||||
payload = prettifyHTTP(payload)
|
||||
if len(payload) == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
wIndex++
|
||||
if Settings.SplitOutput {
|
||||
if Settings.RecognizeTCPSessions {
|
||||
if !PRO {
|
||||
log.Fatal("Detailed TCP sessions work only with PRO license")
|
||||
}
|
||||
hasher := fnv.New32a()
|
||||
// First 20 bytes contain tcp session
|
||||
id := payloadID(payload)
|
||||
hasher.Write(id[:20])
|
||||
|
||||
if wIndex >= len(writers) {
|
||||
wIndex = 0
|
||||
wIndex = int(hasher.Sum32()) % len(writers)
|
||||
writers[wIndex].Write(payload)
|
||||
} else {
|
||||
// Simple round robin
|
||||
if _, err := writers[wIndex].Write(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wIndex++
|
||||
|
||||
if wIndex >= len(writers) {
|
||||
wIndex = 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, dst := range writers {
|
||||
dst.Write(payload)
|
||||
if _, err := dst.Write(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if nr > 0 {
|
||||
log.Println("WARN: Packet", nr, "bytes is too large to process. Consider increasing --copy-buffer-size")
|
||||
}
|
||||
|
||||
// Run GC on each 1000 request
|
||||
if i%1000 == 0 {
|
||||
// Clean up filtered requests for which we didn't get a response to filter
|
||||
now := time.Now()
|
||||
if now.Sub(filteredRequestsLastCleanTime) > 60*time.Second {
|
||||
for k, v := range filteredRequests {
|
||||
if now.Sub(v) > 60*time.Second {
|
||||
delete(filteredRequests, k)
|
||||
}
|
||||
}
|
||||
filteredRequestsLastCleanTime = time.Now()
|
||||
}
|
||||
}
|
||||
if er == io.EOF {
|
||||
break
|
||||
}
|
||||
if er != nil {
|
||||
err = er
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
+199
-15
@@ -1,12 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
PRO = true
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestEmitter(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
@@ -16,10 +25,102 @@ func TestEmitter(t *testing.T) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
func TestEmitterFiltered(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
input.skipHeader = true
|
||||
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
methods := HTTPMethods{[]byte("GET")}
|
||||
Settings.modifierConfig = HTTPModifierConfig{methods: methods}
|
||||
|
||||
emitter := &emitter{quit: quit}
|
||||
go emitter.Start(plugins, "")
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
id := uuid()
|
||||
reqh := payloadHeader(RequestPayload, id, time.Now().UnixNano(), -1)
|
||||
reqb := 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, id, time.Now().UnixNano()+1, 1)
|
||||
respb := append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...)
|
||||
|
||||
input.EmitBytes(reqb)
|
||||
input.EmitBytes(respb)
|
||||
|
||||
id = uuid()
|
||||
reqh = payloadHeader(RequestPayload, id, time.Now().UnixNano(), -1)
|
||||
reqb = append(reqh, []byte("POST / 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, id, time.Now().UnixNano()+1, 1)
|
||||
respb = append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...)
|
||||
|
||||
input.EmitBytes(reqb)
|
||||
input.EmitBytes(respb)
|
||||
|
||||
wg.Wait()
|
||||
emitter.Close()
|
||||
|
||||
Settings.modifierConfig = HTTPModifierConfig{}
|
||||
}
|
||||
|
||||
func TestEmitterSplitRoundRobin(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 := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output1, output2},
|
||||
}
|
||||
|
||||
Settings.SplitOutput = true
|
||||
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
wg.Add(1)
|
||||
@@ -28,7 +129,13 @@ func TestEmitter(t *testing.T) {
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
|
||||
if counter1 == 0 || counter2 == 0 || counter1 != counter2 {
|
||||
t.Errorf("Round robin should split traffic equally: %d vs %d", counter1, counter2)
|
||||
}
|
||||
|
||||
Settings.SplitOutput = false
|
||||
}
|
||||
|
||||
func TestEmitterRoundRobin(t *testing.T) {
|
||||
@@ -49,12 +156,16 @@ func TestEmitterRoundRobin(t *testing.T) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output1, output2}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output1, output2},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output1, output2)
|
||||
|
||||
Settings.splitOutput = true
|
||||
Settings.SplitOutput = true
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
wg.Add(1)
|
||||
@@ -62,14 +173,83 @@ func TestEmitterRoundRobin(t *testing.T) {
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
|
||||
if counter1 == 0 || counter2 == 0 {
|
||||
t.Errorf("Round robin should split traffic equally: %d vs %d", counter1, counter2)
|
||||
}
|
||||
|
||||
Settings.splitOutput = false
|
||||
Settings.SplitOutput = false
|
||||
}
|
||||
|
||||
func TestEmitterSplitSession(t *testing.T) {
|
||||
wg1 := new(sync.WaitGroup)
|
||||
wg2 := new(sync.WaitGroup)
|
||||
wg1.Add(1000)
|
||||
wg2.Add(1000)
|
||||
|
||||
// Base uuids, only 1 letter changed
|
||||
uuid1 := []byte("1234567890123456789a0000")
|
||||
uuid2 := []byte("1234567890123456789d0000")
|
||||
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
input.skipHeader = true
|
||||
|
||||
var counter1, counter2 int32
|
||||
|
||||
output1 := NewTestOutput(func(data []byte) {
|
||||
atomic.AddInt32(&counter1, 1)
|
||||
if !bytes.Equal(uuid1[:20], payloadID(data)[:20]) {
|
||||
t.Errorf("All tcp sessions should have same id")
|
||||
}
|
||||
|
||||
wg1.Done()
|
||||
})
|
||||
|
||||
output2 := NewTestOutput(func(data []byte) {
|
||||
atomic.AddInt32(&counter2, 1)
|
||||
if !bytes.Equal(uuid2[:20], payloadID(data)[:20]) {
|
||||
t.Errorf("All tcp sessions should have same id")
|
||||
}
|
||||
|
||||
wg2.Done()
|
||||
})
|
||||
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output1, output2},
|
||||
}
|
||||
|
||||
Settings.SplitOutput = true
|
||||
Settings.RecognizeTCPSessions = true
|
||||
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
// Keep session but randomize ACK
|
||||
copy(uuid1[20:], randByte(4))
|
||||
input.EmitBytes([]byte("1 " + string(uuid1) + " 1\n" + "GET / HTTP/1.1\r\n\r\n"))
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
// Keep session but randomize ACK
|
||||
copy(uuid2[20:], randByte(4))
|
||||
input.EmitBytes([]byte("1 " + string(uuid2) + " 1\n" + "GET / HTTP/1.1\r\n\r\n"))
|
||||
}
|
||||
|
||||
wg1.Wait()
|
||||
wg2.Wait()
|
||||
|
||||
if counter1 != 1000 || counter2 != 1000 {
|
||||
t.Errorf("Round robin should split traffic equally: %d vs %d", counter1, counter2)
|
||||
}
|
||||
|
||||
Settings.SplitOutput = false
|
||||
Settings.RecognizeTCPSessions = false
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
func BenchmarkEmitter(b *testing.B) {
|
||||
@@ -82,10 +262,14 @@ func BenchmarkEmitter(b *testing.B) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
@@ -95,5 +279,5 @@ func BenchmarkEmitter(b *testing.B) {
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
(ns echo.core
|
||||
(:gen-class)
|
||||
(:require [clojure.string :as cs]
|
||||
[clojure.java.io :as io])
|
||||
(:import org.apache.commons.codec.binary.Hex
|
||||
java.io.BufferedReader
|
||||
java.io.IOException
|
||||
java.io.InputStreamReader))
|
||||
|
||||
|
||||
(defn transform-http-msg
|
||||
"Function that transforms/filters the incoming HTTP messages."
|
||||
[headers body]
|
||||
;; do actual transformations here
|
||||
[headers body])
|
||||
|
||||
|
||||
(defn decode-hex-string
|
||||
"Decode an Hex-encoded string."
|
||||
[s]
|
||||
(String. (Hex/decodeHex (.toCharArray s))))
|
||||
|
||||
|
||||
(defn encode-hex-string
|
||||
"Encode a string to a hex-encoded string."
|
||||
[^String s]
|
||||
(String. (Hex/encodeHex (.getBytes s))))
|
||||
|
||||
|
||||
(defn -main
|
||||
[& args]
|
||||
(let [br (BufferedReader. (InputStreamReader. System/in))]
|
||||
(try
|
||||
(loop [hex-line (.readLine br)]
|
||||
(let [decoded-req (decode-hex-string hex-line)
|
||||
|
||||
;; empty line separates headers from body
|
||||
http-request (partition-by empty? (cs/split-lines decoded-req))
|
||||
headers (first http-request)
|
||||
|
||||
;; HTTP messages can contain no body:
|
||||
body (when (= 3 (count http-request)) (last http-request))
|
||||
[new-headers new-body] (transform-http-msg headers body)]
|
||||
|
||||
(println (encode-hex-string (str (cs/join "\n" headers)
|
||||
(when body
|
||||
(str "\n\n"
|
||||
(cs/join "\n" body)))))))
|
||||
(when-let [line (.readLine br)]
|
||||
(recur line)))
|
||||
(catch IOException e nil))))
|
||||
|
||||
|
||||
@@ -2,10 +2,25 @@ import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
|
||||
public class echo {
|
||||
public static void main(String[] args) {
|
||||
|
||||
class Echo {
|
||||
public static String decodeHexString(String s) throws DecoderException {
|
||||
return new String(Hex.decodeHex(s.toCharArray()));
|
||||
}
|
||||
|
||||
public static String encodeHexString(String s) {
|
||||
return new String(Hex.encodeHex(s.getBytes()));
|
||||
}
|
||||
|
||||
public static String transformHTTPMessage(String req) {
|
||||
// do actual transformations here
|
||||
return req;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws DecoderException {
|
||||
if(args != null){
|
||||
for(String arg : args){
|
||||
System.out.println(arg);
|
||||
@@ -14,17 +29,20 @@ public class echo {
|
||||
}
|
||||
|
||||
BufferedReader stdin = new BufferedReader(new InputStreamReader(
|
||||
System.in));
|
||||
System.in));
|
||||
String line = null;
|
||||
|
||||
try {
|
||||
while ((line = stdin.readLine()) != null) {
|
||||
String decodedLine = decodeHexString(line);
|
||||
|
||||
System.out.println(line);
|
||||
String transformedLine = transformHTTPMessage(decodedLine);
|
||||
|
||||
String encodedLine = encodeHexString(transformedLine);
|
||||
System.out.println(encodedLine);
|
||||
|
||||
}
|
||||
} catch (IOException e) {
|
||||
IOUtils.closeQuietly(stdin);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
const readline = require("readline");
|
||||
const StringDecoder = require("string_decoder").StringDecoder
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin
|
||||
});
|
||||
|
||||
var ignoreIds = new Set();
|
||||
var ignoreAddresses = "/api";
|
||||
const decoder = new StringDecoder("utf8");
|
||||
|
||||
function convertHexString(hex) {
|
||||
var bytes = [];
|
||||
for (var i = 0; i < hex.length - 1; i += 2) {
|
||||
bytes.push(parseInt(hex.substr(i, 2), 16));
|
||||
}
|
||||
return decoder.write(Buffer.from(bytes));
|
||||
}
|
||||
|
||||
function log(output) {
|
||||
console.error("===================");
|
||||
console.error(output);
|
||||
}
|
||||
|
||||
function shouldOutputLine(request) {
|
||||
const components = request.split("\n");
|
||||
const header = components[0].split(" ");
|
||||
const type = parseInt(header[0]);
|
||||
const tag = header[1];
|
||||
|
||||
if (type === 3) {
|
||||
return true;
|
||||
}
|
||||
if (type === 1) {
|
||||
// Check if it's oauth
|
||||
const endpoint = components[1].split(" ")[1];
|
||||
if (!endpoint.startsWith(ignoreAddresses)) {
|
||||
ignoreIds.add(tag);
|
||||
return false;
|
||||
}
|
||||
} else if (type === 2) {
|
||||
if (ignoreIds.has(tag)) {
|
||||
ignoreIds.delete(tag);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
rl.on("line", (input) => {
|
||||
const str = convertHexString(input);
|
||||
console.log(input);
|
||||
if (shouldOutputLine(str)) {
|
||||
log(str);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import fileinput
|
||||
import binascii
|
||||
|
||||
# Used to find end of the Headers section
|
||||
EMPTY_LINE = b'\r\n\r\n'
|
||||
|
||||
|
||||
def log(msg):
|
||||
"""
|
||||
Logging to STDERR as STDOUT and STDIN used for data transfer
|
||||
@type msg: str or byte string
|
||||
@param msg: Message to log to STDERR
|
||||
"""
|
||||
try:
|
||||
msg = str(msg) + '\n'
|
||||
except:
|
||||
pass
|
||||
sys.stderr.write(msg)
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
def find_end_of_headers(byte_data):
|
||||
"""
|
||||
Finds where the header portion ends and the content portion begins.
|
||||
@type byte_data: str or byte string
|
||||
@param byte_data: Hex decoded req or resp string
|
||||
"""
|
||||
return byte_data.index(EMPTY_LINE) + 4
|
||||
|
||||
|
||||
def process_stdin():
|
||||
"""
|
||||
Process STDIN and output to STDOUT
|
||||
"""
|
||||
for raw_line in fileinput.input():
|
||||
|
||||
line = raw_line.rstrip()
|
||||
|
||||
# Decode base64 encoded line
|
||||
decoded = bytes.fromhex(line)
|
||||
|
||||
# Split into metadata and payload, the payload is headers + body
|
||||
(raw_metadata, payload) = decoded.split(b'\n', 1)
|
||||
|
||||
# Split into headers and payload
|
||||
headers_pos = find_end_of_headers(payload)
|
||||
raw_headers = payload[:headers_pos]
|
||||
raw_content = payload[headers_pos:]
|
||||
|
||||
log('===================================')
|
||||
request_type_id = int(raw_metadata.split(b' ')[0])
|
||||
log('Request type: {}'.format({
|
||||
1: 'Request',
|
||||
2: 'Original Response',
|
||||
3: 'Replayed Response'
|
||||
}[request_type_id]))
|
||||
log('===================================')
|
||||
|
||||
log('Original data:')
|
||||
log(line)
|
||||
|
||||
log('Decoded request:')
|
||||
log(decoded)
|
||||
|
||||
encoded = binascii.hexlify(raw_metadata + b'\n' + raw_headers + raw_content).decode('ascii')
|
||||
log('Encoded data:')
|
||||
log(encoded)
|
||||
|
||||
sys.stdout.write(encoded + '\n')
|
||||
|
||||
if __name__ == '__main__':
|
||||
process_stdin()
|
||||
+17
-10
@@ -1,15 +1,22 @@
|
||||
#!/usr/bin/env ruby
|
||||
# encoding: utf-8
|
||||
while data = STDIN.gets
|
||||
while data = STDIN.gets # continuously read line from STDIN
|
||||
next unless data
|
||||
data = data.chomp
|
||||
|
||||
decoded = [data].pack("H*")
|
||||
encoded = decoded.unpack("H*").first
|
||||
|
||||
data = data.chomp # remove end of line symbol
|
||||
|
||||
decoded = [data].pack("H*") # decode base64 encoded request
|
||||
|
||||
# decoded 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
|
||||
|
||||
STDERR.puts "[DEBUG][MIDDLEWARE] Original data: #{data}"
|
||||
STDERR.puts "[DEBUG][MIDDLEWARE] Decoded request: #{decoded}"
|
||||
STDERR.puts "[DEBUG][MIDDLEWARE] Encoded data: #{encoded}"
|
||||
end
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
#
|
||||
# `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
|
||||
|
||||
@@ -24,8 +24,9 @@ import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/buger/gor/proto"
|
||||
"os"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
// requestID -> originalToken
|
||||
@@ -45,7 +46,7 @@ func main() {
|
||||
buf := make([]byte, len(encoded)/2)
|
||||
hex.Decode(buf, encoded)
|
||||
|
||||
go process(buf)
|
||||
process(buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
module github.com/buger/goreplay
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/Shopify/sarama v1.26.4
|
||||
github.com/araddon/gou v0.0.0-20190110011759-c797efecbb61 // indirect
|
||||
github.com/aws/aws-sdk-go v1.32.7
|
||||
github.com/bitly/go-hostpool v0.1.0 // indirect
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
|
||||
github.com/filecoin-project/go-fil-markets v0.3.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/google/gopacket v1.1.17
|
||||
github.com/klauspost/compress v1.10.10 // indirect
|
||||
github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/spf13/viper v1.7.0
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
|
||||
)
|
||||
@@ -0,0 +1,839 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3 h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/firestore v1.1.0 h1:9x7Bx0A9R5/M9jibeJeZWqjeVEIxYW9fZYqB9a70/bY=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Kubuxu/go-os-helper v0.0.1/go.mod h1:N8B+I7vPCT80IcP58r50u4+gEEcsZETFUpAzWW2ep1Y=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/Shopify/sarama v1.26.4 h1:+17TxUq/PJEAfZAll0T7XJjSgQWCpaQSoki/x5yN8o8=
|
||||
github.com/Shopify/sarama v1.26.4/go.mod h1:NbSGBSSndYaIhRcBtY9V0U7AyH+x71bG668AuWys/yU=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/Stebalien/go-bitfield v0.0.1/go.mod h1:GNjFpasyUVkHMsfEOk8EFLJ9syQ6SI+XWrX9Wf2XH0s=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/araddon/gou v0.0.0-20190110011759-c797efecbb61 h1:Xz25cuW4REGC5W5UtpMU3QItMIImag615HiQcRbxqKQ=
|
||||
github.com/araddon/gou v0.0.0-20190110011759-c797efecbb61/go.mod h1:ikc1XA58M+Rx7SEbf0bLJCfBkwayZ8T5jBo5FXK8Uz8=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aws/aws-sdk-go v1.32.7 h1:H4VgdCSF1cHw0VD8zGc98T1bGdACoLkh/vK2L6wgOUU=
|
||||
github.com/aws/aws-sdk-go v1.32.7/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bitly/go-hostpool v0.1.0 h1:XKmsF6k5el6xHG3WPJ8U0Ku/ye7njX7W81Ng7O2ioR0=
|
||||
github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c h1:+0HFd5KSZ/mm3JmhmrDukiId5iR6w4+BdFtfSy4yWIc=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/btcsuite/btcd v0.0.0-20190213025234-306aecffea32/go.mod h1:DrZx5ec/dmnfpw9KyYoQyYo7d0KEvTkk/5M/vbZjAr8=
|
||||
github.com/btcsuite/btcd v0.0.0-20190523000118-16327141da8c/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
|
||||
github.com/btcsuite/btcd v0.0.0-20190605094302-a0d1e3e36d50/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
|
||||
github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE=
|
||||
github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidlazar/go-crypto v0.0.0-20170701192655-dcfb0a7ac018/go.mod h1:rQYf4tfk5sSwFsnDg3qYaBxSjsD9S8+59vW0dKUgme4=
|
||||
github.com/detailyang/go-fallocate v0.0.0-20180908115635-432fa640bd2e/go.mod h1:3ZQK6DMPSz/QZ73jlWxBtUhNA8xZx7LzUFSq/OfP8vk=
|
||||
github.com/dgraph-io/badger v1.5.5-0.20190226225317-8115aed38f8f/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ=
|
||||
github.com/dgraph-io/badger v1.6.0-rc1/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-farm v0.0.0-20190104051053-3adb47b1fb0f/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q=
|
||||
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/elastic/go-sysinfo v1.3.0/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0=
|
||||
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.8.0/go.mod h1:3l45GVGkyrnYNl9HoIjnp2NnNWvh6hLAqD8yTfGjnw8=
|
||||
github.com/filecoin-project/filecoin-ffi v0.0.0-20200326153646-e899cc1dd072/go.mod h1:PtH9YP0rURHUKHrKeEBeWg/BqIBMQOz8wtlXlVGREBE=
|
||||
github.com/filecoin-project/go-address v0.0.0-20200107215422-da8eea2842b5/go.mod h1:SAOwJoakQ8EPjwNIsiakIQKsoKdkcbx8U3IapgCg9R0=
|
||||
github.com/filecoin-project/go-address v0.0.2-0.20200218010043-eb9bb40ed5be/go.mod h1:SAOwJoakQ8EPjwNIsiakIQKsoKdkcbx8U3IapgCg9R0=
|
||||
github.com/filecoin-project/go-amt-ipld/v2 v2.0.1-0.20200131012142-05d80eeccc5e/go.mod h1:boRtQhzmxNocrMxOXo1NYn4oUc1NGvR8tEa79wApNXg=
|
||||
github.com/filecoin-project/go-amt-ipld/v2 v2.0.1-0.20200424220931-6263827e49f2/go.mod h1:boRtQhzmxNocrMxOXo1NYn4oUc1NGvR8tEa79wApNXg=
|
||||
github.com/filecoin-project/go-bitfield v0.0.0-20200416002808-b3ee67ec9060/go.mod h1:iodsLxOFZnqKtjj2zkgqzoGNrv6vUqj69AT/J8DKXEw=
|
||||
github.com/filecoin-project/go-bitfield v0.0.1/go.mod h1:Ry9/iUlWSyjPUzlAvdnfy4Gtvrq4kWmWDztCU1yEgJY=
|
||||
github.com/filecoin-project/go-bitfield v0.0.2-0.20200518150651-562fdb554b6e/go.mod h1:Ry9/iUlWSyjPUzlAvdnfy4Gtvrq4kWmWDztCU1yEgJY=
|
||||
github.com/filecoin-project/go-cbor-util v0.0.0-20191219014500-08c40a1e63a2/go.mod h1:pqTiPHobNkOVM5thSRsHYjyQfq7O5QSCMhvuu9JoDlg=
|
||||
github.com/filecoin-project/go-crypto v0.0.0-20191218222705-effae4ea9f03/go.mod h1:+viYnvGtUTgJRdy6oaeF4MTFKAfatX071MPDPBL11EQ=
|
||||
github.com/filecoin-project/go-data-transfer v0.3.0/go.mod h1:cONglGP4s/d+IUQw5mWZrQK+FQATQxr3AXzi4dRh0l4=
|
||||
github.com/filecoin-project/go-fil-commcid v0.0.0-20200208005934-2b8bd03caca5/go.mod h1:JbkIgFF/Z9BDlvrJO1FuKkaWsH673/UdFaiVS6uIHlA=
|
||||
github.com/filecoin-project/go-fil-markets v0.3.2 h1:fvNgdTTIVtckBu61wxbKYSMJzedoFFIKYJagiCDFCiM=
|
||||
github.com/filecoin-project/go-fil-markets v0.3.2/go.mod h1:e/IofcotbwH7ftgeK+TjjdjFsrCDWrh5vvnr7k1OSH8=
|
||||
github.com/filecoin-project/go-padreader v0.0.0-20200210211231-548257017ca6/go.mod h1:0HgYnrkeSU4lu1p+LEOeDpFsNBssa0OGGriWdA4hvaE=
|
||||
github.com/filecoin-project/go-paramfetch v0.0.1/go.mod h1:fZzmf4tftbwf9S37XRifoJlz7nCjRdIrMGLR07dKLCc=
|
||||
github.com/filecoin-project/go-statemachine v0.0.0-20200619205156-c7bf525c06ef/go.mod h1:FGwQgZAt2Gh5mjlwJUlVB62JeYdo+if0xWxSEfBD9ig=
|
||||
github.com/filecoin-project/go-statestore v0.1.0/go.mod h1:LFc9hD+fRxPqiHiaqUEZOinUJB4WARkRfNl10O7kTnI=
|
||||
github.com/filecoin-project/go-storedcounter v0.0.0-20200421200003-1c99c62e8a5b/go.mod h1:Q0GQOBtKf1oE10eSXSlhN45kDBdGvEcVOqMiffqX+N8=
|
||||
github.com/filecoin-project/sector-storage v0.0.0-20200615154852-728a47ab99d6/go.mod h1:M59QnAeA/oV+Z8oHFLoNpGMv0LZ8Rll+vHVXX7GirPM=
|
||||
github.com/filecoin-project/specs-actors v0.0.0-20200210130641-2d1fbd8672cf/go.mod h1:xtDZUB6pe4Pksa/bAJbJ693OilaC5Wbot9jMhLm3cZA=
|
||||
github.com/filecoin-project/specs-actors v0.0.0-20200226200336-94c9b92b2775/go.mod h1:0HAWYrvajFHDgRaKbF0rl+IybVLZL5z4gQ8koCMPhoU=
|
||||
github.com/filecoin-project/specs-actors v0.3.0/go.mod h1:nQYnFbQ7Y0bHZyq6HDEuVlCPR+U3z5Q3wMOQ+2aiV+Y=
|
||||
github.com/filecoin-project/specs-actors v0.6.0/go.mod h1:dRdy3cURykh2R8O/DKqy8olScl70rmIS7GrB4hB1IDY=
|
||||
github.com/filecoin-project/specs-storage v0.1.0/go.mod h1:Pr5ntAaxsh+sLG/LYiL4tKzvA83Vk5vLODYhfNwOg7k=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/frankban/quicktest v1.7.2 h1:2QxQoC1TS09S7fhCPsrvqYdvP1H5M1P1ih5ABm3BTYk=
|
||||
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbBY=
|
||||
github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU=
|
||||
github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmvhST0bie/0lS48=
|
||||
github.com/hannahhoward/cbor-gen-for v0.0.0-20191218204337-9ab7b1bcc099/go.mod h1:WVPCl0HO/0RAL5+vBH2GMxBomlxBF70MAS78+Lu1//k=
|
||||
github.com/hannahhoward/go-pubsub v0.0.0-20200423002714-8d62886cc36e/go.mod h1:I8h3MITA53gN9OnWGCgaMa0JWVRdXthWw4M3CPM54OY=
|
||||
github.com/hashicorp/consul/api v1.1.0 h1:BNQPM9ytxj6jbjjdRPioQ94T6YXriSopn0i8COv6SRA=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc=
|
||||
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/ipfs/bbloom v0.0.1/go.mod h1:oqo8CVWsJFMOZqTglBG4wydCE4IQA/G2/SEofB0rjUI=
|
||||
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
|
||||
github.com/ipfs/go-bitswap v0.1.0/go.mod h1:FFJEf18E9izuCqUtHxbWEvq+reg7o4CW5wSAE1wsxj0=
|
||||
github.com/ipfs/go-bitswap v0.1.2/go.mod h1:qxSWS4NXGs7jQ6zQvoPY3+NmOfHHG47mhkiLzBpJQIs=
|
||||
github.com/ipfs/go-bitswap v0.1.8/go.mod h1:TOWoxllhccevbWFUR2N7B1MTSVVge1s6XSMiCSA4MzM=
|
||||
github.com/ipfs/go-block-format v0.0.1/go.mod h1:DK/YYcsSUIVAFNwo/KZCdIIbpN0ROH/baNLgayt4pFc=
|
||||
github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY=
|
||||
github.com/ipfs/go-blockservice v0.1.0/go.mod h1:hzmMScl1kXHg3M2BjTymbVPjv627N7sYcvYaKbop39M=
|
||||
github.com/ipfs/go-blockservice v0.1.3/go.mod h1:OTZhFpkgY48kNzbgyvcexW9cHrpjBYIjSR0KoDOFOLU=
|
||||
github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
|
||||
github.com/ipfs/go-cid v0.0.2/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
|
||||
github.com/ipfs/go-cid v0.0.3/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
|
||||
github.com/ipfs/go-cid v0.0.4-0.20191112011718-79e75dffeb10/go.mod h1:/BYOuUoxkE+0f6tGzlzMvycuN+5l35VOR4Bpg2sCmds=
|
||||
github.com/ipfs/go-cid v0.0.4/go.mod h1:4LLaPOQwmk5z9LBgQnpkivrx8BJjUyGwTXCd5Xfj6+M=
|
||||
github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog=
|
||||
github.com/ipfs/go-datastore v0.0.1/go.mod h1:d4KVXhMt913cLBEI/PXAy6ko+W7e9AhyAKBGh803qeE=
|
||||
github.com/ipfs/go-datastore v0.0.5/go.mod h1:d4KVXhMt913cLBEI/PXAy6ko+W7e9AhyAKBGh803qeE=
|
||||
github.com/ipfs/go-datastore v0.1.0/go.mod h1:d4KVXhMt913cLBEI/PXAy6ko+W7e9AhyAKBGh803qeE=
|
||||
github.com/ipfs/go-datastore v0.1.1/go.mod h1:w38XXW9kVFNp57Zj5knbKWM2T+KOZCGDRVNdgPHtbHw=
|
||||
github.com/ipfs/go-datastore v0.3.1/go.mod h1:w38XXW9kVFNp57Zj5knbKWM2T+KOZCGDRVNdgPHtbHw=
|
||||
github.com/ipfs/go-datastore v0.4.0/go.mod h1:SX/xMIKoCszPqp+z9JhPYCmoOoXTvaa13XEbGtsFUhA=
|
||||
github.com/ipfs/go-datastore v0.4.1/go.mod h1:SX/xMIKoCszPqp+z9JhPYCmoOoXTvaa13XEbGtsFUhA=
|
||||
github.com/ipfs/go-datastore v0.4.4/go.mod h1:SX/xMIKoCszPqp+z9JhPYCmoOoXTvaa13XEbGtsFUhA=
|
||||
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
|
||||
github.com/ipfs/go-ds-badger v0.0.2/go.mod h1:Y3QpeSFWQf6MopLTiZD+VT6IC1yZqaGmjvRcKeSGij8=
|
||||
github.com/ipfs/go-ds-badger v0.0.5/go.mod h1:g5AuuCGmr7efyzQhLL8MzwqcauPojGPUaHzfGTzuE3s=
|
||||
github.com/ipfs/go-ds-badger v0.2.1/go.mod h1:Tx7l3aTph3FMFrRS838dcSJh+jjA7cX9DrGVwx/NOwE=
|
||||
github.com/ipfs/go-ds-leveldb v0.0.1/go.mod h1:feO8V3kubwsEF22n0YRQCffeb79OOYIykR4L04tMOYc=
|
||||
github.com/ipfs/go-ds-leveldb v0.4.1/go.mod h1:jpbku/YqBSsBc1qgME8BkWS4AxzF2cEu1Ii2r79Hh9s=
|
||||
github.com/ipfs/go-graphsync v0.0.6-0.20200504202014-9d5f2c26a103/go.mod h1:jMXfqIEDFukLPZHqDPp8tJMbHO9Rmeb9CEGevngQbmE=
|
||||
github.com/ipfs/go-hamt-ipld v0.0.15-0.20200131012125-dd88a59d3f2e/go.mod h1:9aQJu/i/TaRDW6jqB5U217dLIDopn50wxLdHXM2CTfE=
|
||||
github.com/ipfs/go-ipfs-blockstore v0.0.1/go.mod h1:d3WClOmRQKFnJ0Jz/jj/zmksX0ma1gROTlovZKBmN08=
|
||||
github.com/ipfs/go-ipfs-blockstore v0.1.0/go.mod h1:5aD0AvHPi7mZc6Ci1WCAhiBQu2IsfTduLl+422H6Rqw=
|
||||
github.com/ipfs/go-ipfs-blockstore v0.1.4/go.mod h1:Jxm3XMVjh6R17WvxFEiyKBLUGr86HgIYJW/D/MwqeYQ=
|
||||
github.com/ipfs/go-ipfs-blockstore v1.0.0/go.mod h1:knLVdhVU9L7CC4T+T4nvGdeUIPAXlnd9zmXfp+9MIjU=
|
||||
github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk=
|
||||
github.com/ipfs/go-ipfs-chunker v0.0.1/go.mod h1:tWewYK0we3+rMbOh7pPFGDyypCtvGcBFymgY4rSDLAw=
|
||||
github.com/ipfs/go-ipfs-chunker v0.0.5/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8=
|
||||
github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
|
||||
github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
|
||||
github.com/ipfs/go-ipfs-ds-help v0.0.1/go.mod h1:gtP9xRaZXqIQRh1HRpp595KbBEdgqWFxefeVKOV8sxo=
|
||||
github.com/ipfs/go-ipfs-ds-help v0.1.1/go.mod h1:SbBafGJuGsPI/QL3j9Fc5YPLeAu+SzOkI0gFwAg+mOs=
|
||||
github.com/ipfs/go-ipfs-ds-help v1.0.0/go.mod h1:ujAbkeIgkKAWtxxNkoZHWLCyk5JpPoKnGyCcsoF6ueE=
|
||||
github.com/ipfs/go-ipfs-exchange-interface v0.0.1/go.mod h1:c8MwfHjtQjPoDyiy9cFquVtVHkO9b9Ob3FG91qJnWCM=
|
||||
github.com/ipfs/go-ipfs-exchange-offline v0.0.1/go.mod h1:WhHSFCVYX36H/anEKQboAzpUws3x7UeEGkzQc3iNkM0=
|
||||
github.com/ipfs/go-ipfs-files v0.0.3/go.mod h1:INEFm0LL2LWXBhNJ2PMIIb2w45hpXgPjNoE7yA8Y1d4=
|
||||
github.com/ipfs/go-ipfs-files v0.0.7/go.mod h1:wiN/jSG8FKyk7N0WyctKSvq3ljIa2NNTiZB55kpTdOs=
|
||||
github.com/ipfs/go-ipfs-files v0.0.8/go.mod h1:wiN/jSG8FKyk7N0WyctKSvq3ljIa2NNTiZB55kpTdOs=
|
||||
github.com/ipfs/go-ipfs-posinfo v0.0.1/go.mod h1:SwyeVP+jCwiDu0C313l/8jg6ZxM0qqtlt2a0vILTc1A=
|
||||
github.com/ipfs/go-ipfs-pq v0.0.1/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY=
|
||||
github.com/ipfs/go-ipfs-pq v0.0.2/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY=
|
||||
github.com/ipfs/go-ipfs-routing v0.1.0/go.mod h1:hYoUkJLyAUKhF58tysKpids8RNDPO42BVMgK5dNsoqY=
|
||||
github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc=
|
||||
github.com/ipfs/go-ipld-cbor v0.0.2/go.mod h1:wTBtrQZA3SoFKMVkp6cn6HMRteIB1VsmHA0AQFOn7Nc=
|
||||
github.com/ipfs/go-ipld-cbor v0.0.3/go.mod h1:wTBtrQZA3SoFKMVkp6cn6HMRteIB1VsmHA0AQFOn7Nc=
|
||||
github.com/ipfs/go-ipld-cbor v0.0.4/go.mod h1:BkCduEx3XBCO6t2Sfo5BaHzuok7hbhdMm9Oh8B2Ftq4=
|
||||
github.com/ipfs/go-ipld-format v0.0.1/go.mod h1:kyJtbkDALmFHv3QR6et67i35QzO3S0dCDnkOJhcZkms=
|
||||
github.com/ipfs/go-ipld-format v0.0.2/go.mod h1:4B6+FM2u9OJ9zCV+kSbgFAZlOrv1Hqbf0INGQgiKf9k=
|
||||
github.com/ipfs/go-ipld-format v0.2.0/go.mod h1:3l3C1uKoadTPbeNfrDi+xMInYKlx2Cvg1BuydPSdzQs=
|
||||
github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM=
|
||||
github.com/ipfs/go-log v1.0.0/go.mod h1:JO7RzlMK6rA+CIxFMLOuB6Wf5b81GDiKElL7UPSIKjA=
|
||||
github.com/ipfs/go-log v1.0.1/go.mod h1:HuWlQttfN6FWNHRhlY5yMk/lW7evQC0HHGOxEwMRR8I=
|
||||
github.com/ipfs/go-log v1.0.2/go.mod h1:1MNjMxe0u6xvJZgeqbJ8vdo2TKaGwZ1a0Bpza+sr2Sk=
|
||||
github.com/ipfs/go-log v1.0.3/go.mod h1:OsLySYkwIbiSUR/yBTdv1qPtcE4FW3WPWk/ewz9Ru+A=
|
||||
github.com/ipfs/go-log/v2 v2.0.1/go.mod h1:O7P1lJt27vWHhOwQmcFEvlmo49ry2VY2+JfBWFaa9+0=
|
||||
github.com/ipfs/go-log/v2 v2.0.2/go.mod h1:O7P1lJt27vWHhOwQmcFEvlmo49ry2VY2+JfBWFaa9+0=
|
||||
github.com/ipfs/go-log/v2 v2.0.3/go.mod h1:O7P1lJt27vWHhOwQmcFEvlmo49ry2VY2+JfBWFaa9+0=
|
||||
github.com/ipfs/go-merkledag v0.2.3/go.mod h1:SQiXrtSts3KGNmgOzMICy5c0POOpUNQLvB3ClKnBAlk=
|
||||
github.com/ipfs/go-merkledag v0.3.1/go.mod h1:fvkZNNZixVW6cKSZ/JfLlON5OlgTXNdRLz0p6QG/I2M=
|
||||
github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY=
|
||||
github.com/ipfs/go-peertaskqueue v0.1.0/go.mod h1:Jmk3IyCcfl1W3jTW3YpghSwSEC6IJ3Vzz/jUmWw8Z0U=
|
||||
github.com/ipfs/go-peertaskqueue v0.1.1/go.mod h1:Jmk3IyCcfl1W3jTW3YpghSwSEC6IJ3Vzz/jUmWw8Z0U=
|
||||
github.com/ipfs/go-peertaskqueue v0.2.0/go.mod h1:5/eNrBEbtSKWCG+kQK8K8fGNixoYUnr+P7jivavs9lY=
|
||||
github.com/ipfs/go-unixfs v0.2.4/go.mod h1:SUdisfUjNoSDzzhGVxvCL9QO/nKdwXdr+gbMUdqcbYw=
|
||||
github.com/ipfs/go-verifcid v0.0.1/go.mod h1:5Hrva5KBeIog4A+UpqlaIU+DEstipcJYQQZc0g37pY0=
|
||||
github.com/ipld/go-car v0.1.1-0.20200429200904-c222d793c339/go.mod h1:eajxljm6I8o3LitnFeVEmucwZmz7+yLSiKce9yYMefg=
|
||||
github.com/ipld/go-ipld-prime v0.0.2-0.20200428162820-8b59dc292b8e/go.mod h1:uVIwe/u0H4VdKv3kaN1ck7uCb6yD9cFLS9/ELyXbsw8=
|
||||
github.com/ipld/go-ipld-prime-proto v0.0.0-20200428191222-c1ffdadc01e1/go.mod h1:OAV6xBmuTLsPZ+epzKkPB1e25FHk/vCtyatkdHcArLs=
|
||||
github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52/go.mod h1:fdg+/X9Gg4AsAIzWpEHwnqd+QY3b7lajxyjE1m4hkq4=
|
||||
github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA=
|
||||
github.com/jackpal/go-nat-pmp v1.0.1/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jbenet/go-cienv v0.0.0-20150120210510-1bb1476777ec/go.mod h1:rGaEvXB4uRSZMmzKNLoXvTu1sfx+1kv/DojUlPrSZGs=
|
||||
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
|
||||
github.com/jbenet/go-random v0.0.0-20190219211222-123a90aedc0c/go.mod h1:sdx1xVM9UuLw1tXnhJWN3piypTUO3vCIHYmG15KE/dU=
|
||||
github.com/jbenet/go-temp-err-catcher v0.0.0-20150120210811-aac704a3f4f2/go.mod h1:8GXXJV31xl8whumTzdZsTt3RnUIiPqzkyf7mxToRCMs=
|
||||
github.com/jbenet/goprocess v0.0.0-20160826012719-b497e2f366b8/go.mod h1:Ly/wlsjFq/qrU3Rar62tu1gASgGw6chQbSh/XgIIXCY=
|
||||
github.com/jbenet/goprocess v0.1.3/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
|
||||
github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
|
||||
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/compress v1.9.8 h1:VMAMUUOh+gaxKTMk+zqbjsSjsIcUcL/LF4o63i82QyA=
|
||||
github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
|
||||
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/koron/go-ssdp v0.0.0-20180514024734-4a0ed625a78b/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk=
|
||||
github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/libp2p/go-addr-util v0.0.1/go.mod h1:4ac6O7n9rIAKB1dnd+s8IbbMXkt+oBpzX4/+RACcnlQ=
|
||||
github.com/libp2p/go-buffer-pool v0.0.1/go.mod h1:xtyIz9PMobb13WaxR6Zo1Pd1zXJKYg0a8KiIvDp3TzQ=
|
||||
github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM=
|
||||
github.com/libp2p/go-conn-security-multistream v0.1.0/go.mod h1:aw6eD7LOsHEX7+2hJkDxw1MteijaVcI+/eP2/x3J1xc=
|
||||
github.com/libp2p/go-eventbus v0.1.0/go.mod h1:vROgu5cs5T7cv7POWlWxBaVLxfSegC5UGQf8A2eEmx4=
|
||||
github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZxBdp967ls1g+k8=
|
||||
github.com/libp2p/go-flow-metrics v0.0.3/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs=
|
||||
github.com/libp2p/go-libp2p v0.1.0/go.mod h1:6D/2OBauqLUoqcADOJpn9WbKqvaM07tDw68qHM0BxUM=
|
||||
github.com/libp2p/go-libp2p v0.1.1/go.mod h1:I00BRo1UuUSdpuc8Q2mN7yDF/oTUTRAX6JWpTiK9Rp8=
|
||||
github.com/libp2p/go-libp2p v0.6.0/go.mod h1:mfKWI7Soz3ABX+XEBR61lGbg+ewyMtJHVt043oWeqwg=
|
||||
github.com/libp2p/go-libp2p-autonat v0.1.0/go.mod h1:1tLf2yXxiE/oKGtDwPYWTSYG3PtvYlJmg7NeVtPRqH8=
|
||||
github.com/libp2p/go-libp2p-autonat v0.1.1/go.mod h1:OXqkeGOY2xJVWKAGV2inNF5aKN/djNA3fdpCWloIudE=
|
||||
github.com/libp2p/go-libp2p-blankhost v0.1.1/go.mod h1:pf2fvdLJPsC1FsVrNP3DUUvMzUts2dsLLBEpo1vW1ro=
|
||||
github.com/libp2p/go-libp2p-blankhost v0.1.4/go.mod h1:oJF0saYsAXQCSfDq254GMNmLNz6ZTHTOvtF4ZydUvwU=
|
||||
github.com/libp2p/go-libp2p-circuit v0.1.0/go.mod h1:Ahq4cY3V9VJcHcn1SBXjr78AbFkZeIRmfunbA7pmFh8=
|
||||
github.com/libp2p/go-libp2p-circuit v0.1.4/go.mod h1:CY67BrEjKNDhdTk8UgBX1Y/H5c3xkAcs3gnksxY7osU=
|
||||
github.com/libp2p/go-libp2p-core v0.0.1/go.mod h1:g/VxnTZ/1ygHxH3dKok7Vno1VfpvGcGip57wjTU4fco=
|
||||
github.com/libp2p/go-libp2p-core v0.0.2/go.mod h1:9dAcntw/n46XycV4RnlBq3BpgrmyUi9LuoTNdPrbUco=
|
||||
github.com/libp2p/go-libp2p-core v0.0.3/go.mod h1:j+YQMNz9WNSkNezXOsahp9kwZBKBvxLpKD316QWSJXE=
|
||||
github.com/libp2p/go-libp2p-core v0.0.4/go.mod h1:jyuCQP356gzfCFtRKyvAbNkyeuxb7OlyhWZ3nls5d2I=
|
||||
github.com/libp2p/go-libp2p-core v0.2.0/go.mod h1:X0eyB0Gy93v0DZtSYbEM7RnMChm9Uv3j7yRXjO77xSI=
|
||||
github.com/libp2p/go-libp2p-core v0.2.2/go.mod h1:8fcwTbsG2B+lTgRJ1ICZtiM5GWCWZVoVrLaDRvIRng0=
|
||||
github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g=
|
||||
github.com/libp2p/go-libp2p-core v0.3.0/go.mod h1:ACp3DmS3/N64c2jDzcV429ukDpicbL6+TrrxANBjPGw=
|
||||
github.com/libp2p/go-libp2p-core v0.3.1/go.mod h1:thvWy0hvaSBhnVBaW37BvzgVV68OUhgJJLAa6almrII=
|
||||
github.com/libp2p/go-libp2p-core v0.4.0/go.mod h1:49XGI+kc38oGVwqSBhDEwytaAxgZasHhFfQKibzTls0=
|
||||
github.com/libp2p/go-libp2p-core v0.5.0/go.mod h1:49XGI+kc38oGVwqSBhDEwytaAxgZasHhFfQKibzTls0=
|
||||
github.com/libp2p/go-libp2p-crypto v0.1.0/go.mod h1:sPUokVISZiy+nNuTTH/TY+leRSxnFj/2GLjtOTW90hI=
|
||||
github.com/libp2p/go-libp2p-discovery v0.1.0/go.mod h1:4F/x+aldVHjHDHuX85x1zWoFTGElt8HnoDzwkFZm29g=
|
||||
github.com/libp2p/go-libp2p-discovery v0.2.0/go.mod h1:s4VGaxYMbw4+4+tsoQTqh7wfxg97AEdo4GYBt6BadWg=
|
||||
github.com/libp2p/go-libp2p-loggables v0.1.0/go.mod h1:EyumB2Y6PrYjr55Q3/tiJ/o3xoDasoRYM7nOzEpoa90=
|
||||
github.com/libp2p/go-libp2p-mplex v0.2.0/go.mod h1:Ejl9IyjvXJ0T9iqUTE1jpYATQ9NM3g+OtR+EMMODbKo=
|
||||
github.com/libp2p/go-libp2p-mplex v0.2.1/go.mod h1:SC99Rxs8Vuzrf/6WhmH41kNn13TiYdAWNYHrwImKLnE=
|
||||
github.com/libp2p/go-libp2p-mplex v0.2.2/go.mod h1:74S9eum0tVQdAfFiKxAyKzNdSuLqw5oadDq7+L/FELo=
|
||||
github.com/libp2p/go-libp2p-nat v0.0.4/go.mod h1:N9Js/zVtAXqaeT99cXgTV9e75KpnWCvVOiGzlcHmBbY=
|
||||
github.com/libp2p/go-libp2p-nat v0.0.5/go.mod h1:1qubaE5bTZMJE+E/uu2URroMbzdubFz1ChgiN79yKPE=
|
||||
github.com/libp2p/go-libp2p-netutil v0.1.0/go.mod h1:3Qv/aDqtMLTUyQeundkKsA+YCThNdbQD54k3TqjpbFU=
|
||||
github.com/libp2p/go-libp2p-peer v0.2.0/go.mod h1:RCffaCvUyW2CJmG2gAWVqwePwW7JMgxjsHm7+J5kjWY=
|
||||
github.com/libp2p/go-libp2p-peerstore v0.1.0/go.mod h1:2CeHkQsr8svp4fZ+Oi9ykN1HBb6u0MOvdJ7YIsmcwtY=
|
||||
github.com/libp2p/go-libp2p-peerstore v0.1.3/go.mod h1:BJ9sHlm59/80oSkpWgr1MyY1ciXAXV397W6h1GH/uKI=
|
||||
github.com/libp2p/go-libp2p-peerstore v0.2.0/go.mod h1:N2l3eVIeAitSg3Pi2ipSrJYnqhVnMNQZo9nkSCuAbnQ=
|
||||
github.com/libp2p/go-libp2p-pnet v0.2.0/go.mod h1:Qqvq6JH/oMZGwqs3N1Fqhv8NVhrdYcO0BW4wssv21LA=
|
||||
github.com/libp2p/go-libp2p-record v0.1.0/go.mod h1:ujNc8iuE5dlKWVy6wuL6dd58t0n7xI4hAIl8pE6wu5Q=
|
||||
github.com/libp2p/go-libp2p-record v0.1.1/go.mod h1:VRgKajOyMVgP/F0L5g3kH7SVskp17vFi2xheb5uMJtg=
|
||||
github.com/libp2p/go-libp2p-secio v0.1.0/go.mod h1:tMJo2w7h3+wN4pgU2LSYeiKPrfqBgkOsdiKK77hE7c8=
|
||||
github.com/libp2p/go-libp2p-secio v0.2.0/go.mod h1:2JdZepB8J5V9mBp79BmwsaPQhRPNN2NrnB2lKQcdy6g=
|
||||
github.com/libp2p/go-libp2p-secio v0.2.1/go.mod h1:cWtZpILJqkqrSkiYcDBh5lA3wbT2Q+hz3rJQq3iftD8=
|
||||
github.com/libp2p/go-libp2p-swarm v0.1.0/go.mod h1:wQVsCdjsuZoc730CgOvh5ox6K8evllckjebkdiY5ta4=
|
||||
github.com/libp2p/go-libp2p-swarm v0.2.2/go.mod h1:fvmtQ0T1nErXym1/aa1uJEyN7JzaTNyBcHImCxRpPKU=
|
||||
github.com/libp2p/go-libp2p-testing v0.0.2/go.mod h1:gvchhf3FQOtBdr+eFUABet5a4MBLK8jM3V4Zghvmi+E=
|
||||
github.com/libp2p/go-libp2p-testing v0.0.3/go.mod h1:gvchhf3FQOtBdr+eFUABet5a4MBLK8jM3V4Zghvmi+E=
|
||||
github.com/libp2p/go-libp2p-testing v0.0.4/go.mod h1:gvchhf3FQOtBdr+eFUABet5a4MBLK8jM3V4Zghvmi+E=
|
||||
github.com/libp2p/go-libp2p-testing v0.1.0/go.mod h1:xaZWMJrPUM5GlDBxCeGUi7kI4eqnjVyavGroI2nxEM0=
|
||||
github.com/libp2p/go-libp2p-testing v0.1.1/go.mod h1:xaZWMJrPUM5GlDBxCeGUi7kI4eqnjVyavGroI2nxEM0=
|
||||
github.com/libp2p/go-libp2p-transport-upgrader v0.1.1/go.mod h1:IEtA6or8JUbsV07qPW4r01GnTenLW4oi3lOPbUMGJJA=
|
||||
github.com/libp2p/go-libp2p-transport-upgrader v0.2.0/go.mod h1:mQcrHj4asu6ArfSoMuyojOdjx73Q47cYD7s5+gZOlns=
|
||||
github.com/libp2p/go-libp2p-yamux v0.2.0/go.mod h1:Db2gU+XfLpm6E4rG5uGCFX6uXA8MEXOxFcRoXUODaK8=
|
||||
github.com/libp2p/go-libp2p-yamux v0.2.1/go.mod h1:1FBXiHDk1VyRM1C0aez2bCfHQ4vMZKkAQzZbkSQt5fI=
|
||||
github.com/libp2p/go-libp2p-yamux v0.2.2/go.mod h1:lIohaR0pT6mOt0AZ0L2dFze9hds9Req3OfS+B+dv4qw=
|
||||
github.com/libp2p/go-maddr-filter v0.0.4/go.mod h1:6eT12kSQMA9x2pvFQa+xesMKUBlj9VImZbj3B9FBH/Q=
|
||||
github.com/libp2p/go-maddr-filter v0.0.5/go.mod h1:Jk+36PMfIqCJhAnaASRH83bdAvfDRp/w6ENFaC9bG+M=
|
||||
github.com/libp2p/go-mplex v0.0.3/go.mod h1:pK5yMLmOoBR1pNCqDlA2GQrdAVTMkqFalaTWe7l4Yd0=
|
||||
github.com/libp2p/go-mplex v0.1.0/go.mod h1:SXgmdki2kwCUlCCbfGLEgHjC4pFqhTp0ZoV6aiKgxDU=
|
||||
github.com/libp2p/go-mplex v0.1.1/go.mod h1:Xgz2RDCi3co0LeZfgjm4OgUF15+sVR8SRcu3SFXI1lk=
|
||||
github.com/libp2p/go-msgio v0.0.2/go.mod h1:63lBBgOTDKQL6EWazRMCwXsEeEeK9O2Cd+0+6OOuipQ=
|
||||
github.com/libp2p/go-msgio v0.0.3/go.mod h1:63lBBgOTDKQL6EWazRMCwXsEeEeK9O2Cd+0+6OOuipQ=
|
||||
github.com/libp2p/go-msgio v0.0.4/go.mod h1:63lBBgOTDKQL6EWazRMCwXsEeEeK9O2Cd+0+6OOuipQ=
|
||||
github.com/libp2p/go-nat v0.0.3/go.mod h1:88nUEt0k0JD45Bk93NIwDqjlhiOwOoV36GchpcVc1yI=
|
||||
github.com/libp2p/go-nat v0.0.4/go.mod h1:Nmw50VAvKuk38jUBcmNh6p9lUJLoODbJRvYAa/+KSDo=
|
||||
github.com/libp2p/go-openssl v0.0.2/go.mod h1:v8Zw2ijCSWBQi8Pq5GAixw6DbFfa9u6VIYDXnvOXkc0=
|
||||
github.com/libp2p/go-openssl v0.0.3/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc=
|
||||
github.com/libp2p/go-openssl v0.0.4/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc=
|
||||
github.com/libp2p/go-reuseport v0.0.1/go.mod h1:jn6RmB1ufnQwl0Q1f+YxAj8isJgDCQzaaxIFYDhcYEA=
|
||||
github.com/libp2p/go-reuseport-transport v0.0.2/go.mod h1:YkbSDrvjUVDL6b8XqriyA20obEtsW9BLkuOUyQAOCbs=
|
||||
github.com/libp2p/go-stream-muxer v0.0.1/go.mod h1:bAo8x7YkSpadMTbtTaxGVHWUQsR/l5MEaHbKaliuT14=
|
||||
github.com/libp2p/go-stream-muxer-multistream v0.2.0/go.mod h1:j9eyPol/LLRqT+GPLSxvimPhNph4sfYfMoDPd7HkzIc=
|
||||
github.com/libp2p/go-tcp-transport v0.1.0/go.mod h1:oJ8I5VXryj493DEJ7OsBieu8fcg2nHGctwtInJVpipc=
|
||||
github.com/libp2p/go-tcp-transport v0.1.1/go.mod h1:3HzGvLbx6etZjnFlERyakbaYPdfjg2pWP97dFZworkY=
|
||||
github.com/libp2p/go-testutil v0.1.0/go.mod h1:81b2n5HypcVyrCg/MJx4Wgfp/VHojytjVe/gLzZ2Ehc=
|
||||
github.com/libp2p/go-ws-transport v0.1.0/go.mod h1:rjw1MG1LU9YDC6gzmwObkPd/Sqwhw7yT74kj3raBFuo=
|
||||
github.com/libp2p/go-ws-transport v0.2.0/go.mod h1:9BHJz/4Q5A9ludYWKoGCFC5gUElzlHoKzu0yY9p/klM=
|
||||
github.com/libp2p/go-yamux v1.2.2/go.mod h1:FGTiPvoV/3DVdgWpX+tM0OW3tsM+W5bSE3gZwqQTcow=
|
||||
github.com/libp2p/go-yamux v1.2.3/go.mod h1:FGTiPvoV/3DVdgWpX+tM0OW3tsM+W5bSE3gZwqQTcow=
|
||||
github.com/libp2p/go-yamux v1.3.0/go.mod h1:FGTiPvoV/3DVdgWpX+tM0OW3tsM+W5bSE3gZwqQTcow=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b h1:v29yPGHhOqw7VHEnTeQFAth3SsBrmwc8JfuhNY0G34k=
|
||||
github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b/go.mod h1:5MWrJXKRQyhQdUCF+vu6U5c4nQpg70vW3eHaU0/AYbU=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.12/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
|
||||
github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
|
||||
github.com/minio/sha256-simd v0.0.0-20190328051042-05b4dd3047e5/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
|
||||
github.com/minio/sha256-simd v0.1.0/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
|
||||
github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
|
||||
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
|
||||
github.com/mr-tron/base58 v1.1.1/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
|
||||
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA=
|
||||
github.com/multiformats/go-multiaddr v0.0.1/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
|
||||
github.com/multiformats/go-multiaddr v0.0.2/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
|
||||
github.com/multiformats/go-multiaddr v0.0.4/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
|
||||
github.com/multiformats/go-multiaddr v0.1.0/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
|
||||
github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
|
||||
github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4=
|
||||
github.com/multiformats/go-multiaddr v0.2.1/go.mod h1:s/Apk6IyxfvMjDafnhJgJ3/46z7tZ04iMk5wP4QMGGE=
|
||||
github.com/multiformats/go-multiaddr-dns v0.0.1/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q=
|
||||
github.com/multiformats/go-multiaddr-dns v0.0.2/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q=
|
||||
github.com/multiformats/go-multiaddr-dns v0.2.0/go.mod h1:TJ5pr5bBO7Y1B18djPuRsVkduhQH2YqYSbxWJzYGdK0=
|
||||
github.com/multiformats/go-multiaddr-fmt v0.0.1/go.mod h1:aBYjqL4T/7j4Qx+R73XSv/8JsgnRFlf0w2KGLCmXl3Q=
|
||||
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
|
||||
github.com/multiformats/go-multiaddr-net v0.0.1/go.mod h1:nw6HSxNmCIQH27XPGBuX+d1tnvM7ihcFwHMSstNAVUU=
|
||||
github.com/multiformats/go-multiaddr-net v0.1.0/go.mod h1:5JNbcfBOP4dnhoZOv10JJVkJO0pCCEf8mTnipAo2UZQ=
|
||||
github.com/multiformats/go-multiaddr-net v0.1.1/go.mod h1:5JNbcfBOP4dnhoZOv10JJVkJO0pCCEf8mTnipAo2UZQ=
|
||||
github.com/multiformats/go-multiaddr-net v0.1.2/go.mod h1:QsWt3XK/3hwvNxZJp92iMQKME1qHfpYmyIjFVsSOY6Y=
|
||||
github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs=
|
||||
github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U=
|
||||
github.com/multiformats/go-multihash v0.0.5/go.mod h1:lt/HCbqlQwlPBz7lv0sQCdtfcMtlJvakRUn/0Ual8po=
|
||||
github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
|
||||
github.com/multiformats/go-multihash v0.0.9/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
|
||||
github.com/multiformats/go-multihash v0.0.10/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
|
||||
github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc=
|
||||
github.com/multiformats/go-multistream v0.1.0/go.mod h1:fJTiDfXJVmItycydCnNx4+wSzZ5NwG2FEVAI30fiovg=
|
||||
github.com/multiformats/go-multistream v0.1.1/go.mod h1:KmHZ40hzVxiaiwlj3MEbYgK9JFk2/9UktWZAF54Du38=
|
||||
github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
|
||||
github.com/multiformats/go-varint v0.0.2/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
|
||||
github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pierrec/lz4 v2.4.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/polydawn/refmt v0.0.0-20190221155625-df39d6c2d992/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o=
|
||||
github.com/polydawn/refmt v0.0.0-20190408063855-01bf1e26dd14/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o=
|
||||
github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o=
|
||||
github.com/polydawn/refmt v0.0.0-20190809202753-05966cbd336a/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190222223459-a17d461953aa/go.mod h1:2RVY1rIf+2J2o/IM9+vPq9RzmHDSseB7FoXiSNIUsoU=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smola/gocompat v0.2.0/go.mod h1:1B0MlxbmoZNo3h8guHp8HztB3BSYR5itql9qtVc0ypY=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spacemonkeygo/openssl v0.0.0-20181017203307-c2dcc5cca94a/go.mod h1:7AyxJNCJ7SBZ1MfVQCWD6Uqo2oubI2Eq2y2eqf+A5r0=
|
||||
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/src-d/envconfig v1.0.0/go.mod h1:Q9YQZ7BKITldTBnoxsE5gOeB5y66RyPXeue/R4aaNBc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/urfave/cli/v2 v2.0.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/warpfork/go-wish v0.0.0-20180510122957-5ad1f5abf436/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||
github.com/warpfork/go-wish v0.0.0-20190328234359-8b3e70f8e830/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||
github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||
github.com/whyrusleeping/cbor-gen v0.0.0-20191212224538-d370462a7e8a/go.mod h1:xdlJQaiqipF0HW+Mzpg7XRM3fWbGvfgFlcppuvlkIvY=
|
||||
github.com/whyrusleeping/cbor-gen v0.0.0-20191216205031-b047b6acb3c0/go.mod h1:xdlJQaiqipF0HW+Mzpg7XRM3fWbGvfgFlcppuvlkIvY=
|
||||
github.com/whyrusleeping/cbor-gen v0.0.0-20200123233031-1cdf64d27158/go.mod h1:Xj/M2wWU+QdTdRbu/L/1dIZY8/Wb2K9pAhtroQuxJJI=
|
||||
github.com/whyrusleeping/cbor-gen v0.0.0-20200206220010-03c9665e2a66/go.mod h1:Xj/M2wWU+QdTdRbu/L/1dIZY8/Wb2K9pAhtroQuxJJI=
|
||||
github.com/whyrusleeping/cbor-gen v0.0.0-20200402171437-3d27c146c105/go.mod h1:Xj/M2wWU+QdTdRbu/L/1dIZY8/Wb2K9pAhtroQuxJJI=
|
||||
github.com/whyrusleeping/cbor-gen v0.0.0-20200414195334-429a0b5e922e/go.mod h1:Xj/M2wWU+QdTdRbu/L/1dIZY8/Wb2K9pAhtroQuxJJI=
|
||||
github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f/go.mod h1:p9UJB6dDgdPgMJZs7UjUOdulKyRr9fqkS+6JKAInPy8=
|
||||
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc=
|
||||
github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM=
|
||||
github.com/whyrusleeping/go-logging v0.0.1/go.mod h1:lDPYj54zutzG1XYfHAhcc7oNXEburHQBn+Iqd4yS4vE=
|
||||
github.com/whyrusleeping/go-notifier v0.0.0-20170827234753-097c5d47330f/go.mod h1:cZNvX9cFybI01GriPRMXDtczuvUhgbcYr9iCGaNlRv8=
|
||||
github.com/whyrusleeping/mafmt v1.2.8/go.mod h1:faQJFPbLSxzD9xpA02ttW/tS9vZykNvXwGvqIpk20FA=
|
||||
github.com/whyrusleeping/mdns v0.0.0-20180901202407-ef14215e6b30/go.mod h1:j4l84WPFclQPj320J9gp0XwNKBb3U0zt5CBqjPp22G4=
|
||||
github.com/whyrusleeping/mdns v0.0.0-20190826153040-b9b60ed33aa9/go.mod h1:j4l84WPFclQPj320J9gp0XwNKBb3U0zt5CBqjPp22G4=
|
||||
github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7/go.mod h1:X2c0RVCI1eSUFI8eLcY3c0423ykwiUdxLJtkDvruhjI=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190225124518-7f87c0fbb88b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72 h1:+ELyKg6m8UBf0nPFSqD0mi7zUfwPyXo23HNjMnXPz7w=
|
||||
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190227160552-c95aed5357e7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180202135801-37707fdb30a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190302025703-b6889370fb10/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190524122548-abf6ff778158/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181130052023-1c3d964395ce/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200108195415-316d2f248479/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0 h1:Q3Ui3V3/CVinFWFiW39Iw0kMuVrRzYX0wN6OPFp0lTA=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a h1:Ob5/580gVHBJZgXnff1cZDbG+xLtMVE5mDRTe+nIsX4=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw=
|
||||
gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
|
||||
gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM=
|
||||
gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
|
||||
gopkg.in/jcmturner/goidentity.v3 v3.0.0 h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI=
|
||||
gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
|
||||
gopkg.in/jcmturner/gokrb5.v7 v7.5.0 h1:a9tsXlIDD9SKxotJMK3niV7rPZAJeX2aD/0yg3qlIrg=
|
||||
gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
|
||||
gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU=
|
||||
gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/src-d/go-cli.v0 v0.0.0-20181105080154-d492247bbc0d/go.mod h1:z+K8VcOYVYcSwSjGebuDL6176A1XskgbtNl64NSg+n8=
|
||||
gopkg.in/src-d/go-log.v1 v1.0.1/go.mod h1:GN34hKP0g305ysm2/hctJ0Y8nWP3zxXXJ8GFabTyABE=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
@@ -5,11 +5,17 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
_ "runtime/debug"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -19,11 +25,22 @@ var (
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
var closeCh chan int
|
||||
|
||||
func main() {
|
||||
closeCh = make(chan int)
|
||||
// // Don't exit on panic
|
||||
// defer func() {
|
||||
// if r := recover(); r != nil {
|
||||
// fmt.Printf("PANIC: pkg: %v %s \n", r, debug.Stack())
|
||||
// fmt.Printf("PANIC: pkg: %v %s \n", r, Debug.Stack())
|
||||
// }
|
||||
// }()
|
||||
|
||||
@@ -32,12 +49,27 @@ func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU() * 2)
|
||||
}
|
||||
|
||||
args := os.Args[1:]
|
||||
var plugins *InOutPlugins
|
||||
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 directory on address ", args[1])
|
||||
|
||||
log.Fatal(http.ListenAndServe(args[1], loggingMiddleware(http.FileServer(http.Dir(dir)))))
|
||||
} else {
|
||||
flag.Parse()
|
||||
checkSettings()
|
||||
time.Sleep(60*time.Second)
|
||||
plugins = InitPlugins()
|
||||
}
|
||||
|
||||
fmt.Println("Version:", VERSION)
|
||||
|
||||
flag.Parse()
|
||||
InitPlugins()
|
||||
|
||||
if len(Plugins.Inputs) == 0 || len(Plugins.Outputs) == 0 {
|
||||
if len(plugins.Inputs) == 0 || len(plugins.Outputs) == 0 {
|
||||
log.Fatal("Required at least 1 input and 1 output")
|
||||
}
|
||||
|
||||
@@ -49,7 +81,41 @@ func main() {
|
||||
profileCPU(*cpuprofile)
|
||||
}
|
||||
|
||||
Start(nil)
|
||||
if Settings.Pprof != "" {
|
||||
go func() {
|
||||
log.Println(http.ListenAndServe(Settings.Pprof, nil))
|
||||
}()
|
||||
}
|
||||
|
||||
emitter := NewEmitter(closeCh)
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
finalize(plugins)
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
if Settings.ExitAfter > 0 {
|
||||
log.Println("Running gor for a duration of", Settings.ExitAfter)
|
||||
|
||||
time.AfterFunc(Settings.ExitAfter, func() {
|
||||
log.Println("Stopping gor after", Settings.ExitAfter)
|
||||
close(closeCh)
|
||||
})
|
||||
}
|
||||
|
||||
emitter.Start(plugins, Settings.Middleware)
|
||||
}
|
||||
|
||||
func finalize(plugins *InOutPlugins) {
|
||||
for _, p := range plugins.All {
|
||||
if cp, ok := p.(io.Closer); ok {
|
||||
cp.Close()
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func profileCPU(cpuprofile string) {
|
||||
|
||||
+8
-10
@@ -7,27 +7,25 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
rate = 5
|
||||
)
|
||||
|
||||
type GorStat struct {
|
||||
statName string
|
||||
rateMs int
|
||||
latest int
|
||||
mean int
|
||||
max int
|
||||
count int
|
||||
}
|
||||
|
||||
func NewGorStat(statName string) (s *GorStat) {
|
||||
func NewGorStat(statName string, rateMs int) (s *GorStat) {
|
||||
s = new(GorStat)
|
||||
s.statName = statName
|
||||
s.rateMs = rateMs
|
||||
s.latest = 0
|
||||
s.mean = 0
|
||||
s.max = 0
|
||||
s.count = 0
|
||||
|
||||
if Settings.stats {
|
||||
if Settings.Stats {
|
||||
log.Println(s.statName + ":latest,mean,max,count,count/second,gcount")
|
||||
go s.reportStats()
|
||||
}
|
||||
@@ -35,12 +33,12 @@ func NewGorStat(statName string) (s *GorStat) {
|
||||
}
|
||||
|
||||
func (s *GorStat) Write(latest int) {
|
||||
if Settings.stats {
|
||||
if Settings.Stats {
|
||||
if latest > s.max {
|
||||
s.max = latest
|
||||
}
|
||||
if latest != 0 {
|
||||
s.mean = (s.mean + latest) / 2
|
||||
s.mean = ((s.mean * s.count) + latest) / (s.count + 1)
|
||||
}
|
||||
s.latest = latest
|
||||
s.count = s.count + 1
|
||||
@@ -55,13 +53,13 @@ func (s *GorStat) Reset() {
|
||||
}
|
||||
|
||||
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())
|
||||
return s.statName + ":" + strconv.Itoa(s.latest) + "," + strconv.Itoa(s.mean) + "," + strconv.Itoa(s.max) + "," + strconv.Itoa(s.count) + "," + strconv.Itoa(s.count/(s.rateMs/1000.0)) + "," + strconv.Itoa(runtime.NumGoroutine())
|
||||
}
|
||||
|
||||
func (s *GorStat) reportStats() {
|
||||
for {
|
||||
log.Println(s)
|
||||
s.Reset()
|
||||
time.Sleep(rate * time.Second)
|
||||
time.Sleep(time.Duration(s.rateMs) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
+333
-38
@@ -1,17 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"github.com/buger/gor/proto"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/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",
|
||||
@@ -24,15 +42,20 @@ type HTTPClientConfig struct {
|
||||
ConnectionTimeout time.Duration
|
||||
Timeout time.Duration
|
||||
ResponseBufferSize int
|
||||
CompatibilityMode bool
|
||||
}
|
||||
|
||||
type HTTPClient struct {
|
||||
baseURL string
|
||||
scheme string
|
||||
host string
|
||||
auth string
|
||||
conn net.Conn
|
||||
proxy *url.URL
|
||||
proxyAuth string
|
||||
respBuf []byte
|
||||
config *HTTPClientConfig
|
||||
goClient *http.Client
|
||||
redirectsCount int
|
||||
}
|
||||
|
||||
@@ -42,15 +65,12 @@ func NewHTTPClient(baseURL string, config *HTTPClientConfig) *HTTPClient {
|
||||
}
|
||||
|
||||
u, _ := url.Parse(baseURL)
|
||||
if !strings.Contains(u.Host, ":") {
|
||||
if u.Scheme != "http" {
|
||||
u.Host += ":" + defaultPorts[u.Scheme]
|
||||
}
|
||||
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = time.Second
|
||||
}
|
||||
|
||||
if config.Timeout.Nanoseconds() == 0 {
|
||||
config.Timeout = 5 * time.Second
|
||||
}
|
||||
config.ConnectionTimeout = config.Timeout
|
||||
|
||||
if config.ResponseBufferSize == 0 {
|
||||
config.ResponseBufferSize = 100 * 1024 // 100kb
|
||||
@@ -63,26 +83,93 @@ func NewHTTPClient(baseURL string, config *HTTPClientConfig) *HTTPClient {
|
||||
client.respBuf = make([]byte, config.ResponseBufferSize)
|
||||
client.config = config
|
||||
|
||||
if config.CompatibilityMode {
|
||||
client.goClient = &http.Client{
|
||||
// #TODO
|
||||
// CheckRedirect: redirectPolicyFunc,
|
||||
}
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
client.auth = "Basic " + base64.StdEncoding.EncodeToString([]byte(u.User.String()))
|
||||
}
|
||||
|
||||
client.proxy, _ = http.ProxyFromEnvironment(&http.Request{URL: u})
|
||||
|
||||
if client.isProxy() && client.proxy.User != nil {
|
||||
client.proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(client.proxy.User.String()))
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Connect() (err error) {
|
||||
c.Disconnect()
|
||||
|
||||
var toDial string
|
||||
if !strings.Contains(c.host, ":") {
|
||||
c.conn, err = net.DialTimeout("tcp", c.host+":80", c.config.ConnectionTimeout)
|
||||
toDial = c.host + ":" + defaultPorts[c.scheme]
|
||||
} else {
|
||||
c.conn, err = net.DialTimeout("tcp", c.host, c.config.ConnectionTimeout)
|
||||
toDial = c.host
|
||||
}
|
||||
|
||||
if c.isProxy() {
|
||||
if c.proxy.Scheme != "http" {
|
||||
panic("Unsupported HTTP Proxy method")
|
||||
}
|
||||
Debug("[HTTPClient] Connecting to proxy", c.proxy.String(), "<>", toDial)
|
||||
c.conn, err = net.DialTimeout("tcp", c.proxy.Host, c.config.ConnectionTimeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if c.scheme == "https" {
|
||||
c.conn.Write([]byte("CONNECT " + toDial + " HTTP/1.1\r\n"))
|
||||
if c.proxyAuth != "" {
|
||||
c.conn.Write([]byte("Proxy-Authorization: " + c.proxyAuth + "\r\n"))
|
||||
}
|
||||
c.conn.Write([]byte("\r\n"))
|
||||
br := bufio.NewReader(c.conn)
|
||||
l, _, err := br.ReadLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(l) < 12 {
|
||||
panic("HTTP proxy did not respond correctly")
|
||||
}
|
||||
status := l[9:12]
|
||||
if !bytes.Equal(status, []byte("200")) {
|
||||
panic("HTTP proxy did not respond correctly")
|
||||
}
|
||||
for {
|
||||
// Read until we find the empty line
|
||||
l, _, err := br.ReadLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(l) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Debug("[HTTPClient] Proxy successfully connected")
|
||||
} else {
|
||||
c.conn, err = net.DialTimeout("tcp", toDial, c.config.ConnectionTimeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if c.scheme == "https" {
|
||||
tlsConn := tls.Client(c.conn, &tls.Config{InsecureSkipVerify: true})
|
||||
// Wrap our socket in TLS
|
||||
Debug("[HTTPClient] Wrapping socket in TLS", c.host)
|
||||
tlsConn := tls.Client(c.conn, &tls.Config{InsecureSkipVerify: true, ServerName: c.host})
|
||||
|
||||
if err = tlsConn.Handshake(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.conn = tlsConn
|
||||
Debug("[HTTPClient] Successfully wrapped in TLS")
|
||||
}
|
||||
|
||||
return
|
||||
@@ -96,32 +183,76 @@ func (c *HTTPClient) Disconnect() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPClient) isAlive() bool {
|
||||
one := make([]byte, 1)
|
||||
|
||||
func (c *HTTPClient) isAlive(readBytes *int) bool {
|
||||
// Ready 1 byte from socket without timeout to check if it not closed
|
||||
c.conn.SetReadDeadline(time.Now().Add(time.Millisecond))
|
||||
if _, err := c.conn.Read(one); err == io.EOF {
|
||||
n, err := c.conn.Read(c.respBuf[:1])
|
||||
|
||||
if err == io.EOF {
|
||||
Debug("[HTTPClient] connection closed, reconnecting")
|
||||
return false
|
||||
}
|
||||
|
||||
if err == syscall.EPIPE {
|
||||
Debug("Detected broken pipe.", err)
|
||||
return false
|
||||
}
|
||||
if n != 0 {
|
||||
*readBytes += n
|
||||
Debug("[HTTPClient] isAlive readBytes ", *readBytes)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *HTTPClient) SendGoClient(data []byte) ([]byte, error) {
|
||||
var req *http.Request
|
||||
var resp *http.Response
|
||||
var err error
|
||||
|
||||
req, err = http.ReadRequest(bufio.NewReader(bytes.NewBuffer(data)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !c.config.OriginalHost {
|
||||
req.Host = c.host
|
||||
}
|
||||
|
||||
if c.auth != "" {
|
||||
req.Header.Add("Authorization", c.auth)
|
||||
}
|
||||
|
||||
req.URL, _ = url.ParseRequestURI(c.scheme + "://" + c.host + req.RequestURI)
|
||||
req.RequestURI = ""
|
||||
|
||||
resp, err = c.goClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return httputil.DumpResponse(resp, 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 {
|
||||
if _, ok := r.(error); ok {
|
||||
log.Println("[HTTPClient] Failed to send request: ", string(data))
|
||||
log.Println("PANIC: pkg:", r, debug.Stack())
|
||||
log.Println("[HTTPClient] Response: ", string(response))
|
||||
log.Println("PANIC: pkg:", r, string(debug.Stack()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if c.conn == nil || !c.isAlive() {
|
||||
if c.config.CompatibilityMode {
|
||||
return c.SendGoClient(data)
|
||||
}
|
||||
|
||||
var readBytes int
|
||||
if c.conn == nil || !c.isAlive(&readBytes) {
|
||||
Debug("[HTTPClient] Connecting:", c.baseURL)
|
||||
if err = c.Connect(); err != nil {
|
||||
log.Println("[HTTPClient] Connection error:", err)
|
||||
@@ -138,36 +269,179 @@ func (c *HTTPClient) Send(data []byte) (response []byte, err error) {
|
||||
data = proto.SetHost(data, []byte(c.baseURL), []byte(c.host))
|
||||
}
|
||||
|
||||
if c.isProxy() && c.scheme == "http" {
|
||||
path := proto.Path(data)
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
data = proto.SetPath(data, c.proxyPath(path))
|
||||
if c.proxyAuth != "" {
|
||||
data = proto.SetHeader(data, []byte("Proxy-Authorization"), []byte(c.proxyAuth))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.auth != "" {
|
||||
data = proto.SetHeader(data, []byte("Authorization"), []byte(c.auth))
|
||||
}
|
||||
|
||||
if c.config.Debug {
|
||||
Debug("[HTTPClient] Sending:", string(data))
|
||||
}
|
||||
|
||||
return c.send(data, readBytes, timeout)
|
||||
}
|
||||
|
||||
func (c *HTTPClient) send(data []byte, readBytes int, timeout time.Time) (response []byte, err error) {
|
||||
var payload []byte
|
||||
var n int
|
||||
if _, err = c.conn.Write(data); err != nil {
|
||||
Debug("[HTTPClient] Write error:", err, c.baseURL)
|
||||
response = errorPayload(HTTP_TIMEOUT)
|
||||
return
|
||||
}
|
||||
|
||||
c.conn.SetReadDeadline(timeout)
|
||||
n, err := c.conn.Read(c.respBuf)
|
||||
|
||||
// If response large then our buffer, we need to read all response buffer
|
||||
// Otherwise it will corrupt response of next request
|
||||
// Parsing response body is non trivial thing, especially with keep-alive
|
||||
// Simples case is to to close connection if response too large
|
||||
//
|
||||
// See https://github.com/buger/gor/issues/184
|
||||
if n == len(c.respBuf) {
|
||||
c.Disconnect()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Debug("[HTTPClient] Response read error", err, c.conn)
|
||||
response = errorPayload(HTTP_TIMEOUT)
|
||||
return
|
||||
}
|
||||
|
||||
payload := c.respBuf[:n]
|
||||
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++
|
||||
|
||||
// First chunk
|
||||
if chunked || contentLength != -1 {
|
||||
currentContentLength += n
|
||||
} else {
|
||||
// If headers are finished
|
||||
var firstEmptyLine = bytes.Index(c.respBuf[:readBytes], proto.EmptyLine)
|
||||
if firstEmptyLine != -1 {
|
||||
if bytes.Equal(proto.Header(c.respBuf[:readBytes], []byte("Transfer-Encoding")), []byte("chunked")) {
|
||||
chunked = true
|
||||
} else {
|
||||
status, _ := strconv.Atoi(string(proto.Status(c.respBuf[:readBytes])))
|
||||
// We want to soak up all 100 Continues received to get the real result code
|
||||
if status >= 100 && status < 200 {
|
||||
timeout = time.Now().Add(c.config.Timeout)
|
||||
var deleteLen = firstEmptyLine + len(proto.EmptyLine)
|
||||
copy(c.respBuf, c.respBuf[deleteLen:readBytes])
|
||||
readBytes -= deleteLen
|
||||
chunks--
|
||||
continue
|
||||
} else if status == 204 || status == 304 {
|
||||
contentLength = 0
|
||||
break
|
||||
} else {
|
||||
l := proto.Header(c.respBuf[:readBytes], []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
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if currentChunk == nil {
|
||||
currentChunk = make([]byte, readChunkSize)
|
||||
}
|
||||
|
||||
n, err = c.conn.Read(currentChunk)
|
||||
|
||||
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 err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
Debug("[HTTPClient] Read the whole body error:", err, c.baseURL)
|
||||
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 && readBytes == 0 {
|
||||
Debug("[HTTPClient] Response read timeout error", err, c.conn, readBytes, string(c.respBuf[:readBytes]))
|
||||
response = errorPayload(HTTP_TIMEOUT)
|
||||
c.Disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
if readBytes < 4 || string(c.respBuf[:4]) != "HTTP" {
|
||||
maxRead := 100
|
||||
if readBytes < maxRead {
|
||||
maxRead = readBytes
|
||||
}
|
||||
Debug("[HTTPClient] Response read unknown error", err, c.conn, readBytes, string(c.respBuf[:maxRead]))
|
||||
response = errorPayload(HTTP_UNKNOWN_ERROR)
|
||||
c.Disconnect()
|
||||
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))
|
||||
@@ -181,7 +455,7 @@ func (c *HTTPClient) Send(data []byte) (response []byte, err error) {
|
||||
c.redirectsCount++
|
||||
|
||||
location := proto.Header(payload, []byte("Location"))
|
||||
redirectPayload := []byte("GET " + string(location) + " HTTP/1.1\r\n\r\n")
|
||||
redirectPayload := proto.SetPath(data, location)
|
||||
|
||||
if c.config.Debug {
|
||||
Debug("[HTTPClient] Redirecting to: " + string(location))
|
||||
@@ -191,6 +465,11 @@ func (c *HTTPClient) Send(data []byte) (response []byte, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if bytes.Equal(proto.Status(payload), []byte("400")) {
|
||||
Debug("[HTTPClient] Closed connection on 400 response")
|
||||
c.Disconnect()
|
||||
}
|
||||
|
||||
c.redirectsCount = 0
|
||||
|
||||
return payload, err
|
||||
@@ -202,6 +481,22 @@ func (c *HTTPClient) Get(path string) (response []byte, err error) {
|
||||
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))
|
||||
}
|
||||
|
||||
func (c *HTTPClient) proxyPath(path []byte) []byte {
|
||||
return append([]byte(c.scheme+"://"+c.host), path...)
|
||||
}
|
||||
|
||||
func (c *HTTPClient) isProxy() bool {
|
||||
return c.proxy != nil
|
||||
}
|
||||
|
||||
const (
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200171936-Error-520-Web-server-is-returning-an-unknown-error
|
||||
HTTP_UNKNOWN_ERROR = "520"
|
||||
|
||||
+179
-46
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"github.com/buger/gor/proto"
|
||||
"io/ioutil"
|
||||
_ "log"
|
||||
"net"
|
||||
@@ -11,10 +10,12 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
_ "reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
_ "time"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
func TestHTTPClientURLPort(t *testing.T) {
|
||||
@@ -24,8 +25,8 @@ func TestHTTPClientURLPort(t *testing.T) {
|
||||
}
|
||||
|
||||
c2 := NewHTTPClient("https://example.com", &HTTPClientConfig{})
|
||||
if c2.baseURL != "https://example.com:443" {
|
||||
t.Error("Sould add 443 port for https:", c2.baseURL)
|
||||
if c2.baseURL != "https://example.com" {
|
||||
t.Error("Sould not add 443 port for https:", c2.baseURL)
|
||||
}
|
||||
|
||||
c3 := NewHTTPClient("https://example.com:1", &HTTPClientConfig{})
|
||||
@@ -86,37 +87,121 @@ func TestHTTPClientSend(t *testing.T) {
|
||||
client.Send(payload("POST"))
|
||||
|
||||
wg.Wait()
|
||||
|
||||
client = NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true, CompatibilityMode: true})
|
||||
|
||||
wg.Add(4)
|
||||
|
||||
if _, err := client.Send(payload("POST")); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if _, err := client.Send(payload("GET")); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if _, err := client.Send(payload("POST_CHUNKED")); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if _, err := client.Send(payload("POST")); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientResonseByClose(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
payload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
ln, _ := net.Listen("tcp", ":0")
|
||||
go func() {
|
||||
for {
|
||||
conn, _ := ln.Accept()
|
||||
buf := make([]byte, 4096)
|
||||
conn.Read(buf)
|
||||
|
||||
conn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
|
||||
conn.Write([]byte("ab"))
|
||||
conn.Close()
|
||||
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
client := NewHTTPClient(ln.Addr().String(), &HTTPClientConfig{Debug: true})
|
||||
|
||||
wg.Add(1)
|
||||
resp, _ := client.Send(payload)
|
||||
|
||||
if !bytes.Equal(resp, []byte("HTTP/1.1 200 OK\r\n\r\nab")) {
|
||||
t.Error("Should return valid response", string(resp))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// https://github.com/buger/gor/issues/184
|
||||
func TestHTTPClientResponseBuffer(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
testCases := []struct {
|
||||
name string
|
||||
responseSize int
|
||||
buffserSize int
|
||||
expectedSize int
|
||||
timeout time.Duration
|
||||
}{
|
||||
{"Chunked, buffer overflow", 10 * 1024, 1024, 1024, 50 * time.Millisecond},
|
||||
|
||||
payload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
{"Chunked, fits buffer", 10 * 1024, 64 * 1024, 10*1024 + 145 /* headers length + chunked meta */, 50 * time.Millisecond},
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
{"Content-Length, buffer overflow", 1024, 1000, 1000, 50 * time.Millisecond},
|
||||
|
||||
size := 10 * 1024 // 10kb
|
||||
rb := make([]byte, size)
|
||||
rand.Read(rb)
|
||||
|
||||
w.Write(rb)
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: false, ResponseBufferSize: 1024})
|
||||
|
||||
wg.Add(2)
|
||||
client.Send(payload)
|
||||
resp, _ := client.Send(payload)
|
||||
|
||||
if !bytes.Equal(resp[0:8], []byte("HTTP/1.1")) {
|
||||
t.Error("Response buffer contains data from previous request", string(resp[0:5]))
|
||||
{"Content-Length, fits buffer", 1024, 64 * 1024, 1024 + 118, 50 * time.Millisecond},
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
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) {
|
||||
@@ -292,6 +377,74 @@ func TestHTTPClientRedirectLimit(t *testing.T) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientKeepHeadersRedirect(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
GETPayload = proto.AddHeader(GETPayload, []byte("keep-header"), []byte("true"))
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.URL.Path == "/" {
|
||||
http.Redirect(w, r, "/new", 301)
|
||||
}
|
||||
|
||||
if r.Header.Get("keep-header") != "true" {
|
||||
t.Errorf("Header keep-header was incorrect, got: %s, want: %s.", r.Header.Get("keep-header"), "true")
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{FollowRedirects: 1, Debug: false})
|
||||
|
||||
wg.Add(2)
|
||||
client.Send(GETPayload)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientBasicAuth(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(2)
|
||||
|
||||
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, _ := r.BasicAuth()
|
||||
|
||||
if user != "user" || pass != "pass" {
|
||||
http.Error(w, "Unauthorized.", 401)
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: false})
|
||||
resp, _ := client.Send(GETPayload)
|
||||
client.Disconnect()
|
||||
|
||||
if !bytes.Equal(proto.Status(resp), []byte("401")) {
|
||||
t.Error("Should return unauthorized error", string(resp))
|
||||
}
|
||||
|
||||
authUrl := strings.Replace(server.URL, "http://", "http://user:pass@", -1)
|
||||
client = NewHTTPClient(authUrl, &HTTPClientConfig{Debug: false})
|
||||
resp, _ = client.Send(GETPayload)
|
||||
client.Disconnect()
|
||||
|
||||
if !bytes.Equal(proto.Status(resp), []byte("200")) {
|
||||
t.Error("Should return proper response", string(resp))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientHandleHTTP10(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
@@ -377,24 +530,4 @@ func TestHTTPClientErrors(t *testing.T) {
|
||||
} else {
|
||||
t.Error("Should throw error")
|
||||
}
|
||||
|
||||
ln2, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
go func() {
|
||||
buf := make([]byte, 64*1024)
|
||||
conn, _ := ln2.Accept()
|
||||
|
||||
conn.Read(buf)
|
||||
defer conn.Close()
|
||||
}()
|
||||
defer ln2.Close()
|
||||
|
||||
client = NewHTTPClient("http://"+ln2.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")
|
||||
}
|
||||
}
|
||||
|
||||
+41
-2
@@ -2,9 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"hash/fnv"
|
||||
"strings"
|
||||
|
||||
"github.com/buger/gor/proto"
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
type HTTPModifier struct {
|
||||
@@ -16,8 +18,10 @@ func NewHTTPModifier(config *HTTPModifierConfig) *HTTPModifier {
|
||||
if len(config.urlRegexp) == 0 &&
|
||||
len(config.urlNegativeRegexp) == 0 &&
|
||||
len(config.urlRewrite) == 0 &&
|
||||
len(config.headerRewrite) == 0 &&
|
||||
len(config.headerFilters) == 0 &&
|
||||
len(config.headerNegativeFilters) == 0 &&
|
||||
len(config.headerBasicAuthFilters) == 0 &&
|
||||
len(config.headerHashFilters) == 0 &&
|
||||
len(config.paramHashFilters) == 0 &&
|
||||
len(config.params) == 0 &&
|
||||
@@ -94,7 +98,11 @@ func (m *HTTPModifier) Rewrite(payload []byte) (response []byte) {
|
||||
for _, f := range m.config.headerFilters {
|
||||
value := proto.Header(payload, f.name)
|
||||
|
||||
if len(value) > 0 && !f.regexp.Match(value) {
|
||||
if len(value) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !f.regexp.Match(value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -110,6 +118,23 @@ func (m *HTTPModifier) Rewrite(payload []byte) (response []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.headerBasicAuthFilters) > 0 {
|
||||
for _, f := range m.config.headerBasicAuthFilters {
|
||||
value := proto.Header(payload, []byte("Authorization"))
|
||||
|
||||
if len(value) > 0 {
|
||||
valueString := string(value)
|
||||
trimmedBasicAuthEncoded := strings.TrimPrefix(valueString, "Basic ")
|
||||
if strings.Compare(valueString, trimmedBasicAuthEncoded) != 0 {
|
||||
decodedAuth, _ := base64.StdEncoding.DecodeString(trimmedBasicAuthEncoded)
|
||||
if !f.regexp.Match(decodedAuth) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.headerHashFilters) > 0 {
|
||||
for _, f := range m.config.headerHashFilters {
|
||||
value := proto.Header(payload, f.name)
|
||||
@@ -153,5 +178,19 @@ func (m *HTTPModifier) Rewrite(payload []byte) (response []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.headerRewrite) > 0 {
|
||||
for _, f := range m.config.headerRewrite {
|
||||
value := proto.Header(payload, f.header)
|
||||
if len(value) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if f.src.Match(value) {
|
||||
newValue := f.src.ReplaceAll(value, f.target)
|
||||
payload = proto.SetHeader(payload, f.header, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
+78
-12
@@ -10,13 +10,15 @@ import (
|
||||
|
||||
// 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
|
||||
urlNegativeRegexp HTTPUrlRegexp
|
||||
urlRegexp HTTPUrlRegexp
|
||||
urlRewrite UrlRewriteMap
|
||||
headerRewrite HeaderRewriteMap
|
||||
headerFilters HTTPHeaderFilters
|
||||
headerNegativeFilters HTTPHeaderFilters
|
||||
headerBasicAuthFilters HTTPHeaderBasicAuthFilters
|
||||
headerHashFilters HTTPHashFilters
|
||||
paramHashFilters HTTPHashFilters
|
||||
|
||||
params HTTPParams
|
||||
headers HTTPHeaders
|
||||
@@ -43,7 +45,8 @@ func (h *HTTPHeaderFilters) Set(value string) error {
|
||||
if len(valArr) < 2 {
|
||||
return errors.New("need both header and value, colon-delimited (ex. user_id:^169$).")
|
||||
}
|
||||
r, err := regexp.Compile(valArr[1])
|
||||
val := strings.TrimSpace(valArr[1])
|
||||
r, err := regexp.Compile(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -53,6 +56,31 @@ func (h *HTTPHeaderFilters) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-basic-auth-filter option
|
||||
//
|
||||
type basicAuthFilter struct {
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
// HTTPHeaderFilters holds list of headers and their regexps
|
||||
type HTTPHeaderBasicAuthFilters []basicAuthFilter
|
||||
|
||||
func (h *HTTPHeaderBasicAuthFilters) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
func (h *HTTPHeaderBasicAuthFilters) Set(value string) error {
|
||||
r, err := regexp.Compile(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*h = append(*h, basicAuthFilter{regexp: r})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-header-hash and --http-allow-param-hash options
|
||||
//
|
||||
@@ -75,14 +103,16 @@ func (h *HTTPHashFilters) Set(value string) error {
|
||||
|
||||
f := hashFilter{name: []byte(valArr[0])}
|
||||
|
||||
if strings.Contains(valArr[1], "%") {
|
||||
p, _ := strconv.ParseInt(valArr[1][:len(valArr[1])-1], 0, 0)
|
||||
val := strings.TrimSpace(valArr[1])
|
||||
|
||||
if strings.Contains(val, "%") {
|
||||
p, _ := strconv.ParseInt(val[:len(val)-1], 0, 0)
|
||||
f.percent = uint32(p)
|
||||
} else if strings.Contains(valArr[1], "/") {
|
||||
} else if strings.Contains(val, "/") {
|
||||
// DEPRECATED format
|
||||
var num, den uint64
|
||||
|
||||
fracArr := strings.Split(valArr[1], "/")
|
||||
fracArr := strings.Split(val, "/")
|
||||
num, _ = strconv.ParseUint(fracArr[0], 10, 64)
|
||||
den, _ = strconv.ParseUint(fracArr[1], 10, 64)
|
||||
|
||||
@@ -193,6 +223,42 @@ func (r *UrlRewriteMap) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-rewrite-header option
|
||||
//
|
||||
type headerRewrite struct {
|
||||
header []byte
|
||||
src *regexp.Regexp
|
||||
target []byte
|
||||
}
|
||||
|
||||
type HeaderRewriteMap []headerRewrite
|
||||
|
||||
func (r *HeaderRewriteMap) String() string {
|
||||
return fmt.Sprint(*r)
|
||||
}
|
||||
|
||||
func (r *HeaderRewriteMap) Set(value string) error {
|
||||
headerArr := strings.SplitN(value, ":", 2)
|
||||
if len(headerArr) < 2 {
|
||||
return errors.New("need both header, regexp and rewrite target, colon-delimited (ex. Header: regexp,target)")
|
||||
}
|
||||
|
||||
header := headerArr[0]
|
||||
valArr := strings.SplitN(strings.TrimSpace(headerArr[1]), ",", 2)
|
||||
|
||||
if len(valArr) < 2 {
|
||||
return errors.New("need both header, regexp and rewrite target, colon-delimited (ex. Header: regexp,target)")
|
||||
}
|
||||
|
||||
regexp, err := regexp.Compile(valArr[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*r = append(*r, headerRewrite{header: []byte(header), src: regexp, target: []byte(valArr[1])})
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-url option
|
||||
//
|
||||
|
||||
+74
-1
@@ -2,8 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/buger/gor/proto"
|
||||
"testing"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
func TestHTTPModifierWithoutConfig(t *testing.T) {
|
||||
@@ -64,6 +65,57 @@ func TestHTTPModifierHeaderNegativeFilters(t *testing.T) {
|
||||
if len(modifier.Rewrite(payload)) != 0 {
|
||||
t.Error("Request should not pass filters")
|
||||
}
|
||||
|
||||
filters = HTTPHeaderFilters{}
|
||||
// Setting filter that not match our header
|
||||
filters.Set("Host: www*")
|
||||
|
||||
modifier = NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerNegativeFilters: filters,
|
||||
})
|
||||
|
||||
if len(modifier.Rewrite(payload)) != 0 {
|
||||
t.Error("Request should not pass filters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPHeaderBasicAuthFilters(t *testing.T) {
|
||||
filters := HTTPHeaderBasicAuthFilters{}
|
||||
filters.Set("^customer[0-9].*")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerBasicAuthFilters: filters,
|
||||
})
|
||||
|
||||
//Encoded UserId:Password = customer3:welcome
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nAuthorization: Basic Y3VzdG9tZXIzOndlbGNvbWU=\r\n\r\na=1&b=2")
|
||||
if len(modifier.Rewrite(payload)) == 0 {
|
||||
t.Error("Request should pass filters")
|
||||
}
|
||||
|
||||
//customer6:rest@123^TEST
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 88\r\nAuthorization: Basic Y3VzdG9tZXI2OnJlc3RAMTIzXlRFU1Q==\r\n\r\na=1&b=2")
|
||||
if len(modifier.Rewrite(payload)) == 0 {
|
||||
t.Error("Request should pass filters")
|
||||
}
|
||||
|
||||
filters = HTTPHeaderBasicAuthFilters{}
|
||||
// Setting filter that not match our header
|
||||
filters.Set("^(homer simpson|mickey mouse).*")
|
||||
|
||||
modifier = NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerBasicAuthFilters: filters,
|
||||
})
|
||||
|
||||
if len(modifier.Rewrite(payload)) != 0 {
|
||||
t.Error("Request should not pass filters")
|
||||
}
|
||||
|
||||
//mickey mouse:happy123
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 88\r\nAuthorization: Basic bWlja2V5IG1vdXNlOmhhcHB5MTIz\r\n\r\na=1&b=2")
|
||||
if len(modifier.Rewrite(payload)) == 0 {
|
||||
t.Error("Request should pass filters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierURLRewrite(t *testing.T) {
|
||||
@@ -95,6 +147,27 @@ func TestHTTPModifierURLRewrite(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierHeaderRewrite(t *testing.T) {
|
||||
var header, newHeader []byte
|
||||
|
||||
rewrites := HeaderRewriteMap{}
|
||||
payload := []byte("GET / HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
err := rewrites.Set("Host: (.*).w3.org,$1.beta.w3.org")
|
||||
if err != nil {
|
||||
t.Error("Should not error", err)
|
||||
}
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerRewrite: rewrites,
|
||||
})
|
||||
|
||||
header = []byte("www.beta.w3.org")
|
||||
if newHeader = proto.Header(modifier.Rewrite(payload), []byte("Host")); !bytes.Equal(newHeader, header) {
|
||||
t.Error("Request header should have been rewritten, wasn't", string(newHeader), string(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierHeaderHashFilters(t *testing.T) {
|
||||
filters := HTTPHashFilters{}
|
||||
filters.Set("Header2:1/2")
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io/ioutil"
|
||||
"net/http/httputil"
|
||||
"strconv"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
func prettifyHTTP(p []byte) []byte {
|
||||
headSize := bytes.IndexByte(p, '\n') + 1
|
||||
head := p[:headSize]
|
||||
body := p[headSize:]
|
||||
|
||||
headersPos := proto.MIMEHeadersEndPos(body)
|
||||
|
||||
if headersPos < 5 || headersPos > len(body) {
|
||||
return p
|
||||
}
|
||||
|
||||
headers := body[:headersPos]
|
||||
content := body[headersPos:]
|
||||
|
||||
var tEnc, cEnc []byte
|
||||
proto.ParseHeaders([][]byte{headers}, func(header, value []byte) bool {
|
||||
if proto.HeadersEqual(header, []byte("Transfer-Encoding")) {
|
||||
tEnc = value
|
||||
}
|
||||
|
||||
if proto.HeadersEqual(header, []byte("Content-Encoding")) {
|
||||
cEnc = value
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if len(tEnc) == 0 && len(cEnc) == 0 {
|
||||
return p
|
||||
}
|
||||
|
||||
if bytes.Equal(tEnc, []byte("chunked")) {
|
||||
buf := bytes.NewBuffer(content)
|
||||
r := httputil.NewChunkedReader(buf)
|
||||
content, _ = ioutil.ReadAll(r)
|
||||
|
||||
headers = proto.DeleteHeader(headers, []byte("Transfer-Encoding"))
|
||||
|
||||
newLen := strconv.Itoa(len(content))
|
||||
headers = proto.SetHeader(headers, []byte("Content-Length"), []byte(newLen))
|
||||
}
|
||||
|
||||
if bytes.Equal(cEnc, []byte("gzip")) {
|
||||
buf := bytes.NewBuffer(content)
|
||||
g, err := gzip.NewReader(buf)
|
||||
|
||||
if err != nil {
|
||||
Debug("[Prettifier] GZIP encoding error:", err)
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
content, _ = ioutil.ReadAll(g)
|
||||
|
||||
headers = proto.DeleteHeader(headers, []byte("Content-Encoding"))
|
||||
|
||||
newLen := strconv.Itoa(len(content))
|
||||
headers = proto.SetHeader(headers, []byte("Content-Length"), []byte(newLen))
|
||||
}
|
||||
|
||||
newPayload := append(append(head, headers...), content...)
|
||||
|
||||
return newPayload
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPPrettifierGzip(t *testing.T) {
|
||||
b := bytes.NewBufferString("")
|
||||
w := gzip.NewWriter(b)
|
||||
w.Write([]byte("test"))
|
||||
w.Close()
|
||||
|
||||
size := strconv.Itoa(len(b.Bytes()))
|
||||
|
||||
payload := []byte("2 1 1\nHTTP/1.1 200 OK\r\nContent-Length: " + size + "\r\nContent-Encoding: gzip\r\n\r\n")
|
||||
payload = append(payload, b.Bytes()...)
|
||||
|
||||
newPayload := prettifyHTTP(payload)
|
||||
|
||||
if string(newPayload) != "2 1 1\nHTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ntest" {
|
||||
t.Error("Payload not match:", string(newPayload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPPrettifierChunked(t *testing.T) {
|
||||
payload := []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")
|
||||
|
||||
newPayload := prettifyHTTP(payload)
|
||||
|
||||
if string(newPayload) != "POST / HTTP/1.1\r\nHost: www.w3.org\r\nContent-Length: 23\r\n\r\nWikipedia in\r\n\r\nchunks." {
|
||||
t.Error("Payload not match:", string(newPayload))
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -34,10 +34,10 @@ func (i *DummyInput) emit() {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
uuid := uuid()
|
||||
reqh := payloadHeader(RequestPayload, uuid, time.Now().UnixNano())
|
||||
i.data <- append(reqh, []byte("POST /pub/WWW/å HTTP/1.1\r\nHost: www.w3.org\r\nUser-Agent: Go 1.1 package http\r\nAccept-Encoding: gzip\r\nContent-Length: 7\r\n\r\na=1&b=2")...)
|
||||
reqh := payloadHeader(RequestPayload, uuid, time.Now().UnixNano(), -1)
|
||||
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)
|
||||
resh := payloadHeader(ResponsePayload, uuid, time.Now().UnixNano()+1, 1)
|
||||
i.data <- append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...)
|
||||
}
|
||||
}
|
||||
|
||||
+238
-42
@@ -2,47 +2,198 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
type fileInputReader struct {
|
||||
reader *bufio.Reader
|
||||
data []byte
|
||||
file io.ReadCloser
|
||||
timestamp int64
|
||||
closed int32 // Value of 0 indicates that the file is still open.
|
||||
s3 bool
|
||||
}
|
||||
|
||||
func (f *fileInputReader) parseNext() error {
|
||||
payloadSeparatorAsBytes := []byte(payloadSeparator)
|
||||
var buffer bytes.Buffer
|
||||
|
||||
for {
|
||||
line, err := f.reader.ReadBytes('\n')
|
||||
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if bytes.Equal(payloadSeparatorAsBytes[1:], line) {
|
||||
asBytes := buffer.Bytes()
|
||||
meta := payloadMeta(asBytes)
|
||||
|
||||
f.timestamp, _ = strconv.ParseInt(string(meta[2]), 10, 64)
|
||||
f.data = asBytes[:len(asBytes)-1]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
buffer.Write(line)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fileInputReader) ReadPayload() []byte {
|
||||
defer f.parseNext()
|
||||
|
||||
return f.data
|
||||
}
|
||||
func (f *fileInputReader) Close() error {
|
||||
if atomic.LoadInt32(&f.closed) == 0 {
|
||||
atomic.StoreInt32(&f.closed, 1)
|
||||
f.file.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewFileInputReader(path string) *fileInputReader {
|
||||
var file io.ReadCloser
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(path, "s3://") {
|
||||
file = NewS3ReadCloser(path)
|
||||
} else {
|
||||
file, err = os.Open(path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
r := &fileInputReader{file: file, closed: 0}
|
||||
if strings.HasSuffix(path, ".gz") {
|
||||
gzReader, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil
|
||||
}
|
||||
r.reader = bufio.NewReader(gzReader)
|
||||
} else {
|
||||
r.reader = bufio.NewReader(file)
|
||||
}
|
||||
|
||||
r.parseNext()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// FileInput can read requests generated by FileOutput
|
||||
type FileInput struct {
|
||||
mu sync.Mutex
|
||||
data chan []byte
|
||||
exit chan bool
|
||||
path string
|
||||
file *os.File
|
||||
readers []*fileInputReader
|
||||
speedFactor float64
|
||||
loop bool
|
||||
}
|
||||
|
||||
// NewFileInput constructor for FileInput. Accepts file path as argument.
|
||||
func NewFileInput(path string) (i *FileInput) {
|
||||
func NewFileInput(path string, loop bool) (i *FileInput) {
|
||||
i = new(FileInput)
|
||||
i.data = make(chan []byte)
|
||||
i.data = make(chan []byte, 1000)
|
||||
i.exit = make(chan bool)
|
||||
i.path = path
|
||||
i.speedFactor = 1
|
||||
i.init(path)
|
||||
i.loop = loop
|
||||
|
||||
if err := i.init(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go i.emit()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *FileInput) init(path string) {
|
||||
file, err := os.Open(path)
|
||||
func (i *FileInput) init() (err error) {
|
||||
defer i.mu.Unlock()
|
||||
i.mu.Lock()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(i, "Cannot open file %q. Error: %s", path, err)
|
||||
var matches []string
|
||||
|
||||
if strings.HasPrefix(i.path, "s3://") {
|
||||
sess := session.Must(session.NewSession(awsConfig()))
|
||||
svc := s3.New(sess)
|
||||
|
||||
bucket, key := parseS3Url(i.path)
|
||||
|
||||
params := &s3.ListObjectsInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Prefix: aws.String(key),
|
||||
}
|
||||
|
||||
resp, err := svc.ListObjects(params)
|
||||
if err != nil {
|
||||
log.Println("Error while retreiving list of files from S3", i.path, err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, c := range resp.Contents {
|
||||
matches = append(matches, "s3://"+bucket+"/"+(*c.Key))
|
||||
}
|
||||
} else {
|
||||
if matches, err = filepath.Glob(i.path); err != nil {
|
||||
log.Println("Wrong file pattern", i.path, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
i.file = file
|
||||
if len(matches) == 0 {
|
||||
log.Println("No files match pattern: ", i.path)
|
||||
return errors.New("No matching files")
|
||||
}
|
||||
|
||||
i.readers = make([]*fileInputReader, len(matches))
|
||||
|
||||
for idx, p := range matches {
|
||||
i.readers[idx] = NewFileInputReader(p)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *FileInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
var buf []byte
|
||||
select {
|
||||
case <-i.exit:
|
||||
return 0, ErrorStopped
|
||||
case buf = <-i.data:
|
||||
}
|
||||
copy(data, buf)
|
||||
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
@@ -50,39 +201,84 @@ func (i *FileInput) String() string {
|
||||
return "File input: " + i.path
|
||||
}
|
||||
|
||||
func (i *FileInput) emit() {
|
||||
var lastTime int64
|
||||
|
||||
// reader := bufio.NewReader(conn)
|
||||
scanner := bufio.NewScanner(i.file)
|
||||
scanner.Split(payloadScanner)
|
||||
|
||||
for scanner.Scan() {
|
||||
buf := scanner.Bytes()
|
||||
meta := payloadMeta(buf)
|
||||
|
||||
if len(meta) > 2 && meta[0][0] == RequestPayload {
|
||||
ts, _ := strconv.ParseInt(string(meta[2]), 10, 64)
|
||||
|
||||
if lastTime != 0 {
|
||||
timeDiff := ts - lastTime
|
||||
|
||||
if i.speedFactor != 1 {
|
||||
timeDiff = int64(float64(timeDiff) / i.speedFactor)
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(timeDiff))
|
||||
}
|
||||
|
||||
lastTime = ts
|
||||
// Find reader with smallest timestamp e.g next payload in row
|
||||
func (i *FileInput) nextReader() (next *fileInputReader) {
|
||||
for _, r := range i.readers {
|
||||
if r == nil || atomic.LoadInt32(&r.closed) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// scanner returs only pointer, so to remove data-race we have to allocate new array
|
||||
newBuf := make([]byte, len(buf))
|
||||
copy(newBuf, buf)
|
||||
|
||||
i.data <- newBuf
|
||||
if next == nil || r.timestamp < next.timestamp {
|
||||
next = r
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("FileInput: end of file")
|
||||
return
|
||||
}
|
||||
|
||||
func (i *FileInput) emit() {
|
||||
var lastTime int64 = -1
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-i.exit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
reader := i.nextReader()
|
||||
|
||||
if reader == nil {
|
||||
if i.loop {
|
||||
i.init()
|
||||
lastTime = -1
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lastTime != -1 {
|
||||
diff := reader.timestamp - lastTime
|
||||
lastTime = reader.timestamp
|
||||
|
||||
if i.speedFactor != 1 {
|
||||
diff = int64(float64(diff) / i.speedFactor)
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(diff))
|
||||
} else {
|
||||
lastTime = reader.timestamp
|
||||
}
|
||||
|
||||
// Recheck if we have exited since last check.
|
||||
select {
|
||||
case <-i.exit:
|
||||
return
|
||||
default:
|
||||
i.data <- reader.ReadPayload()
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("FileInput: end of file '%s'\n", i.path)
|
||||
|
||||
// For now having fixed timeout is temporary solution
|
||||
// Further should be modified, so outputs can report if their queue empty or not
|
||||
time.Sleep(time.Second)
|
||||
if closeCh != nil {
|
||||
close(closeCh)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *FileInput) Close() error {
|
||||
defer i.mu.Unlock()
|
||||
i.mu.Lock()
|
||||
|
||||
close(i.exit)
|
||||
for _, r := range i.readers {
|
||||
r.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
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 := &InOutPlugins{
|
||||
Inputs: requestGenerator.inputs,
|
||||
Outputs: []io.Writer{output, outputFile},
|
||||
}
|
||||
for _, input := range requestGenerator.inputs {
|
||||
plugins.All = append(plugins.All, input)
|
||||
}
|
||||
plugins.All = append(plugins.All, output, outputFile)
|
||||
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
requestGenerator.emit()
|
||||
requestGenerator.wg.Wait()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
emitter.Close()
|
||||
|
||||
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 := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
wg.Add(count)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
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")
|
||||
}
|
||||
emitter.close()
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func Duplicate(data []byte) (duplicate []byte) {
|
||||
duplicate = make([]byte, len(data))
|
||||
copy(duplicate, data)
|
||||
|
||||
return
|
||||
}
|
||||
+14
-2
@@ -13,6 +13,7 @@ type HTTPInput struct {
|
||||
data chan []byte
|
||||
address string
|
||||
listener net.Listener
|
||||
stop chan bool // Channel used only to indicate goroutine should shutdown
|
||||
}
|
||||
|
||||
// NewHTTPInput constructor for HTTPInput. Accepts address with port which he will listen on.
|
||||
@@ -20,6 +21,7 @@ func NewHTTPInput(address string) (i *HTTPInput) {
|
||||
i = new(HTTPInput)
|
||||
i.data = make(chan []byte, 10000)
|
||||
i.address = address
|
||||
i.stop = make(chan bool)
|
||||
|
||||
i.listen(address)
|
||||
|
||||
@@ -27,9 +29,14 @@ func NewHTTPInput(address string) (i *HTTPInput) {
|
||||
}
|
||||
|
||||
func (i *HTTPInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
var buf []byte
|
||||
select {
|
||||
case <-i.stop:
|
||||
return 0, ErrorStopped
|
||||
case buf = <-i.data:
|
||||
}
|
||||
|
||||
header := payloadHeader(RequestPayload, uuid(), time.Now().UnixNano())
|
||||
header := payloadHeader(RequestPayload, uuid(), time.Now().UnixNano(), -1)
|
||||
|
||||
copy(data[0:len(header)], header)
|
||||
copy(data[len(header):], buf)
|
||||
@@ -37,6 +44,11 @@ func (i *HTTPInput) Read(data []byte) (int, error) {
|
||||
return len(buf) + len(header), nil
|
||||
}
|
||||
|
||||
func (i *HTTPInput) Close() error {
|
||||
close(i.stop)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *HTTPInput) handler(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Scheme = "http"
|
||||
r.URL.Host = i.listener.Addr().String()
|
||||
|
||||
+19
-12
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/buger/gor/proto"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -9,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
func TestHTTPInput(t *testing.T) {
|
||||
@@ -20,10 +21,14 @@ func TestHTTPInput(t *testing.T) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
address := strings.Replace(input.listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
|
||||
@@ -33,16 +38,14 @@ func TestHTTPInput(t *testing.T) {
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
func TestInputHTTPLargePayload(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
// Generate 1000kb file
|
||||
dd := exec.Command("dd", "if=/dev/urandom", "of=/tmp/large", "bs=1MB", "count=4")
|
||||
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)
|
||||
@@ -55,10 +58,14 @@ func TestInputHTTPLargePayload(t *testing.T) {
|
||||
}
|
||||
wg.Done()
|
||||
})
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
wg.Add(1)
|
||||
address := strings.Replace(input.listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
@@ -69,5 +76,5 @@ func TestInputHTTPLargePayload(t *testing.T) {
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/Shopify/sarama"
|
||||
"github.com/Shopify/sarama/mocks"
|
||||
)
|
||||
|
||||
// KafkaInput is used for recieving Kafka messages and
|
||||
// transforming them into HTTP payloads.
|
||||
type KafkaInput struct {
|
||||
config *KafkaConfig
|
||||
consumers []sarama.PartitionConsumer
|
||||
messages chan *sarama.ConsumerMessage
|
||||
}
|
||||
|
||||
// NewKafkaInput creates instance of kafka consumer client.
|
||||
func NewKafkaInput(address string, config *KafkaConfig) *KafkaInput {
|
||||
c := sarama.NewConfig()
|
||||
// Configuration options go here
|
||||
|
||||
var con sarama.Consumer
|
||||
|
||||
if mock, ok := config.consumer.(*mocks.Consumer); ok && mock != nil {
|
||||
con = config.consumer
|
||||
} else {
|
||||
var err error
|
||||
//con, err = sarama.NewConsumer([]string{config.host}, c)
|
||||
con, err = sarama.NewConsumer(strings.Split(config.host, ","), c)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to start Sarama(Kafka) consumer:", err)
|
||||
}
|
||||
}
|
||||
|
||||
partitions, err := con.Partitions(config.topic)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to collect Sarama(Kafka) partitions:", err)
|
||||
}
|
||||
|
||||
i := &KafkaInput{
|
||||
config: config,
|
||||
consumers: make([]sarama.PartitionConsumer, len(partitions)),
|
||||
messages: make(chan *sarama.ConsumerMessage, 256),
|
||||
}
|
||||
|
||||
for index, partition := range partitions {
|
||||
consumer, err := con.ConsumePartition(config.topic, partition, sarama.OffsetNewest)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to start Sarama(Kafka) partition consumer:", err)
|
||||
}
|
||||
|
||||
go func(consumer sarama.PartitionConsumer) {
|
||||
defer consumer.Close()
|
||||
|
||||
for message := range consumer.Messages() {
|
||||
i.messages <- message
|
||||
}
|
||||
}(consumer)
|
||||
|
||||
if Settings.Verbose {
|
||||
// Start infinite loop for tracking errors for kafka producer.
|
||||
go i.ErrorHandler(consumer)
|
||||
}
|
||||
|
||||
i.consumers[index] = consumer
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// ErrorHandler should receive errors
|
||||
func (i *KafkaInput) ErrorHandler(consumer sarama.PartitionConsumer) {
|
||||
for err := range consumer.Errors() {
|
||||
log.Println("Failed to read access log entry:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *KafkaInput) Read(data []byte) (int, error) {
|
||||
message := <-i.messages
|
||||
|
||||
if !i.config.useJSON {
|
||||
copy(data, message.Value)
|
||||
return len(message.Value), nil
|
||||
}
|
||||
|
||||
var kafkaMessage KafkaMessage
|
||||
json.Unmarshal(message.Value, &kafkaMessage)
|
||||
|
||||
buf, err := kafkaMessage.Dump()
|
||||
if err != nil {
|
||||
log.Println("Failed to decode access log entry:", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
copy(data, buf)
|
||||
|
||||
return len(buf), nil
|
||||
|
||||
}
|
||||
|
||||
func (i *KafkaInput) String() string {
|
||||
return "Kafka Input: " + i.config.host + "/" + i.config.topic
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Shopify/sarama"
|
||||
"github.com/Shopify/sarama/mocks"
|
||||
)
|
||||
|
||||
func TestInputKafkaRAW(t *testing.T) {
|
||||
consumer := mocks.NewConsumer(t, nil)
|
||||
defer consumer.Close()
|
||||
|
||||
consumer.ExpectConsumePartition("test", 0, mocks.AnyOffset).YieldMessage(&sarama.ConsumerMessage{Value: []byte("1 2 3\nGET / HTTP1.1\r\nHeader: 1\r\n\r\n")})
|
||||
consumer.SetTopicMetadata(
|
||||
map[string][]int32{"test": {0}},
|
||||
)
|
||||
|
||||
input := NewKafkaInput("", &KafkaConfig{
|
||||
consumer: consumer,
|
||||
topic: "test",
|
||||
useJSON: false,
|
||||
})
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := input.Read(buf)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(buf[:n]) != "1 2 3\nGET / HTTP1.1\r\nHeader: 1\r\n\r\n" {
|
||||
t.Error("Message not properly decoded: ", string(buf[:n]), n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputKafkaJSON(t *testing.T) {
|
||||
consumer := mocks.NewConsumer(t, nil)
|
||||
defer consumer.Close()
|
||||
|
||||
consumer.ExpectConsumePartition("test", 0, mocks.AnyOffset).YieldMessage(&sarama.ConsumerMessage{Value: []byte(`{"Req_URL":"/","Req_Type":"1","Req_ID":"2","Req_Ts":"3","Req_Method":"GET","Req_Headers":{"Header":"1"}}`)})
|
||||
consumer.SetTopicMetadata(
|
||||
map[string][]int32{"test": {0}},
|
||||
)
|
||||
|
||||
input := NewKafkaInput("", &KafkaConfig{
|
||||
consumer: consumer,
|
||||
topic: "test",
|
||||
useJSON: true,
|
||||
})
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := input.Read(buf)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(buf[:n]) != "1 2 3\nGET / HTTP/1.1\r\nHeader: 1\r\n\r\n" {
|
||||
t.Error("Message not properly decoded: ", string(buf[:n]), n)
|
||||
}
|
||||
}
|
||||
+70
-30
@@ -1,45 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
raw "github.com/buger/gor/raw_socket_listener"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
raw "github.com/buger/goreplay/capture"
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
// RAWInput used for intercepting traffic for given address
|
||||
type RAWInput struct {
|
||||
data chan *raw.TCPMessage
|
||||
address string
|
||||
expire time.Duration
|
||||
quit chan bool
|
||||
listener *raw.Listener
|
||||
data chan *raw.TCPMessage
|
||||
address string
|
||||
expire time.Duration
|
||||
quit chan bool // Channel used only to indicate goroutine should shutdown
|
||||
engine int
|
||||
realIPHeader []byte
|
||||
trackResponse bool
|
||||
listener *raw.Listener
|
||||
protocol raw.TCPProtocol
|
||||
bpfFilter string
|
||||
timestampType string
|
||||
bufferSize int64
|
||||
}
|
||||
|
||||
// Available engines for intercepting traffic
|
||||
const (
|
||||
EngineRawSocket = 1 << iota
|
||||
EnginePcap
|
||||
EnginePcapFile
|
||||
)
|
||||
|
||||
// NewRAWInput constructor for RAWInput. Accepts address with port as argument.
|
||||
func NewRAWInput(address string, expire time.Duration) (i *RAWInput) {
|
||||
func NewRAWInput(address string, engine int, trackResponse bool, expire time.Duration, realIPHeader string, protocol string, bpfFilter string, timestampType string, bufferSize int64) (i *RAWInput) {
|
||||
i = new(RAWInput)
|
||||
i.data = make(chan *raw.TCPMessage)
|
||||
i.address = address
|
||||
i.expire = expire
|
||||
i.engine = engine
|
||||
i.bpfFilter = bpfFilter
|
||||
i.realIPHeader = []byte(realIPHeader)
|
||||
i.quit = make(chan bool)
|
||||
i.trackResponse = trackResponse
|
||||
i.timestampType = timestampType
|
||||
i.bufferSize = bufferSize
|
||||
|
||||
go i.listen(address)
|
||||
switch protocol {
|
||||
case "http":
|
||||
i.protocol = raw.ProtocolHTTP
|
||||
case "binary":
|
||||
i.protocol = raw.ProtocolBinary
|
||||
if !PRO {
|
||||
log.Fatal("Binary protocols can be used only with PRO license")
|
||||
}
|
||||
default:
|
||||
log.Fatal("Unsupported protocol:", protocol)
|
||||
}
|
||||
|
||||
i.listen(address)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *RAWInput) Read(data []byte) (int, error) {
|
||||
msg := <-i.data
|
||||
var msg *raw.TCPMessage
|
||||
select {
|
||||
case <-i.quit:
|
||||
return 0, ErrorStopped
|
||||
case msg = <-i.data:
|
||||
}
|
||||
|
||||
buf := msg.Bytes()
|
||||
|
||||
var header []byte
|
||||
|
||||
if msg.IsIncoming {
|
||||
header = payloadHeader(RequestPayload, msg.UUID(), msg.Start.UnixNano())
|
||||
header = payloadHeader(RequestPayload, msg.UUID(), msg.Start.UnixNano(), -1)
|
||||
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.RequestStart.UnixNano())
|
||||
header = payloadHeader(ResponsePayload, msg.UUID(), msg.Start.UnixNano(), msg.End.UnixNano()-msg.AssocMessage.End.UnixNano())
|
||||
}
|
||||
|
||||
copy(data[0:len(header)], header)
|
||||
@@ -49,37 +91,35 @@ func (i *RAWInput) Read(data []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (i *RAWInput) listen(address string) {
|
||||
address = strings.Replace(address, "[::]", "127.0.0.1", -1)
|
||||
|
||||
Debug("Listening for traffic on: " + address)
|
||||
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("input-raw: error while parsing address", err)
|
||||
log.Fatalf("input-raw: error while parsing address: %s", err)
|
||||
}
|
||||
|
||||
i.listener = raw.NewListener(host, port, i.expire)
|
||||
i.listener = raw.NewListener(host, port, i.engine, i.trackResponse, i.expire, i.protocol, i.bpfFilter, i.timestampType, i.bufferSize, Settings.InputRAWOverrideSnapLen, Settings.InputRAWImmediateMode)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-i.quit:
|
||||
return
|
||||
default:
|
||||
ch := i.listener.Receiver()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-i.quit:
|
||||
return
|
||||
case i.data <- <-ch: // Receiving TCPMessage object
|
||||
}
|
||||
}
|
||||
|
||||
// Receiving TCPMessage object
|
||||
m := i.listener.Receive()
|
||||
|
||||
i.data <- m
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *RAWInput) String() string {
|
||||
return "RAW Socket input: " + i.address
|
||||
return "Intercepting traffic from: " + i.address
|
||||
}
|
||||
|
||||
func (i *RAWInput) Close() {
|
||||
// Close closes the input raw listener
|
||||
func (i *RAWInput) Close() error {
|
||||
i.listener.Close()
|
||||
close(i.quit)
|
||||
return nil
|
||||
}
|
||||
|
||||
+255
-42
@@ -5,31 +5,160 @@ import (
|
||||
"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"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
const testRawExpire = time.Millisecond * 200
|
||||
|
||||
func TestRAWInput(t *testing.T) {
|
||||
func TestRAWInputIPv4(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
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)
|
||||
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, testRawExpire)
|
||||
input := NewRAWInput(originAddr, EnginePcap, true, testRawExpire, "X-Real-IP", "http", "", "", 0)
|
||||
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 := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
client := NewHTTPClient("http://"+listener.Addr().String(), &HTTPClientConfig{})
|
||||
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
// request + response
|
||||
wg.Add(2)
|
||||
client.Get("/")
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
func TestRAWInputNoKeepAlive(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) {
|
||||
w.Write([]byte("a"))
|
||||
w.Write([]byte("b"))
|
||||
}),
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
origin.SetKeepAlivesEnabled(false)
|
||||
go origin.Serve(listener)
|
||||
defer listener.Close()
|
||||
|
||||
originAddr := listener.Addr().String()
|
||||
|
||||
input := NewRAWInput(originAddr, EnginePcap, true, testRawExpire, "", "http", "", "", 0)
|
||||
defer input.Close()
|
||||
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
client := NewHTTPClient("http://"+listener.Addr().String(), &HTTPClientConfig{})
|
||||
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
// request + response
|
||||
wg.Add(2)
|
||||
client.Get("/")
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
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, "", "http", "", "", 0)
|
||||
defer input.Close()
|
||||
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
@@ -39,37 +168,40 @@ func TestRAWInput(t *testing.T) {
|
||||
atomic.AddInt64(&respCounter, 1)
|
||||
}
|
||||
|
||||
if Settings.debug {
|
||||
if Settings.Debug {
|
||||
log.Println(reqCounter, respCounter)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
client := NewHTTPClient(origin.URL, &HTTPClientConfig{})
|
||||
client := NewHTTPClient("http://"+listener.Addr().String(), &HTTPClientConfig{})
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
// request + response
|
||||
wg.Add(2)
|
||||
client.Get("/")
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
func TestInputRAW100Expect(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
fileContent, _ := ioutil.ReadFile("LICENSE.txt")
|
||||
fileContent, _ := ioutil.ReadFile("COMM-LICENSE")
|
||||
|
||||
// Origing and Replay server initialization
|
||||
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -81,7 +213,7 @@ func TestInputRAW100Expect(t *testing.T) {
|
||||
|
||||
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
|
||||
input := NewRAWInput(originAddr, time.Second)
|
||||
input := NewRAWInput(originAddr, EnginePcap, true, time.Second, "", "http", "", "", 0)
|
||||
defer input.Close()
|
||||
|
||||
// We will use it to get content of raw HTTP request
|
||||
@@ -112,21 +244,25 @@ func TestInputRAW100Expect(t *testing.T) {
|
||||
|
||||
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{testOutput, httpOutput}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{testOutput, httpOutput},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, testOutput, httpOutput)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
// Origin + Response/Request Test Output + Request Http Output
|
||||
wg.Add(4)
|
||||
curl := exec.Command("curl", "http://"+originAddr, "--data-binary", "@LICENSE.txt")
|
||||
curl := exec.Command("curl", "http://"+originAddr, "--data-binary", "@COMM-LICENSE")
|
||||
err := curl.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
func TestInputRAWChunkedEncoding(t *testing.T) {
|
||||
@@ -144,7 +280,7 @@ func TestInputRAWChunkedEncoding(t *testing.T) {
|
||||
}))
|
||||
|
||||
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
input := NewRAWInput(originAddr, time.Second)
|
||||
input := NewRAWInput(originAddr, EnginePcap, true, time.Second, "", "http", "", "", 0)
|
||||
defer input.Close()
|
||||
|
||||
replay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -162,22 +298,24 @@ func TestInputRAWChunkedEncoding(t *testing.T) {
|
||||
|
||||
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{Debug: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{httpOutput}
|
||||
|
||||
go Start(quit)
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{httpOutput},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, httpOutput)
|
||||
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
wg.Add(2)
|
||||
|
||||
curl := exec.Command("curl", "http://"+originAddr, "--header", "Transfer-Encoding: chunked", "--data-binary", "@README.md")
|
||||
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)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
func TestInputRAWLargePayload(t *testing.T) {
|
||||
@@ -187,9 +325,10 @@ func TestInputRAWLargePayload(t *testing.T) {
|
||||
}
|
||||
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=1KB", "count=100")
|
||||
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)
|
||||
@@ -199,7 +338,7 @@ func TestInputRAWLargePayload(t *testing.T) {
|
||||
defer req.Body.Close()
|
||||
body, _ := ioutil.ReadAll(req.Body)
|
||||
|
||||
if len(body) != 100*1000 {
|
||||
if len(body) != sizeB {
|
||||
t.Error("File size should be 1mb:", len(body))
|
||||
}
|
||||
|
||||
@@ -207,17 +346,18 @@ func TestInputRAWLargePayload(t *testing.T) {
|
||||
}))
|
||||
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
|
||||
input := NewRAWInput(originAddr, time.Second)
|
||||
input := NewRAWInput(originAddr, EnginePcap, true, testRawExpire, "", "http", "", "", 0)
|
||||
defer input.Close()
|
||||
|
||||
replay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
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]
|
||||
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) != 100*1000 {
|
||||
t.Error("File size should be 100000 bytes:", len(body))
|
||||
if len(body) != sizeB {
|
||||
t.Errorf("File size should be %d bytes: %d", sizeB, len(body))
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
@@ -226,18 +366,91 @@ func TestInputRAWLargePayload(t *testing.T) {
|
||||
|
||||
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{Debug: false})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{httpOutput}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{httpOutput},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, httpOutput)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
wg.Add(2)
|
||||
curl := exec.Command("curl", "http://"+originAddr, "--header", "Transfer-Encoding: chunked", "--data-binary", "@/tmp/large")
|
||||
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)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
func BenchmarkRAWInput(b *testing.B) {
|
||||
var respCounter, reqCounter, replayCounter int64
|
||||
|
||||
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)
|
||||
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddInt64(&replayCounter, 1)
|
||||
}))
|
||||
defer origin.Close()
|
||||
upstreamAddr := strings.Replace(upstream.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
|
||||
input := NewRAWInput(originAddr, EnginePcap, true, testRawExpire, "", "http", "", "", 0)
|
||||
defer input.Close()
|
||||
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
if data[0] == '1' {
|
||||
atomic.AddInt64(&reqCounter, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&respCounter, 1)
|
||||
}
|
||||
})
|
||||
|
||||
httpOutput := NewLimiter(NewHTTPOutput(upstreamAddr, &HTTPOutputConfig{}), "10%")
|
||||
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output, httpOutput},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output, httpOutput)
|
||||
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
emitted := 0
|
||||
fileContent, _ := ioutil.ReadFile("LICENSE.txt")
|
||||
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(100 * 1000)
|
||||
emitted += 100 * 1000
|
||||
for w := 0; w < 100; w++ {
|
||||
go func() {
|
||||
client := NewHTTPClient(origin.URL, &HTTPClientConfig{})
|
||||
for i := 0; i < 1000; i++ {
|
||||
if i%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", "and replayed", replayCounter)
|
||||
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
+70
-17
@@ -2,7 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
@@ -11,15 +14,25 @@ import (
|
||||
// TCPInput used for internal communication
|
||||
type TCPInput struct {
|
||||
data chan []byte
|
||||
address string
|
||||
listener net.Listener
|
||||
address string
|
||||
config *TCPInputConfig
|
||||
stop chan bool // Channel used only to indicate goroutine should shutdown
|
||||
}
|
||||
|
||||
type TCPInputConfig struct {
|
||||
Secure bool
|
||||
CertificatePath string
|
||||
KeyPath string
|
||||
}
|
||||
|
||||
// NewTCPInput constructor for TCPInput, accepts address with port
|
||||
func NewTCPInput(address string) (i *TCPInput) {
|
||||
func NewTCPInput(address string, config *TCPInputConfig) (i *TCPInput) {
|
||||
i = new(TCPInput)
|
||||
i.data = make(chan []byte)
|
||||
i.data = make(chan []byte, 1000)
|
||||
i.address = address
|
||||
i.config = config
|
||||
i.stop = make(chan bool)
|
||||
|
||||
i.listen(address)
|
||||
|
||||
@@ -27,23 +40,47 @@ func NewTCPInput(address string) (i *TCPInput) {
|
||||
}
|
||||
|
||||
func (i *TCPInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
var buf []byte
|
||||
select {
|
||||
case <-i.stop:
|
||||
return 0, ErrorStopped
|
||||
case 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
|
||||
func (i *TCPInput) Close() error {
|
||||
close(i.stop)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Can't start:", err)
|
||||
func (i *TCPInput) listen(address string) {
|
||||
if i.config.Secure {
|
||||
cer, err := tls.LoadX509KeyPair(i.config.CertificatePath, i.config.KeyPath)
|
||||
if err != nil {
|
||||
log.Fatal("Error while loading --input-file certificate:", err)
|
||||
}
|
||||
|
||||
config := &tls.Config{Certificates: []tls.Certificate{cer}}
|
||||
listener, err := tls.Listen("tcp", address, config)
|
||||
if err != nil {
|
||||
log.Fatal("Can't start --input-tcp with Secure connection:", err)
|
||||
}
|
||||
i.listener = listener
|
||||
} else {
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Fatal("Can't start:", err)
|
||||
}
|
||||
|
||||
i.listener = listener
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
conn, err := i.listener.Accept()
|
||||
|
||||
if err != nil {
|
||||
log.Println("Error while Accept()", err)
|
||||
@@ -58,16 +95,32 @@ func (i *TCPInput) listen(address string) {
|
||||
func (i *TCPInput) handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
payloadSeparatorAsBytes := []byte(payloadSeparator)
|
||||
reader := bufio.NewReader(conn)
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Split(payloadScanner)
|
||||
var buffer bytes.Buffer
|
||||
|
||||
for scanner.Scan() {
|
||||
i.data <- scanner.Bytes()
|
||||
}
|
||||
for {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Unexpected error in input tcp connection:", err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+80
-48
@@ -1,26 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTCPInput(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTCPInput("127.0.0.1:0")
|
||||
input := NewTCPInput("127.0.0.1:0", &TCPInputConfig{})
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", input.listener.Addr().String())
|
||||
|
||||
@@ -43,68 +57,86 @@ func TestTCPInput(t *testing.T) {
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
func BenchmarkTCPInput(b *testing.B) {
|
||||
func genCertificate(template *x509.Certificate) ([]byte, []byte) {
|
||||
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit)
|
||||
template.SerialNumber = serialNumber
|
||||
template.BasicConstraintsValid = true
|
||||
template.NotBefore = time.Now()
|
||||
template.NotAfter = time.Now().Add(time.Hour)
|
||||
|
||||
derBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
|
||||
|
||||
var certPem, keyPem bytes.Buffer
|
||||
pem.Encode(&certPem, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
pem.Encode(&keyPem, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
|
||||
return certPem.Bytes(), keyPem.Bytes()
|
||||
}
|
||||
|
||||
func TestTCPInputSecure(t *testing.T) {
|
||||
serverCertPem, serverPrivPem := genCertificate(&x509.Certificate{
|
||||
DNSNames: []string{"localhost"},
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::")},
|
||||
})
|
||||
|
||||
serverCertPemFile, _ := ioutil.TempFile("", "server.crt")
|
||||
serverCertPemFile.Write(serverCertPem)
|
||||
serverCertPemFile.Close()
|
||||
|
||||
serverPrivPemFile, _ := ioutil.TempFile("", "server.key")
|
||||
serverPrivPemFile.Write(serverPrivPem)
|
||||
serverPrivPemFile.Close()
|
||||
|
||||
defer func() {
|
||||
os.Remove(serverPrivPemFile.Name())
|
||||
os.Remove(serverCertPemFile.Name())
|
||||
}()
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTCPInput("127.0.0.1:0")
|
||||
input := NewTCPInput("127.0.0.1:0", &TCPInputConfig{
|
||||
Secure: true,
|
||||
CertificatePath: serverCertPemFile.Name(),
|
||||
KeyPath: serverPrivPemFile.Name(),
|
||||
})
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", input.listener.Addr().String())
|
||||
conf := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", input.listener.Addr().String(), conf)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var connections []net.Conn
|
||||
msg := []byte("1 1 1\nGET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
// Creating simple pool of workers, same as output_tcp have
|
||||
dataChan := make(chan []byte, 1000)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
conn, _ := net.DialTCP("tcp", nil, tcpAddr)
|
||||
connections = append(connections, conn)
|
||||
|
||||
go func(conn net.Conn) {
|
||||
for {
|
||||
data := <-dataChan
|
||||
|
||||
buf := make([]byte, len(data)+2)
|
||||
data = append(data, []byte("¶")...)
|
||||
copy(buf, data)
|
||||
conn.Write(buf)
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
msg := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
dataChan <- msg
|
||||
conn.Write(msg)
|
||||
conn.Write([]byte(payloadSeparator))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, conn := range connections {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/Shopify/sarama"
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
// KafkaConfig should contains required information to
|
||||
// build producers.
|
||||
type KafkaConfig struct {
|
||||
host string
|
||||
topic string
|
||||
producer sarama.AsyncProducer
|
||||
consumer sarama.Consumer
|
||||
useJSON bool
|
||||
}
|
||||
|
||||
// KafkaMessage should contains catched request information that should be
|
||||
// passed as Json to Apache Kafka.
|
||||
type KafkaMessage struct {
|
||||
ReqURL string `json:"Req_URL"`
|
||||
ReqType string `json:"Req_Type"`
|
||||
ReqID string `json:"Req_ID"`
|
||||
ReqTs string `json:"Req_Ts"`
|
||||
ReqMethod string `json:"Req_Method"`
|
||||
ReqBody string `json:"Req_Body,omitempty"`
|
||||
ReqHeaders map[string]string `json:"Req_Headers,omitempty"`
|
||||
}
|
||||
|
||||
// Dump returns the given request in its HTTP/1.x wire
|
||||
// representation.
|
||||
func (m KafkaMessage) Dump() ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
b.WriteString(fmt.Sprintf("%s %s %s\n", m.ReqType, m.ReqID, m.ReqTs))
|
||||
b.WriteString(fmt.Sprintf("%s %s HTTP/1.1", m.ReqMethod, m.ReqURL))
|
||||
b.Write(proto.CLRF)
|
||||
for key, value := range m.ReqHeaders {
|
||||
b.WriteString(fmt.Sprintf("%s: %s", key, value))
|
||||
b.Write(proto.CLRF)
|
||||
}
|
||||
|
||||
b.Write(proto.CLRF)
|
||||
b.WriteString(m.ReqBody)
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
+14
-3
@@ -77,12 +77,15 @@ func (l *Limiter) Write(data []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
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 r, ok := l.plugin.(io.Reader); ok {
|
||||
n, err = r.Read(data)
|
||||
} else {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if l.isLimited() {
|
||||
return 0, nil
|
||||
@@ -92,5 +95,13 @@ func (l *Limiter) Read(data []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (l *Limiter) String() string {
|
||||
return fmt.Sprintf("Limiting %s to: %d (isPercent: %b)", l.plugin, l.limit, l.isPercent)
|
||||
return fmt.Sprintf("Limiting %s to: %d (isPercent: %v)", l.plugin, l.limit, l.isPercent)
|
||||
}
|
||||
|
||||
// Close closes the resources.
|
||||
func (l *Limiter) Close() error {
|
||||
if fi, ok := l.plugin.(io.ReadCloser); ok {
|
||||
fi.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+32
-20
@@ -18,18 +18,21 @@ func TestOutputLimiter(t *testing.T) {
|
||||
}), "10")
|
||||
wg.Add(10)
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
func TestInputLimiter(t *testing.T) {
|
||||
@@ -42,18 +45,21 @@ func TestInputLimiter(t *testing.T) {
|
||||
})
|
||||
wg.Add(10)
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
input.(*Limiter).plugin.(*TestInput).EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
// Should limit all requests
|
||||
@@ -66,18 +72,21 @@ func TestPercentLimiter1(t *testing.T) {
|
||||
wg.Done()
|
||||
}), "0%")
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
// Should not limit at all
|
||||
@@ -91,16 +100,19 @@ func TestPercentLimiter2(t *testing.T) {
|
||||
}), "100%")
|
||||
wg.Add(100)
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
+63
-26
@@ -21,12 +21,15 @@ type Middleware struct {
|
||||
|
||||
Stdin io.Writer
|
||||
Stdout io.Reader
|
||||
|
||||
stop chan bool // Channel used only to indicate goroutine should shutdown
|
||||
}
|
||||
|
||||
func NewMiddleware(command string) *Middleware {
|
||||
m := new(Middleware)
|
||||
m.command = command
|
||||
m.data = make(chan []byte, 1000)
|
||||
m.stop = make(chan bool)
|
||||
|
||||
commands := strings.Split(command, " ")
|
||||
cmd := exec.Command(commands[0], commands[1:]...)
|
||||
@@ -34,9 +37,7 @@ func NewMiddleware(command string) *Middleware {
|
||||
m.Stdout, _ = cmd.StdoutPipe()
|
||||
m.Stdin, _ = cmd.StdinPipe()
|
||||
|
||||
if Settings.verbose {
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
go m.read(m.Stdout)
|
||||
|
||||
@@ -64,57 +65,93 @@ func (m *Middleware) ReadFrom(plugin io.Reader) {
|
||||
|
||||
func (m *Middleware) copy(to io.Writer, from io.Reader) {
|
||||
buf := make([]byte, 5*1024*1024)
|
||||
dst := make([]byte, len(buf)*2)
|
||||
dst := make([]byte, len(buf)*4)
|
||||
|
||||
for {
|
||||
nr, _ := from.Read(buf)
|
||||
if nr > 0 && len(buf) > nr {
|
||||
if nr == 0 || nr > len(buf) {
|
||||
continue
|
||||
}
|
||||
|
||||
hex.Encode(dst, buf[0:nr])
|
||||
dst[nr*2] = '\n'
|
||||
payload := buf[0:nr]
|
||||
|
||||
m.mu.Lock()
|
||||
to.Write(dst[0 : nr*2+1])
|
||||
m.mu.Unlock()
|
||||
if Settings.PrettifyHTTP {
|
||||
payload = prettifyHTTP(payload)
|
||||
nr = len(payload)
|
||||
|
||||
if Settings.debug {
|
||||
Debug("[MIDDLEWARE-MASTER] Sending:", string(buf[0:nr]), "From:", from)
|
||||
if nr*2 > len(dst) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if Settings.PrettifyHTTP {
|
||||
payload = prettifyHTTP(payload)
|
||||
nr = len(payload)
|
||||
}
|
||||
|
||||
hex.Encode(dst, payload)
|
||||
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) {
|
||||
scanner := bufio.NewScanner(from)
|
||||
reader := bufio.NewReader(from)
|
||||
var line []byte
|
||||
var e error
|
||||
|
||||
for scanner.Scan() {
|
||||
bytes := scanner.Bytes()
|
||||
buf := make([]byte, len(bytes)/2)
|
||||
if _, err := hex.Decode(buf, bytes); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Failed to decode input payload", err, len(bytes))
|
||||
for {
|
||||
if line, e = reader.ReadBytes('\n'); e != nil {
|
||||
if e == io.EOF {
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if Settings.debug {
|
||||
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), string(line[:len(line)-1]))
|
||||
}
|
||||
|
||||
if Settings.Debug {
|
||||
Debug("[MIDDLEWARE-MASTER] Received:", string(buf))
|
||||
}
|
||||
|
||||
m.data <- buf
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Traffic modifier command failed:", err)
|
||||
select {
|
||||
case <-m.stop:
|
||||
return
|
||||
case m.data <- buf:
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Middleware) Read(data []byte) (int, error) {
|
||||
buf := <-m.data
|
||||
copy(data, buf)
|
||||
var buf []byte
|
||||
select {
|
||||
case <-m.stop:
|
||||
return 0, ErrorStopped
|
||||
case 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)
|
||||
}
|
||||
|
||||
func (m *Middleware) Close() error {
|
||||
close(m.stop)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# GoReplay middleware
|
||||
|
||||
GoReplay support protocol for writing middleware in any language, which allows you to implement custom logic like authentification or complex rewriting and filterting. See protocol description here: https://github.com/buger/goreplay/wiki/Middleware, but the basic idea that middleware process receive hex encoded data via STDIN and emits it back via STDOUT. STDERR for loggin inside middleware. Yes, that's simple.
|
||||
|
||||
To simplify middleware creation we provide packages for NodeJS and Go (upcoming).
|
||||
|
||||
If you want to get access to original and replayed responses, do not forget adding `--output-http-track-response` and `--input-raw-track-response` options.
|
||||
|
||||
## NodeJS
|
||||
|
||||
Before starting, you should install the package via npm: `npm install goreplay_middleware`.
|
||||
And initialize middleware the following way:
|
||||
```javascript
|
||||
var gor = require("goreplay_middleware");
|
||||
// `init` will initialize STDIN listener
|
||||
gor.init();
|
||||
```
|
||||
|
||||
Basic idea is that you write callbacks which respond to `request`, `response`, `replay`, or `message` events, which contain request meta information and actuall http paylod. Depending on your needs you may compare, override or filter incoming requests and responses.
|
||||
|
||||
You can respond to the incoming events using `on` function, by providing callbacks:
|
||||
```javascript
|
||||
// valid events are `request`, `response` (original response), `replay` (replayed response), and `message` (all events)
|
||||
gor.on('request', function(data) {
|
||||
// `data` contains incoming message its meta information.
|
||||
data
|
||||
|
||||
// Raw HTTP payload of `Buffer` type
|
||||
// Example (hidden character for line endings shown on purpose):
|
||||
// GET / HTTP/1.1\r\n
|
||||
// User-Agent: Golang\r\n
|
||||
// \r\n
|
||||
data.http
|
||||
|
||||
// Meta is an array size of 4, containing:
|
||||
// 1. request type - 1, 2 or 3 (which maps to `request`, `respose` and `replay`)
|
||||
// 2. uuid - request unique identifier. Request responses have the same ID as their request.
|
||||
// 3. timestamp of when request was made (for responses it is time of request start too)
|
||||
// 4. latency - time difference between request start and finish. For `request` is zero.
|
||||
data.meta
|
||||
|
||||
// Unique request ID. It should be same for `request`, `response` and `replay` events of the same request.
|
||||
data.ID
|
||||
|
||||
// You should return data at the end of function, even if you not changed request, if you do not want to filter it out.
|
||||
// If you just `return` nothing, request will be filtered
|
||||
return data
|
||||
})
|
||||
```
|
||||
### Mapping requests and responses
|
||||
You can provide request ID as additional argument to `on` function, which allow you to map related requests and responses. Below is example of middleware which checks that original and replayed response have same HTTP status code.
|
||||
|
||||
```javascript
|
||||
// Example of very basic way to compare if replayed traffic have no errors
|
||||
gor.on("request", function(req) {
|
||||
gor.on("response", req.ID, function(resp) {
|
||||
gor.on("replay", req.ID, function(repl) {
|
||||
if (gor.httpStatus(resp.http) != gor.httpStatus(repl.http)) {
|
||||
// Note that STDERR is used for logging, and it actually will be send to `Gor` STDOUT.
|
||||
// This trick is used because SDTIN and STDOUT already used for process communication.
|
||||
// You can write logger that writes to files insead.
|
||||
console.error(`${gor.httpPath(req.http)} STATUS NOT MATCH: 'Expected ${gor.httpStatus(resp.http)}' got '${gor.httpStatus(repl.http)}'`)
|
||||
}
|
||||
return repl;
|
||||
})
|
||||
return resp;
|
||||
})
|
||||
return req
|
||||
})
|
||||
```
|
||||
|
||||
This middleware includes `searchResponses` helper which is used to compare value of original response with the replayed response. If authentication system or xsrf protection returns unique tokens in headers or the response, it will be helpful to rewrite your requests based on them. Because tokens are unique, and the value contained in original and replayed responses will be different. So, you need to extract value from both responses, and rewrite requests based on those mappings.
|
||||
|
||||
`searchResponses` accepts request id, regexp pattern for searching the compared value (should include capture group), and callback which returns both original and replayed matched value.
|
||||
|
||||
Example:
|
||||
```javascript
|
||||
// Compare HTTP headers for response and replayed response, and map values
|
||||
let tokMap = {};
|
||||
|
||||
gor.on("request", function(req) {
|
||||
let tok = gor.httpHeader(req.http, "Auth-Token");
|
||||
if (tok && tokMap[tok]) {
|
||||
req.http = gor.setHttpHeader(req.http, "Auth-Token", tokMap[tok])
|
||||
}
|
||||
|
||||
gor.searchResponses(req.ID, "X-Set-Token: (\w+)$", function(respTok, replTok) {
|
||||
if (respTok && replTok) tokMap[respTok] = replTok;
|
||||
})
|
||||
|
||||
return req;
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### API documentation
|
||||
|
||||
Package expose following functions to process raw HTTP payloads:
|
||||
* `init` - initialize middleware object, start reading from STDIN.
|
||||
* `httpPath` - URL path of the request: `gor.httpPath(req.http)`
|
||||
* `httpMethod` - Http method: 'GET', 'POST', etc. `gor.httpMethod(req.http)`.
|
||||
* `setHttpPath` - update URL path: `req.http = gor.setHttpPath(req.http, newPath)`
|
||||
* `httpPathParam` - get param from URL path: `gor.httpPathParam(req.http, queryParam)`
|
||||
* `setHttpPathParam` - set URL param: `req.http = gor.setHttpPathParam(req.http, queryParam, value)`
|
||||
* `httpStatus` - response status code
|
||||
* `httpHeaders` - get all headers: `gor.httpHeaders(req.http)`
|
||||
* `httpHeader` - get HTTP header: `gor.httpHeader(req.http, "Content-Length")`
|
||||
* `setHttpHeader` - Set HTTP header, returns modified payload: `req.http = gor.setHttpHeader(req.http, "X-Replayed", "1")`
|
||||
* `httpBody` - get HTTP Body: `gor.httpBody(req.http)`
|
||||
* `setHttpBody` - Set HTTP Body and ensures that `Content-Length` header have proper value. Returns modified payload: `req.http = gor.setHttpBody(req.http, Buffer.from('hello!'))`.
|
||||
* `httpBodyParam` - get POST body param: `gor.httpBodyParam(req.http, param)`
|
||||
* `setHttpBodyParam` - set POST body param: `req.http = gor.setHttpBodyParam(req.http, param, value)`
|
||||
* `httpCookie` - get HTTP cookie: `gor.httpCookie(req.http, "SESSSION_ID")`
|
||||
* `setHttpCookie` - set HTTP cookie, returns modified payload: `req.http = gor.setHttpCookie(req.http, "iam", "cuckoo")`
|
||||
* `deleteHttpCookie` - delete HTTP cookie, returns modified payload: `req.http = gor.deleteHttpCookie(req.http, "iam")`
|
||||
|
||||
Also it is totally legit to use standard `Buffer` functions like `indexOf` for processing the HTTP payload. Just do not forget that if you modify the body, update the `Content-Length` header with a new value. And if you modify any of the headers, line endings should be `\r\n`. Rest is up to your imagination.
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
Feel free to ask questions here and by sending email to [support@goreplay.org](mailto:support@goreplay.org). Commercial support is available and welcomed 🙈.
|
||||
Executable
+790
@@ -0,0 +1,790 @@
|
||||
// ======= GoReplay Middleware helper =============
|
||||
// Created by Leonid Bugaev in 2017
|
||||
//
|
||||
// For questions use GitHub or support@goreplay.org
|
||||
//
|
||||
// GoReplay: https://github.com/buger/goreplay
|
||||
// Middleware package: https://github.com/buger/goreplay/middleware
|
||||
|
||||
var middleware;
|
||||
|
||||
function init() {
|
||||
var proxy = {
|
||||
ch: {},
|
||||
on: function(chan, id, cb) {
|
||||
if (!cb && id) {
|
||||
cb = id;
|
||||
} else if (cb && id) {
|
||||
chan = chan + "#" + id;
|
||||
}
|
||||
|
||||
if (!proxy.ch[chan]) {
|
||||
proxy.ch[chan] = [];
|
||||
}
|
||||
|
||||
proxy.ch[chan].push({
|
||||
created: new Date(),
|
||||
cb: cb
|
||||
});
|
||||
|
||||
return proxy;
|
||||
},
|
||||
|
||||
emit: function(msg, raw) {
|
||||
var chanPrefix;
|
||||
|
||||
switch(msg.type) {
|
||||
case "1": chanPrefix = "request"; break;
|
||||
case "2": chanPrefix = "response"; break;
|
||||
case "3": chanPrefix = "replay"; break;
|
||||
}
|
||||
|
||||
let resp = msg;
|
||||
|
||||
["message", chanPrefix, chanPrefix + "#" + msg.ID].forEach(function(chanID, idx){
|
||||
if (proxy.ch[chanID]) {
|
||||
proxy.ch[chanID].forEach(function(ch){
|
||||
let r = ch.cb(msg);
|
||||
if (resp) resp = r; // If one of callback decided not to send response back, do not override it in global callbacks
|
||||
})
|
||||
|
||||
// Cleanup Individual message channels to avoid memory leaks
|
||||
if (idx == 2) {
|
||||
delete proxy.ch[chanID]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (resp) {
|
||||
process.stdout.write(`${resp.rawMeta.toString('hex')}${Buffer.from("\n").toString("hex")}${resp.http.toString('hex')}\n`)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up old messaged ID specific channels if they are older then 60s
|
||||
let gc = function(gcTime){
|
||||
let now = new Date();
|
||||
for (k in proxy.ch) {
|
||||
if (k.indexOf("#") == -1) continue;
|
||||
|
||||
proxy.ch[k] = proxy.ch[k].filter(function(ch){
|
||||
return (now - ch.created) < gcTime
|
||||
})
|
||||
|
||||
if (proxy.ch[k].length == 0) {
|
||||
delete proxy.ch[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
proxy.gc = gc
|
||||
|
||||
setInterval(function(){
|
||||
gc(10 * 1000)
|
||||
}, 1000);
|
||||
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin
|
||||
});
|
||||
|
||||
rl.on('line', function(line) {
|
||||
let msg = parseMessage(line)
|
||||
if (msg) {
|
||||
proxy.emit(msg, line)
|
||||
}
|
||||
});
|
||||
|
||||
middleware = proxy;
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
|
||||
function parseMessage(msg) {
|
||||
try {
|
||||
let payload = Buffer.from(msg, "hex");
|
||||
let metaPos = payload.indexOf("\n");
|
||||
let meta = payload.slice(0, metaPos);
|
||||
let metaArr = meta.toString("ascii").split(" ");
|
||||
let pType = metaArr[0];
|
||||
let pID = metaArr[1];
|
||||
let raw = payload.slice(metaPos + 1, payload.length);
|
||||
|
||||
return {
|
||||
type: pType,
|
||||
ID: pID,
|
||||
rawMeta: meta,
|
||||
meta: metaArr,
|
||||
http: raw
|
||||
}
|
||||
} catch(e) {
|
||||
fail(`Error while parsing incoming request: ${msg}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Used to compare values from original and replayed responses
|
||||
// Accepts request id, regexp pattern for searching the compared value (should include capture group), and callback which returns both original and replayed matched value.
|
||||
// Example:
|
||||
//
|
||||
// // Compare HTTP headers for response and replayed response, and map values
|
||||
// let tokMap = {};
|
||||
//
|
||||
// gor.on("request", function(req) {
|
||||
// let tok = gor.httpHeader(req.http, "Auth-Token");
|
||||
// if (tok && tokMap[tok]) {
|
||||
// req.http = gor.setHttpHeader(req.http, "Auth-Token", tokMap[tok])
|
||||
// }
|
||||
//
|
||||
// gor.searchResponses(req.ID, "X-Set-Token: (\w+)$", function(respTok, replTok) {
|
||||
// tokMap[respTok] = replTok;
|
||||
// })
|
||||
//
|
||||
// return req;
|
||||
// })
|
||||
//
|
||||
function searchResponses(id, searchPattern, callback) {
|
||||
let re = new RegExp(searchPattern);
|
||||
|
||||
// Using regexp require converting buffer to string
|
||||
// Before converting to string we can use initial `Buffer.indexOf` check
|
||||
let indexPattern = searchPattern.split("(")[0];
|
||||
|
||||
if (!indexPattern) {
|
||||
console.error("Search regexp should include capture group, pointing to the value: `prefix-(.*)`")
|
||||
return
|
||||
}
|
||||
|
||||
middleware.on("response", id, function(resp){
|
||||
if (resp.http.indexOf(indexPattern) == -1) {
|
||||
callback()
|
||||
return resp
|
||||
}
|
||||
|
||||
let respMatch = resp.http.toString('utf-8').match(re);
|
||||
if (!respMatch) {
|
||||
callback()
|
||||
return resp
|
||||
}
|
||||
|
||||
middleware.on("replay", id, function(repl) {
|
||||
if (repl.http.indexOf(indexPattern) == -1) {
|
||||
callback(respMatch[1]);
|
||||
return repl;
|
||||
}
|
||||
|
||||
let replMatch = repl.http.toString('utf-8').match(re);
|
||||
|
||||
if (!replMatch) {
|
||||
callback(respMatch[1]);
|
||||
return repl;
|
||||
}
|
||||
|
||||
callback(respMatch[1], replMatch[1]);
|
||||
|
||||
return repl;
|
||||
})
|
||||
|
||||
return resp;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// =========== HTTP parsing =================
|
||||
|
||||
// Example HTTP payload record (including hidden characters):
|
||||
//
|
||||
// POST / HTTP/1.1\r\n
|
||||
// User-Agent: Node\r\n
|
||||
// Content-Length: 5\r\n
|
||||
// \r\n
|
||||
// hello
|
||||
|
||||
function httpMethod(payload) {
|
||||
var pEnd = payload.indexOf(' ');
|
||||
return payload.slice(0, pEnd).toString("ascii");
|
||||
}
|
||||
|
||||
function httpPath(payload) {
|
||||
var pStart = payload.indexOf(' ') + 1;
|
||||
var pEnd = payload.indexOf(' ', pStart);
|
||||
return payload.slice(pStart, pEnd).toString("ascii");
|
||||
}
|
||||
|
||||
function setHttpPath(payload, newPath) {
|
||||
var pStart = payload.indexOf(' ') + 1;
|
||||
var pEnd = payload.indexOf(' ', pStart);
|
||||
|
||||
return Buffer.concat([payload.slice(0, pStart), Buffer.from(newPath), payload.slice(pEnd, payload.length)])
|
||||
}
|
||||
|
||||
function httpPathParam(payload, name) {
|
||||
let path = httpPath(payload);
|
||||
let re = new RegExp(name + "=([^&$]+)");
|
||||
let match = path.match(re);
|
||||
|
||||
if (match) return decodeURI(match[1]);
|
||||
}
|
||||
|
||||
function setHttpPathParam(payload, name, value) {
|
||||
let path = httpPath(payload);
|
||||
let re = new RegExp(name + "=([^&$]+)");
|
||||
let newPath = path.replace(re, name + "=" + encodeURI(value));
|
||||
|
||||
// If we should add new param instead
|
||||
if (newPath == path) {
|
||||
if (newPath.indexOf("?") == -1) {
|
||||
newPath += "?"
|
||||
} else {
|
||||
newPath += "&"
|
||||
}
|
||||
|
||||
newPath += name + "=" + encodeURI(value);
|
||||
}
|
||||
|
||||
return setHttpPath(payload, newPath)
|
||||
}
|
||||
|
||||
// HTTP response have status code in same position as `path` for requests
|
||||
function httpStatus(payload) {
|
||||
return httpPath(payload);
|
||||
}
|
||||
|
||||
function setHttpStatus(payload, newStatus) {
|
||||
return setHttpPath(payload, newStatus);
|
||||
}
|
||||
|
||||
function httpHeaders(payload) {
|
||||
var httpHeaderString = payload.slice(0,payload.indexOf("\r\n\r\n") + 4).toString().split("\n").slice(1);
|
||||
var headers = {};
|
||||
|
||||
for (var item in httpHeaderString) {
|
||||
var parts = httpHeaderString[item].split(":");
|
||||
|
||||
if (parts.length > 1) {
|
||||
headers[parts[0]] = parts.slice(1).join(":").trim();
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function httpHeader(payload, name) {
|
||||
var currentLine = 0;
|
||||
var i = 0;
|
||||
var header = { start: -1, end: -1, valueStart: -1 }
|
||||
var nameBuf = Buffer.from(name);
|
||||
var nameBufLower = Buffer.from(name.toLowerCase());
|
||||
|
||||
while(c = payload[i]) {
|
||||
if (c == 13) { // new line "\n"
|
||||
currentLine++;
|
||||
i++
|
||||
header.end = i
|
||||
|
||||
if (currentLine > 0 && header.start > 0 && header.valueStart > 0) {
|
||||
if (nameBuf.compare(payload, header.start, header.valueStart - 1) == 0 ||
|
||||
nameBufLower.compare(payload, header.start, header.valueStart - 1) == 0) { // ensure that headers are not case sensitive
|
||||
header.value = payload.slice(header.valueStart, header.end - 1).toString("utf-8").trim();
|
||||
header.name = payload.slice(header.start, header.valueStart - 1).toString("utf-8");
|
||||
return header
|
||||
}
|
||||
}
|
||||
|
||||
header.start = -1
|
||||
header.valueStart = -1
|
||||
continue;
|
||||
} else if (c == 10) { // "\r"
|
||||
i++
|
||||
continue;
|
||||
} else if (c == 58) { // ":" Header/value separator symbol
|
||||
if (header.valueStart == -1) {
|
||||
header.valueStart = i + 1;
|
||||
i++
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (header.start == -1) header.start = i;
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
function setHttpHeader(payload, name, value) {
|
||||
let header = httpHeader(payload, name);
|
||||
if (!header) {
|
||||
let headerStart = payload.indexOf(13) + 1;
|
||||
return Buffer.concat([payload.slice(0, headerStart + 1), Buffer.from(name + ": " + value + "\r\n"), payload.slice(headerStart + 1, payload.length)])
|
||||
} else {
|
||||
return Buffer.concat([payload.slice(0, header.valueStart), Buffer.from(" " + value + "\r\n"), payload.slice(header.end + 1, payload.length)])
|
||||
}
|
||||
}
|
||||
|
||||
function deleteHttpHeader(payload, name) {
|
||||
let header = httpHeader(payload, name);
|
||||
|
||||
if (header) {
|
||||
return Buffer.concat([payload.slice(0, header.start), payload.slice(header.end+1, payload.length)])
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
function httpBody(payload) {
|
||||
let bodyIndex = payload.indexOf("\r\n\r\n");
|
||||
if (-1 != bodyIndex){
|
||||
return payload.slice(bodyIndex + 4, payload.length);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setHttpBody(payload, newBody) {
|
||||
let p = setHttpHeader(payload, "Content-Length", newBody.length)
|
||||
let headerEnd = p.indexOf("\r\n\r\n") + 4;
|
||||
return Buffer.concat([p.slice(0, headerEnd), newBody])
|
||||
}
|
||||
|
||||
function httpBodyParam(payload, name) {
|
||||
let body = httpBody(payload);
|
||||
let re = new RegExp(name + "=([^&$]+)");
|
||||
if (body.indexOf(name + "=") != -1) {
|
||||
let param = body.toString('utf-8').match(re);
|
||||
if (param) {
|
||||
return decodeURI(param[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setHttpBodyParam(payload, name, value) {
|
||||
let body = httpBody(payload);
|
||||
let re = new RegExp(name + "=([^&$]+)");
|
||||
|
||||
let newBody = body.toString('utf-8');
|
||||
|
||||
if (newBody.indexOf(name + "=") != -1 ) {
|
||||
newBody = newBody.replace(re, name + "=" + encodeURI(value));
|
||||
} else {
|
||||
if (newBody.indexOf("=") != -1) {
|
||||
newBody += "&";
|
||||
}
|
||||
newBody += name + "=" + value;
|
||||
}
|
||||
|
||||
return setHttpBody(payload, Buffer.from(newBody));
|
||||
}
|
||||
|
||||
function setHttpCookie(payload, name, value) {
|
||||
let h = httpHeader(payload, "Cookie");
|
||||
let cookie = h ? h.value : "";
|
||||
let cookies = cookie.split("; ").filter(function(v){ return v.indexOf(name + "=") != 0 })
|
||||
cookies.push(name + "=" + value)
|
||||
return setHttpHeader(payload, "Cookie", cookies.join("; "))
|
||||
}
|
||||
|
||||
function deleteHttpCookie(payload, name) {
|
||||
let h = httpHeader(payload, "Cookie");
|
||||
let cookie = h ? h.value : "";
|
||||
let cookies = cookie.split("; ").filter(function(v){ return v.indexOf(name + "=") != 0 })
|
||||
return setHttpHeader(payload, "Cookie", cookies.join("; "))
|
||||
}
|
||||
|
||||
function httpCookie(payload, name) {
|
||||
let h = httpHeader(payload, "Cookie");
|
||||
let cookie = h ? h.value : "";
|
||||
let value;
|
||||
let cookies = cookie.split("; ").forEach(function(v){
|
||||
if (v.indexOf(name + "=") == 0) {
|
||||
value = v.substr(name.length + 1);
|
||||
}
|
||||
})
|
||||
return value;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: init,
|
||||
on: function(){ return middleware.on.apply(this, arguments) },
|
||||
parseMessage: parseMessage,
|
||||
searchResponses: searchResponses,
|
||||
httpPath: httpPath,
|
||||
httpMethod: httpMethod,
|
||||
setHttpPath: setHttpPath,
|
||||
httpPathParam: httpPathParam,
|
||||
setHttpPathParam: setHttpPathParam,
|
||||
httpStatus: httpStatus,
|
||||
setHttpStatus: setHttpStatus,
|
||||
httpHeader: httpHeader,
|
||||
setHttpHeader: setHttpHeader,
|
||||
deleteHttpHeader: deleteHttpHeader,
|
||||
httpBody: httpBody,
|
||||
setHttpBody: setHttpBody,
|
||||
httpBodyParam: httpBodyParam,
|
||||
setHttpBodyParam: setHttpBodyParam,
|
||||
httpCookie: httpCookie,
|
||||
setHttpCookie: setHttpCookie,
|
||||
deleteHttpCookie: deleteHttpCookie,
|
||||
test: testRunner,
|
||||
benchmark: testBenchmark,
|
||||
httpHeaders: httpHeaders
|
||||
}
|
||||
|
||||
|
||||
// =========== Tests ==============
|
||||
|
||||
function testRunner(){
|
||||
["init", "filter", "parseMessage", "httpMethod", "httpPath", "setHttpHeader", "deleteHttpHeader", "httpPathParam", "httpHeader", "httpBody", "setHttpBody", "httpBodyParam", "httpCookie", "setHttpCookie", "deleteHttpCookie", "httpHeaders"].forEach(function(t){
|
||||
console.log(`====== Start ${t} =======`)
|
||||
eval(`TEST_${t}()`)
|
||||
console.log(`====== End ${t} =======`)
|
||||
})
|
||||
}
|
||||
|
||||
function testBenchmark(){
|
||||
const child_process = require('child_process');
|
||||
|
||||
let gor = init();
|
||||
gor.on("message", function(){
|
||||
});
|
||||
|
||||
gor.on("request", function(){
|
||||
});
|
||||
|
||||
for (var i = 0; i<256; i++) {
|
||||
let req = parseMessage(Buffer.from("1 2 3\nGET / HTTP/1.1\r\n\r\n").toString('hex'));
|
||||
req.ID = +Date.now()
|
||||
gor.emit(req);
|
||||
|
||||
gor.on("request", req.ID+"", function(){
|
||||
gor.on("response", req.ID+"", function(){
|
||||
})
|
||||
})
|
||||
|
||||
if ( i % 3 == 0 ) {
|
||||
let resp = parseMessage(Buffer.from("2 2 3\nHTTP/1.1 200 OK\r\n\r\n").toString('hex'));
|
||||
resp.ID = req.ID
|
||||
gor.emit(resp);
|
||||
}
|
||||
}
|
||||
|
||||
child_process.execSync("sleep 0.01");
|
||||
|
||||
gor.gc(1)
|
||||
|
||||
fail(JSON.stringify(gor.ch))
|
||||
}
|
||||
|
||||
// Just print in red color
|
||||
function fail(message) {
|
||||
console.error("\x1b[31m[MIDDLEWARE] %s\x1b[0m", message)
|
||||
}
|
||||
|
||||
function log(message) {
|
||||
console.error(message)
|
||||
}
|
||||
|
||||
function TEST_init() {
|
||||
const child_process = require('child_process');
|
||||
|
||||
let received = 0;
|
||||
let gor = init();
|
||||
gor.on("message", function(){
|
||||
received++; // should be called 3 times for for every request
|
||||
});
|
||||
|
||||
gor.on("request", function(){
|
||||
received++; // should be called 1 time only for request
|
||||
});
|
||||
|
||||
gor.on("response", "2", function(){
|
||||
received++; // should be called 1 time only for specific response
|
||||
})
|
||||
|
||||
if (Object.keys(gor.ch).length != 3) {
|
||||
return fail("Should create 3 channels");
|
||||
}
|
||||
|
||||
let req = parseMessage(Buffer.from("1 2 3\nGET / HTTP/1.1\r\n\r\n").toString('hex'));
|
||||
let resp = parseMessage(Buffer.from("2 2 3\nHTTP/1.1 200 OK\r\n\r\n").toString('hex'));
|
||||
let resp2 = parseMessage(Buffer.from("2 3 3\nHTTP/1.1 200 OK\r\n\r\n").toString('hex'));
|
||||
|
||||
gor.emit(req);
|
||||
gor.emit(resp);
|
||||
gor.emit(resp2);
|
||||
|
||||
child_process.execSync("sleep 0.01");
|
||||
|
||||
if (received != 5) {
|
||||
fail(`Should receive 5 messages: ${received}`);
|
||||
}
|
||||
}
|
||||
|
||||
function TEST_filter() {
|
||||
const child_process = require('child_process');
|
||||
|
||||
let gor = init();
|
||||
gor.on("request", function(req){
|
||||
if (httpPath(req.http) != "/filter") {
|
||||
return req
|
||||
}
|
||||
});
|
||||
|
||||
gor.on("request", function(req){
|
||||
return req
|
||||
});
|
||||
|
||||
|
||||
let reqPass = parseMessage(Buffer.from("1 2 3\nGET / HTTP/1.1\r\n\r\n").toString('hex'));
|
||||
let reqFilter = parseMessage(Buffer.from("1 2 3\nGET /filter HTTP/1.1\r\n\r\n").toString('hex'));
|
||||
|
||||
if (!gor.emit(reqPass)) {
|
||||
return fail("Should not filter request")
|
||||
}
|
||||
|
||||
if (gor.emit(reqFilter)) {
|
||||
return fail("Should filter request even if one middleware rejected it")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function TEST_parseMessage() {
|
||||
const exampleMessage = Buffer.from("1 2 3\nGET / HTTP/1.1\r\n\r\n").toString('hex')
|
||||
let msg = parseMessage(exampleMessage)
|
||||
let expected = { type: '1', ID: '2', meta: ["1", "2", "3"], http: Buffer.from("GET / HTTP/1.1\r\n\r\n") }
|
||||
|
||||
Object.keys(expected).forEach(function(k){
|
||||
if (msg[k].toString() != expected[k].toString()) {
|
||||
fail(`${k}: '${expected[k]}' != '${msg[k]}'`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function TEST_httpPath() {
|
||||
const examplePayload = "GET /test HTTP/1.1\r\n\r\n";
|
||||
|
||||
let payload = Buffer.from(examplePayload);
|
||||
let path = httpPath(payload);
|
||||
|
||||
if (path != "/test") {
|
||||
return fail(`Path '${patj}' != '/test'`)
|
||||
}
|
||||
|
||||
let newPayload = setHttpPath(payload, '/')
|
||||
if (newPayload.toString() != "GET / HTTP/1.1\r\n\r\n") {
|
||||
return fail(`Malformed payload '${newPayload}'`)
|
||||
}
|
||||
|
||||
newPayload = setHttpPath(payload, '/bigger')
|
||||
if (newPayload.toString() != "GET /bigger HTTP/1.1\r\n\r\n") {
|
||||
return fail(`Malformed payload '${newPayload}'`)
|
||||
}
|
||||
}
|
||||
|
||||
function TEST_httpMethod() {
|
||||
const examplePayload = "GET /test HTTP/1.1\r\n\r\n";
|
||||
|
||||
let payload = Buffer.from(examplePayload);
|
||||
let method = httpMethod(payload);
|
||||
|
||||
if (method != "GET") {
|
||||
return fail(`Path '${method}' != 'GET'`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function TEST_httpPathParam() {
|
||||
let p = Buffer.from("GET / HTTP/1.1\r\n\r\n");
|
||||
|
||||
if (httpPathParam(p, "test")) {
|
||||
return fail("Should not found param")
|
||||
}
|
||||
|
||||
p = setHttpPathParam(p, "test", "123");
|
||||
if (httpPath(p) != "/?test=123") {
|
||||
return fail("Should set first param: " + httpPath(p));
|
||||
}
|
||||
|
||||
if (httpPathParam(p, "test") != "123") {
|
||||
return fail("Should get first param: " + httpPathParam(p, "test"));
|
||||
}
|
||||
|
||||
p = setHttpPathParam(p, "qwer", "ty");
|
||||
if (httpPath(p) != "/?test=123&qwer=ty") {
|
||||
return fail("Should set second param: " + httpPath(p));
|
||||
}
|
||||
|
||||
p = setHttpPathParam(p, "test", "4321");
|
||||
if (httpPath(p) != "/?test=4321&qwer=ty") {
|
||||
return fail("Should update first param: " + httpPath(p));
|
||||
}
|
||||
|
||||
if (httpPathParam(p, "test") != "4321") {
|
||||
return fail("Should update first param: " + httpPath(p));
|
||||
}
|
||||
}
|
||||
|
||||
function TEST_httpBodyParam() {
|
||||
let p = Buffer.from("POST / HTTP/1.1\r\n\r\n");
|
||||
|
||||
if (httpBodyParam(p, "test")) {
|
||||
return fail("Should not found param")
|
||||
}
|
||||
|
||||
p = setHttpBodyParam(p, "test", "123");
|
||||
if (httpBody(p).toString() != "test=123") {
|
||||
return fail("Should set first param: " + httpBody(p).toString());
|
||||
}
|
||||
|
||||
if (httpBodyParam(p, "test") != "123") {
|
||||
return fail("Should get first param: " + httpBodyParam(p, "test"));
|
||||
}
|
||||
|
||||
p = setHttpBodyParam(p, "qwer", "ty");
|
||||
if (httpBody(p).toString() != "test=123&qwer=ty") {
|
||||
return fail("Should set second param: " + httpBody(p).toString());
|
||||
}
|
||||
|
||||
p = setHttpBodyParam(p, "test", "4321");
|
||||
if (httpBody(p).toString() != "test=4321&qwer=ty") {
|
||||
return fail("Should update first param: " + httpBody(p).toString());
|
||||
}
|
||||
|
||||
if (httpBodyParam(p, "test") != "4321") {
|
||||
return fail("Should update first param: " + httpBody(p).toString());
|
||||
}
|
||||
}
|
||||
|
||||
function TEST_httpHeader() {
|
||||
const examplePayload = "GET / HTTP/1.1\r\nHost: localhost:3000\r\nUser-Agent: Node\r\nContent-Length:5\r\n\r\nhello";
|
||||
|
||||
let expected = {"Host": "localhost:3000", "User-Agent": "Node", "Content-Length": "5"}
|
||||
|
||||
Object.keys(expected).forEach(function(name){
|
||||
let payload = Buffer.from(examplePayload);
|
||||
let header = httpHeader(payload, name);
|
||||
if (!header) {
|
||||
fail(`Header not found. Was looking for: ${name}`)
|
||||
}
|
||||
if (header && header.value != expected[name]) {
|
||||
fail(`${name}: '${expected[name]}' != '${header.value}'`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function TEST_setHttpHeader() {
|
||||
const examplePayload = "GET / HTTP/1.1\r\nUser-Agent: Node\r\nContent-Length: 5\r\n\r\nhello";
|
||||
|
||||
// Modify existing header
|
||||
["", "1", "Long test header"].forEach(function(ua){
|
||||
let expected = `GET / HTTP/1.1\r\nUser-Agent: ${ua}\r\nContent-Length: 5\r\n\r\nhello`;
|
||||
let p = Buffer.from(examplePayload);
|
||||
p = setHttpHeader(p, "User-Agent", ua);
|
||||
if (p != expected) {
|
||||
console.error(`setHeader failed, expected User-Agent value: ${ua}.\n${p}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Adding new header
|
||||
let expected = `GET / HTTP/1.1\r\nX-Test: test\r\nUser-Agent: Node\r\nContent-Length: 5\r\n\r\nhello`;
|
||||
let p = Buffer.from(examplePayload);
|
||||
p = setHttpHeader(p, "X-Test", "test");
|
||||
if (p != expected) {
|
||||
console.error(`setHeader failed, expected new header 'X-Test' header: ${p}`)
|
||||
}
|
||||
}
|
||||
|
||||
function TEST_deleteHttpHeader() {
|
||||
const examplePayload = "GET / HTTP/1.1\r\nUser-Agent: Node\r\nContent-Length: 5\r\n\r\nhello";
|
||||
|
||||
// Adding new header
|
||||
let expected = `GET / HTTP/1.1\r\nContent-Length: 5\r\n\r\nhello`;
|
||||
let p = Buffer.from(examplePayload);
|
||||
p = deleteHttpHeader(p, "User-Agent", "test");
|
||||
if (p != expected) {
|
||||
console.error(`setHeader failed, expected delete header 'User-Agent' header: ${p}`)
|
||||
}
|
||||
}
|
||||
|
||||
function TEST_httpBody() {
|
||||
const examplePayload = "GET / HTTP/1.1\r\nUser-Agent: Node\r\nContent-Length: 5\r\n\r\nhello";
|
||||
let body = httpBody(Buffer.from(examplePayload));
|
||||
if (body != "hello") {
|
||||
fail(`'${body}' != 'hello'`)
|
||||
}
|
||||
|
||||
const exampleInvalidPayload = "Invalid HTTP Response by Network issue";
|
||||
let invalidBody = httpBody(Buffer.from(exampleInvalidPayload));
|
||||
if (invalidBody != null) {
|
||||
fail(`'${invalidBody}' != 'null'`)
|
||||
}
|
||||
}
|
||||
|
||||
function TEST_setHttpBody() {
|
||||
const examplePayload = "GET / HTTP/1.1\r\nUser-Agent: Node\r\nContent-Length: 5\r\n\r\nhello";
|
||||
let p = setHttpBody(Buffer.from(examplePayload), Buffer.from("hello, world!"));
|
||||
|
||||
if (p != "GET / HTTP/1.1\r\nUser-Agent: Node\r\nContent-Length: 13\r\n\r\nhello, world!") {
|
||||
fail(`Wrong body: '${p}'`)
|
||||
}
|
||||
}
|
||||
|
||||
function TEST_httpCookie() {
|
||||
const examplePayload = "GET / HTTP/1.1\r\nCookie: a=b; test=zxc\r\n\r\n";
|
||||
let c = httpCookie(Buffer.from(examplePayload), "test");
|
||||
if (c != "zxc") {
|
||||
return fail(`Should get cookie: ${c}`);
|
||||
}
|
||||
|
||||
c = httpCookie(Buffer.from(examplePayload), "nope");
|
||||
if (c != null) {
|
||||
return fail(`Should not find cookie: ${c}`);
|
||||
}
|
||||
}
|
||||
|
||||
function TEST_setHttpCookie() {
|
||||
const examplePayload = "GET / HTTP/1.1\r\nCookie: a=b; test=zxc\r\n\r\n";
|
||||
let p = setHttpCookie(Buffer.from(examplePayload), "test", "1");
|
||||
if (p != "GET / HTTP/1.1\r\nCookie: a=b; test=1\r\n\r\n") {
|
||||
return fail(`Should update cookie: ${p}`)
|
||||
}
|
||||
|
||||
p = setHttpCookie(Buffer.from(examplePayload), "new", "one");
|
||||
if (p != "GET / HTTP/1.1\r\nCookie: a=b; test=zxc; new=one\r\n\r\n") {
|
||||
return fail(`Should add new cookie: ${p}`)
|
||||
}
|
||||
}
|
||||
|
||||
function TEST_deleteHttpCookie() {
|
||||
const examplePayload = "GET / HTTP/1.1\r\nCookie: a=b; test=zxc\r\n\r\n";
|
||||
let p = deleteHttpCookie(Buffer.from(examplePayload), "a");
|
||||
if (p != "GET / HTTP/1.1\r\nCookie: test=zxc\r\n\r\n") {
|
||||
return fail(`Should delete cookie: ${p}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function TEST_httpHeaders() {
|
||||
const examplePayload = "GET / HTTP/1.1\r\nHost: localhost:3000\r\nUser-Agent: Node\r\nContent-Length:5\r\n\r\nhello";
|
||||
|
||||
let expectedHeaders = {"Host": "localhost:3000", "User-Agent": "Node", "Content-Length": "5"}
|
||||
let payload = Buffer.from(examplePayload);
|
||||
let headers = httpHeaders(payload);
|
||||
|
||||
["Host", "User-Agent", "Content-Length"].forEach(function(header){
|
||||
let actual = headers[header];
|
||||
let expected = expectedHeaders[header];
|
||||
|
||||
if (!actual) {
|
||||
fail(`${header} Header was not found`);
|
||||
}
|
||||
|
||||
if (actual != expected) {
|
||||
fail(`${header} Header not Equal to Expected: ${expected} was ${actual}`);
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "goreplay_middleware",
|
||||
"version": "1.0.0",
|
||||
"description": "Package for writing middleware for GoReplay https://goreplay.org",
|
||||
"main": "middleware.js",
|
||||
"scripts": {
|
||||
"test": "node -e \"var gor = require('./middleware.js'); gor.test(); process.exit()\"",
|
||||
"benchmark": "node -e \"var gor = require('./middleware.js'); gor.benchmark(); process.exit()\""
|
||||
},
|
||||
"keywords": [
|
||||
"middleware",
|
||||
"goreplay"
|
||||
],
|
||||
"author": "Leonid Bugaev",
|
||||
"license": "LGPL-3.0"
|
||||
}
|
||||
+37
-25
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"github.com/buger/gor/proto"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -12,11 +11,13 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
type fakeServiceCb func(string, int, []byte)
|
||||
|
||||
// Simple service that generate token on request, and require this token for accesing to secure area
|
||||
// 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
|
||||
@@ -38,7 +39,7 @@ func NewFakeSecureService(wg *sync.WaitGroup, cb fakeServiceCb) *httptest.Server
|
||||
w.Write([]byte(token))
|
||||
|
||||
cb(req.URL.Path, 200, []byte(token))
|
||||
case "/secure":
|
||||
case "/Secure":
|
||||
token := req.URL.Query().Get("token")
|
||||
token_found := false
|
||||
|
||||
@@ -80,13 +81,13 @@ func TestFakeSecureService(t *testing.T) {
|
||||
token = proto.Body(resp)
|
||||
|
||||
// Right token
|
||||
resp, _ = client.Get("/secure?token=" + string(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")
|
||||
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)))
|
||||
}
|
||||
@@ -113,23 +114,28 @@ func TestEchoMiddleware(t *testing.T) {
|
||||
|
||||
quit := make(chan int)
|
||||
|
||||
Settings.middleware = "./examples/middleware/echo.sh"
|
||||
Settings.Middleware = "./examples/Middleware/echo.sh"
|
||||
|
||||
// Catch traffic from one service
|
||||
input := NewRAWInput(from.Listener.Addr().String(), testRawExpire)
|
||||
fromAddr := strings.Replace(from.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
input := NewRAWInput(fromAddr, EnginePcap, true, testRawExpire, "", "http", "", "", 0)
|
||||
defer input.Close()
|
||||
|
||||
// And redirect to another
|
||||
output := NewHTTPOutput(to.URL, &HTTPOutputConfig{Debug: false})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
// Start Gor
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
// Wait till middleware initialization
|
||||
time.Sleep(10*time.Millisecond)
|
||||
// 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})
|
||||
@@ -138,14 +144,16 @@ func TestEchoMiddleware(t *testing.T) {
|
||||
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)
|
||||
emitter.Close()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
Settings.middleware = ""
|
||||
Settings.Middleware = ""
|
||||
}
|
||||
|
||||
func TestTokenMiddleware(t *testing.T) {
|
||||
@@ -160,7 +168,7 @@ func TestTokenMiddleware(t *testing.T) {
|
||||
|
||||
to := NewFakeSecureService(wg, func(path string, status int, tok []byte) {
|
||||
switch path {
|
||||
case "/secure":
|
||||
case "/Secure":
|
||||
if status != 202 {
|
||||
t.Error("Server should receive valid rewritten token")
|
||||
}
|
||||
@@ -172,23 +180,27 @@ func TestTokenMiddleware(t *testing.T) {
|
||||
|
||||
quit := make(chan int)
|
||||
|
||||
Settings.middleware = "go run ./examples/middleware/token_modifier.go"
|
||||
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, testRawExpire)
|
||||
input := NewRAWInput(fromAddr, EnginePcap, true, testRawExpire, "", "http", "", "", 0)
|
||||
defer input.Close()
|
||||
|
||||
// And redirect to another
|
||||
output := NewHTTPOutput(to.URL, &HTTPOutputConfig{Debug: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
// Start Gor
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
// Wait for middleware to initialize
|
||||
// Wait for Middleware to initialize
|
||||
// Give go compiller time to build programm
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
@@ -201,17 +213,17 @@ func TestTokenMiddleware(t *testing.T) {
|
||||
resp, _ = client.Get("/token")
|
||||
token = proto.Body(resp)
|
||||
|
||||
// When delay is too smal, middleware does not always rewrite requests in time
|
||||
// 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))
|
||||
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)
|
||||
emitter.Close()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
Settings.middleware = ""
|
||||
Settings.Middleware = ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
site_name: My Docs
|
||||
theme: readthedocs
|
||||
@@ -0,0 +1,176 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BinaryOutputConfig struct for holding binary output configuration
|
||||
type BinaryOutputConfig struct {
|
||||
workers int
|
||||
Timeout time.Duration
|
||||
BufferSize int
|
||||
Debug bool
|
||||
TrackResponses bool
|
||||
}
|
||||
|
||||
// BinaryOutput 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-tcp-workers`
|
||||
type BinaryOutput 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
|
||||
queue chan []byte
|
||||
|
||||
responses chan response
|
||||
|
||||
needWorker chan int
|
||||
|
||||
config *BinaryOutputConfig
|
||||
|
||||
queueStats *GorStat
|
||||
}
|
||||
|
||||
// NewBinaryOutput constructor for BinaryOutput
|
||||
// Initialize workers
|
||||
func NewBinaryOutput(address string, config *BinaryOutputConfig) io.Writer {
|
||||
o := new(BinaryOutput)
|
||||
|
||||
o.address = address
|
||||
o.config = config
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
go o.workerMaster()
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *BinaryOutput) 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 *BinaryOutput) startWorker() {
|
||||
client := NewTCPClient(o.address, &TCPClientConfig{
|
||||
Debug: o.config.Debug,
|
||||
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 *BinaryOutput) 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.workers == 0 {
|
||||
workersCount := atomic.LoadInt64(&o.activeWorkers)
|
||||
|
||||
if len(o.queue) > int(workersCount) {
|
||||
o.needWorker <- len(o.queue)
|
||||
}
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (o *BinaryOutput) Read(data []byte) (int, error) {
|
||||
resp := <-o.responses
|
||||
|
||||
Debug("[OUTPUT-TCP] Received response:", string(resp.payload))
|
||||
|
||||
header := payloadHeader(ReplayedResponsePayload, resp.uuid, resp.startedAt, resp.roundTripTime)
|
||||
copy(data[0:len(header)], header)
|
||||
copy(data[len(header):], resp.payload)
|
||||
|
||||
return len(resp.payload) + len(header), nil
|
||||
}
|
||||
|
||||
func (o *BinaryOutput) sendRequest(client *TCPClient, request []byte) {
|
||||
meta := payloadMeta(request)
|
||||
if len(meta) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
if !isRequestPayload(request) {
|
||||
return
|
||||
}
|
||||
|
||||
uuid := meta[1]
|
||||
|
||||
body := payloadBody(request)
|
||||
|
||||
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, start.UnixNano(), stop.UnixNano() - start.UnixNano()}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *BinaryOutput) String() string {
|
||||
return "Binary output: " + o.address
|
||||
}
|
||||
+2
-2
@@ -9,14 +9,14 @@ type DummyOutput struct {
|
||||
}
|
||||
|
||||
// NewDummyOutput constructor for DummyOutput
|
||||
func NewDummyOutput(options string) (di *DummyOutput) {
|
||||
func NewDummyOutput() (di *DummyOutput) {
|
||||
di = new(DummyOutput)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *DummyOutput) Write(data []byte) (int, error) {
|
||||
fmt.Println("Writing message: ", string(data))
|
||||
fmt.Println(string(data))
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
+273
-17
@@ -1,47 +1,303 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var dateFileNameFuncs = map[string]func(*FileOutput) string{
|
||||
"%Y": func(o *FileOutput) string { return time.Now().Format("2006") },
|
||||
"%m": func(o *FileOutput) string { return time.Now().Format("01") },
|
||||
"%d": func(o *FileOutput) string { return time.Now().Format("02") },
|
||||
"%H": func(o *FileOutput) string { return time.Now().Format("15") },
|
||||
"%M": func(o *FileOutput) string { return time.Now().Format("04") },
|
||||
"%S": func(o *FileOutput) string { return time.Now().Format("05") },
|
||||
"%NS": func(o *FileOutput) string { return fmt.Sprint(time.Now().Nanosecond()) },
|
||||
"%r": func(o *FileOutput) string { return string(o.currentID) },
|
||||
"%t": func(o *FileOutput) string { return string(o.payloadType) },
|
||||
}
|
||||
|
||||
// FileOutputConfig ...
|
||||
type FileOutputConfig struct {
|
||||
flushInterval time.Duration
|
||||
sizeLimit int64
|
||||
outputFileMaxSize int64
|
||||
queueLimit int64
|
||||
append bool
|
||||
bufferPath string
|
||||
onClose func(string)
|
||||
}
|
||||
|
||||
// FileOutput output plugin
|
||||
type FileOutput struct {
|
||||
path string
|
||||
file *os.File
|
||||
sync.RWMutex
|
||||
pathTemplate string
|
||||
currentName string
|
||||
file *os.File
|
||||
queueLength int64
|
||||
chunkSize int
|
||||
writer io.Writer
|
||||
requestPerFile bool
|
||||
currentID []byte
|
||||
payloadType []byte
|
||||
closed bool
|
||||
totalFileSize int64
|
||||
|
||||
config *FileOutputConfig
|
||||
}
|
||||
|
||||
// NewFileOutput constructor for FileOutput, accepts path
|
||||
func NewFileOutput(path string) io.Writer {
|
||||
func NewFileOutput(pathTemplate string, config *FileOutputConfig) *FileOutput {
|
||||
o := new(FileOutput)
|
||||
o.path = path
|
||||
o.init(path)
|
||||
o.pathTemplate = pathTemplate
|
||||
o.config = config
|
||||
o.updateName()
|
||||
|
||||
if strings.Contains(pathTemplate, "%r") {
|
||||
o.requestPerFile = true
|
||||
}
|
||||
|
||||
if config.flushInterval == 0 {
|
||||
config.flushInterval = 100 * time.Millisecond
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(config.flushInterval)
|
||||
if o.IsClosed() {
|
||||
break
|
||||
}
|
||||
o.updateName()
|
||||
o.flush()
|
||||
}
|
||||
}()
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *FileOutput) init(path string) {
|
||||
var err error
|
||||
func getFileIndex(name string) int {
|
||||
ext := filepath.Ext(name)
|
||||
withoutExt := strings.TrimSuffix(name, ext)
|
||||
|
||||
o.file, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(o, "Cannot open file %q. Error: %s", path, err)
|
||||
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 {
|
||||
o.RLock()
|
||||
defer o.RUnlock()
|
||||
|
||||
path := o.pathTemplate
|
||||
|
||||
for name, fn := range dateFileNameFuncs {
|
||||
path = strings.Replace(path, name, fn(o), -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() {
|
||||
name := filepath.Clean(o.filename())
|
||||
o.Lock()
|
||||
o.currentName = name
|
||||
o.Unlock()
|
||||
}
|
||||
|
||||
func (o *FileOutput) Write(data []byte) (n int, err error) {
|
||||
if !isOriginPayload(data) {
|
||||
return len(data), nil
|
||||
if o.requestPerFile {
|
||||
o.Lock()
|
||||
meta := payloadMeta(data)
|
||||
o.currentID = meta[1]
|
||||
o.payloadType = meta[0]
|
||||
o.Unlock()
|
||||
}
|
||||
|
||||
o.file.Write(data)
|
||||
o.file.Write([]byte(payloadSeparator))
|
||||
o.updateName()
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
|
||||
return len(data), nil
|
||||
if o.file == nil || o.currentName != o.file.Name() {
|
||||
o.closeLocked()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
n, _ = o.writer.Write(data)
|
||||
nSeparator, _ := o.writer.Write([]byte(payloadSeparator))
|
||||
|
||||
n += nSeparator
|
||||
|
||||
o.totalFileSize += int64(n)
|
||||
o.queueLength++
|
||||
|
||||
if Settings.OutputFileConfig.outputFileMaxSize > 0 && o.totalFileSize >= Settings.OutputFileConfig.outputFileMaxSize {
|
||||
return n, errors.New("File output reached size limit")
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (o *FileOutput) flush() {
|
||||
// Don't exit on panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("PANIC while file flush: ", r, o, string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
|
||||
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())
|
||||
} else {
|
||||
log.Println("Error accessing file sats", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *FileOutput) String() string {
|
||||
return "File output: " + o.path
|
||||
return "File output: " + o.file.Name()
|
||||
}
|
||||
|
||||
func (o *FileOutput) closeLocked() 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()
|
||||
|
||||
if o.config.onClose != nil {
|
||||
o.config.onClose(o.file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
o.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the output file that is being written to.
|
||||
func (o *FileOutput) Close() error {
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
return o.closeLocked()
|
||||
}
|
||||
|
||||
// IsClosed returns if the output file is closed or not.
|
||||
func (o *FileOutput) IsClosed() bool {
|
||||
o.Lock()
|
||||
defer o.Unlock()
|
||||
return o.closed
|
||||
}
|
||||
|
||||
+328
-12
@@ -1,9 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFileOutput(t *testing.T) {
|
||||
@@ -11,32 +18,341 @@ func TestFileOutput(t *testing.T) {
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewFileOutput("/tmp/test_requests.gor")
|
||||
output := NewFileOutput("/tmp/test_requests.gor", &FileOutputConfig{flushInterval: time.Minute, append: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(2)
|
||||
input.EmitGET()
|
||||
input.EmitPOST()
|
||||
}
|
||||
close(quit)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
output.flush()
|
||||
emitter.Close()
|
||||
|
||||
quit = make(chan int)
|
||||
|
||||
input2 := NewFileInput("/tmp/test_requests.gor")
|
||||
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}
|
||||
plugins2 := &InOutPlugins{
|
||||
Inputs: []io.Reader{input2},
|
||||
Outputs: []io.Writer{output2},
|
||||
}
|
||||
plugins2.All = append(plugins2.All, input2, output2)
|
||||
|
||||
go Start(quit)
|
||||
quit2 := make(chan int)
|
||||
emitter2 := NewEmitter(quit2)
|
||||
go emitter2.Start(plugins2, Settings.Middleware)
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
emitter2.Close()
|
||||
}
|
||||
|
||||
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-%t", config: &FileOutputConfig{flushInterval: time.Minute, append: true}}
|
||||
now := time.Now()
|
||||
output.payloadType = []byte("3")
|
||||
expectedPath := fmt.Sprintf("/tmp/log-%s-%s-%s-%s-3", 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 TestFileOutputFilePerRequest(t *testing.T) {
|
||||
output := NewFileOutput("/tmp/log-%Y-%m-%d-%S-%r", &FileOutputConfig{append: true})
|
||||
|
||||
if output.file != nil {
|
||||
t.Error("Should not initialize file if no writes")
|
||||
}
|
||||
|
||||
output.Write([]byte("1 1 1\ntest"))
|
||||
name1 := output.file.Name()
|
||||
|
||||
output.Write([]byte("1 2 1\ntest"))
|
||||
name2 := output.file.Name()
|
||||
|
||||
time.Sleep(time.Second)
|
||||
output.updateName()
|
||||
|
||||
output.Write([]byte("1 3 1\ntest"))
|
||||
name3 := output.file.Name()
|
||||
|
||||
if name3 == name2 || name2 == name1 || name3 == name1 {
|
||||
t.Error("File name should change:", name1, name2, name3)
|
||||
}
|
||||
|
||||
os.Remove(name1)
|
||||
os.Remove(name2)
|
||||
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 d = map[string]int64{
|
||||
"42mb": 42 << 20,
|
||||
"4_2": 42,
|
||||
"00": 0,
|
||||
"\n\n 0.0\r\t\f": 0,
|
||||
"0_600tb": 384 << 40,
|
||||
"0600Tb": 384 << 40,
|
||||
"0o12Mb": 10 << 20,
|
||||
"0b_10010001111_1kb": 2335 << 10,
|
||||
"1024": 1 << 10,
|
||||
"0b111": 7,
|
||||
"0x12gB": 18 << 30,
|
||||
"0x_67_7a_2f_cc_40_c6": 113774485586118,
|
||||
"121562380192901": 121562380192901,
|
||||
}
|
||||
for k, v := range d {
|
||||
n, err := bufferParser(k, "0")
|
||||
if err != nil || n != v {
|
||||
t.Errorf("Error parsing %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileOutputAppendSizeLimitOverflow(t *testing.T) {
|
||||
rnd := rand.Int63()
|
||||
name := fmt.Sprintf("/tmp/%d", rnd)
|
||||
|
||||
message := []byte("1 1 1\r\ntest")
|
||||
|
||||
messageSize := len(message) + len(payloadSeparator)
|
||||
|
||||
output := NewFileOutput(name, &FileOutputConfig{append: false, flushInterval: time.Minute, sizeLimit: 2 * int64(messageSize)})
|
||||
|
||||
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.flush()
|
||||
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)
|
||||
}
|
||||
|
||||
+160
-44
@@ -1,31 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/buger/gor/proto"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
var _ = fmt.Println
|
||||
|
||||
const initialDynamicWorkers = 10
|
||||
|
||||
type httpWorker struct {
|
||||
output *HTTPOutput
|
||||
client *HTTPClient
|
||||
lastActivity time.Time
|
||||
queue chan []byte
|
||||
stop chan bool
|
||||
}
|
||||
|
||||
func newHTTPWorker(output *HTTPOutput, queue chan []byte) *httpWorker {
|
||||
client := NewHTTPClient(output.address, &HTTPClientConfig{
|
||||
FollowRedirects: output.config.redirectLimit,
|
||||
Debug: output.config.Debug,
|
||||
OriginalHost: output.config.OriginalHost,
|
||||
Timeout: output.config.Timeout,
|
||||
ResponseBufferSize: output.config.BufferSize,
|
||||
})
|
||||
|
||||
w := &httpWorker{client: client}
|
||||
if queue == nil {
|
||||
w.queue = make(chan []byte, 100)
|
||||
} else {
|
||||
w.queue = queue
|
||||
}
|
||||
w.stop = make(chan bool)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case payload := <-w.queue:
|
||||
output.sendRequest(client, payload)
|
||||
case <-w.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
type response struct {
|
||||
payload []byte
|
||||
uuid []byte
|
||||
roundTripTime int64
|
||||
startedAt int64
|
||||
}
|
||||
|
||||
// HTTPOutputConfig struct for holding http output configuration
|
||||
type HTTPOutputConfig struct {
|
||||
redirectLimit int
|
||||
|
||||
stats bool
|
||||
workers int
|
||||
stats bool
|
||||
workersMin int
|
||||
workersMax int
|
||||
statsMs int
|
||||
workers int
|
||||
queueLen int
|
||||
|
||||
elasticSearch string
|
||||
|
||||
Timeout time.Duration
|
||||
OriginalHost bool
|
||||
BufferSize int
|
||||
|
||||
CompatibilityMode bool
|
||||
|
||||
RequestGroup string
|
||||
|
||||
Debug bool
|
||||
|
||||
@@ -41,6 +95,8 @@ type HTTPOutput struct {
|
||||
// aligned at 64bit. See https://github.com/golang/go/issues/599
|
||||
activeWorkers int64
|
||||
|
||||
workerSessions map[string]*httpWorker
|
||||
|
||||
address string
|
||||
limit int
|
||||
queue chan []byte
|
||||
@@ -54,6 +110,8 @@ type HTTPOutput struct {
|
||||
queueStats *GorStat
|
||||
|
||||
elasticSearch *ESPlugin
|
||||
|
||||
stop chan bool // Channel used only to indicate goroutine should shutdown
|
||||
}
|
||||
|
||||
// NewHTTPOutput constructor for HTTPOutput
|
||||
@@ -63,20 +121,21 @@ func NewHTTPOutput(address string, config *HTTPOutputConfig) io.Writer {
|
||||
|
||||
o.address = address
|
||||
o.config = config
|
||||
o.stop = make(chan bool)
|
||||
|
||||
if o.config.stats {
|
||||
o.queueStats = NewGorStat("output_http")
|
||||
o.queueStats = NewGorStat("output_http", o.config.statsMs)
|
||||
}
|
||||
|
||||
o.queue = make(chan []byte, 1000)
|
||||
o.responses = make(chan response, 1000)
|
||||
o.queue = make(chan []byte, o.config.queueLen)
|
||||
o.responses = make(chan response, o.config.queueLen)
|
||||
o.needWorker = make(chan int, 1)
|
||||
|
||||
// Initial workers count
|
||||
if o.config.workers == 0 {
|
||||
if o.config.workersMax == 0 {
|
||||
o.needWorker <- initialDynamicWorkers
|
||||
} else {
|
||||
o.needWorker <- o.config.workers
|
||||
o.needWorker <- o.config.workersMax
|
||||
}
|
||||
|
||||
if o.config.elasticSearch != "" {
|
||||
@@ -84,62 +143,89 @@ func NewHTTPOutput(address string, config *HTTPOutputConfig) io.Writer {
|
||||
o.elasticSearch.Init(o.config.elasticSearch)
|
||||
}
|
||||
|
||||
if len(Settings.middleware) > 0 {
|
||||
o.config.TrackResponses = true
|
||||
if Settings.RecognizeTCPSessions {
|
||||
if !PRO {
|
||||
log.Fatal("Detailed TCP sessions work only with PRO license")
|
||||
}
|
||||
o.workerSessions = make(map[string]*httpWorker, 100)
|
||||
go o.sessionWorkerMaster()
|
||||
} else {
|
||||
go o.workerMaster()
|
||||
}
|
||||
|
||||
go o.workerMaster()
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) workerMaster() {
|
||||
for {
|
||||
newWorkers := <-o.needWorker
|
||||
atomic.AddInt64(&o.activeWorkers, int64(newWorkers))
|
||||
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) sessionWorkerMaster() {
|
||||
gc := time.Tick(time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case p := <-o.queue:
|
||||
id := payloadID(p)
|
||||
sessionID := string(id[0:20])
|
||||
worker, ok := o.workerSessions[sessionID]
|
||||
|
||||
if !ok {
|
||||
atomic.AddInt64(&o.activeWorkers, 1)
|
||||
worker = newHTTPWorker(o, nil)
|
||||
o.workerSessions[sessionID] = worker
|
||||
}
|
||||
|
||||
worker.queue <- p
|
||||
worker.lastActivity = time.Now()
|
||||
case <-gc:
|
||||
now := time.Now()
|
||||
|
||||
for id, w := range o.workerSessions {
|
||||
if !w.lastActivity.IsZero() && now.Sub(w.lastActivity) >= 120*time.Second {
|
||||
w.stop <- true
|
||||
delete(o.workerSessions, id)
|
||||
atomic.AddInt64(&o.activeWorkers, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
FollowRedirects: o.config.redirectLimit,
|
||||
Debug: o.config.Debug,
|
||||
OriginalHost: o.config.OriginalHost,
|
||||
Timeout: o.config.Timeout,
|
||||
ResponseBufferSize: o.config.BufferSize,
|
||||
CompatibilityMode: o.config.CompatibilityMode,
|
||||
})
|
||||
|
||||
deathCount := 0
|
||||
|
||||
atomic.AddInt64(&o.activeWorkers, 1)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-o.stop:
|
||||
return
|
||||
case data := <-o.queue:
|
||||
o.sendRequest(client, data)
|
||||
deathCount = 0
|
||||
case <-time.After(time.Millisecond * 100):
|
||||
case <-time.After(2 * time.Second):
|
||||
// When dynamic scaling enabled workers die after 2s of inactivity
|
||||
if o.config.workers == 0 {
|
||||
deathCount++
|
||||
} else {
|
||||
if o.config.workersMin == o.config.workersMax {
|
||||
continue
|
||||
}
|
||||
|
||||
if deathCount > 20 {
|
||||
workersCount := atomic.LoadInt64(&o.activeWorkers)
|
||||
workersCount := int(atomic.LoadInt64(&o.activeWorkers))
|
||||
|
||||
// At least 1 startWorker should be alive
|
||||
if workersCount != 1 {
|
||||
atomic.AddInt64(&o.activeWorkers, -1)
|
||||
return
|
||||
}
|
||||
// At least 1 startWorker should be alive
|
||||
if workersCount != 1 && workersCount > o.config.workersMin {
|
||||
atomic.AddInt64(&o.activeWorkers, -1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,17 +239,28 @@ func (o *HTTPOutput) Write(data []byte) (n int, err error) {
|
||||
buf := make([]byte, len(data))
|
||||
copy(buf, data)
|
||||
|
||||
o.queue <- buf
|
||||
select {
|
||||
case <-o.stop:
|
||||
return 0, ErrorStopped
|
||||
case o.queue <- buf:
|
||||
}
|
||||
|
||||
if o.config.stats {
|
||||
o.queueStats.Write(len(o.queue))
|
||||
}
|
||||
|
||||
if o.config.workers == 0 {
|
||||
workersCount := atomic.LoadInt64(&o.activeWorkers)
|
||||
if !Settings.RecognizeTCPSessions && o.config.workersMax != o.config.workersMin {
|
||||
workersCount := int(atomic.LoadInt64(&o.activeWorkers))
|
||||
|
||||
if len(o.queue) > int(workersCount) {
|
||||
o.needWorker <- len(o.queue)
|
||||
if len(o.queue) > workersCount {
|
||||
extraWorkersReq := len(o.queue) - workersCount + 1
|
||||
maxWorkersAvailable := o.config.workersMax - workersCount
|
||||
if extraWorkersReq > maxWorkersAvailable {
|
||||
extraWorkersReq = maxWorkersAvailable
|
||||
}
|
||||
if extraWorkersReq > 0 {
|
||||
o.needWorker <- extraWorkersReq
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,11 +268,18 @@ func (o *HTTPOutput) Write(data []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) Read(data []byte) (int, error) {
|
||||
resp := <-o.responses
|
||||
var resp response
|
||||
select {
|
||||
case <-o.stop:
|
||||
return 0, ErrorStopped
|
||||
case resp = <-o.responses:
|
||||
}
|
||||
|
||||
Debug("[OUTPUT-HTTP] Received response:", string(resp.payload))
|
||||
if Settings.Debug {
|
||||
Debug("[OUTPUT-HTTP] Received response:", string(resp.payload))
|
||||
}
|
||||
|
||||
header := payloadHeader(ReplayedResponsePayload, resp.uuid, resp.roundTripTime)
|
||||
header := payloadHeader(ReplayedResponsePayload, resp.uuid, resp.roundTripTime, resp.startedAt)
|
||||
copy(data[0:len(header)], header)
|
||||
copy(data[len(header):], resp.payload)
|
||||
|
||||
@@ -184,6 +288,11 @@ func (o *HTTPOutput) Read(data []byte) (int, error) {
|
||||
|
||||
func (o *HTTPOutput) sendRequest(client *HTTPClient, request []byte) {
|
||||
meta := payloadMeta(request)
|
||||
|
||||
if Settings.Debug {
|
||||
Debug(meta)
|
||||
}
|
||||
|
||||
if len(meta) < 2 {
|
||||
return
|
||||
}
|
||||
@@ -199,11 +308,12 @@ func (o *HTTPOutput) sendRequest(client *HTTPClient, request []byte) {
|
||||
stop := time.Now()
|
||||
|
||||
if err != nil {
|
||||
log.Println("Error when sending ", err, time.Now())
|
||||
Debug("Request error:", err)
|
||||
}
|
||||
|
||||
if o.config.TrackResponses {
|
||||
o.responses <- response{resp, uuid, stop.UnixNano() - start.UnixNano()}
|
||||
o.responses <- response{resp, uuid, start.UnixNano(), stop.UnixNano() - start.UnixNano()}
|
||||
}
|
||||
|
||||
if o.elasticSearch != nil {
|
||||
@@ -214,3 +324,9 @@ func (o *HTTPOutput) sendRequest(client *HTTPClient, request []byte) {
|
||||
func (o *HTTPOutput) String() string {
|
||||
return "HTTP output: " + o.address
|
||||
}
|
||||
|
||||
// Close closes the data channel so that data
|
||||
func (o *HTTPOutput) Close() error {
|
||||
close(o.stop)
|
||||
return nil
|
||||
}
|
||||
|
||||
+94
-23
@@ -7,6 +7,7 @@ import (
|
||||
"net/http/httptest"
|
||||
_ "net/http/httputil"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -43,24 +44,37 @@ func TestHTTPOutput(t *testing.T) {
|
||||
methods := HTTPMethods{[]byte("GET"), []byte("PUT"), []byte("POST")}
|
||||
Settings.modifierConfig = HTTPModifierConfig{headers: headers, methods: methods}
|
||||
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{Debug: true})
|
||||
http_output := NewHTTPOutput(server.URL, &HTTPOutputConfig{Debug: true, TrackResponses: true})
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{http_output, output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output, http_output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(2) // OPTIONS should be ignored
|
||||
for i := 0; i < 10; i++ {
|
||||
// 2 http-output, 2 - test output request, 2 - test output http response
|
||||
wg.Add(6) // OPTIONS should be ignored
|
||||
input.EmitPOST()
|
||||
input.EmitOPTIONS()
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
|
||||
activeWorkers := atomic.LoadInt64(&http_output.(*HTTPOutput).activeWorkers)
|
||||
|
||||
if activeWorkers < 50 {
|
||||
t.Error("Should create workers for each request", activeWorkers)
|
||||
}
|
||||
|
||||
Settings.modifierConfig = HTTPModifierConfig{}
|
||||
}
|
||||
|
||||
@@ -84,22 +98,24 @@ func TestHTTPOutputKeepOriginalHost(t *testing.T) {
|
||||
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{Debug: false, OriginalHost: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
|
||||
emitter.Close()
|
||||
Settings.modifierConfig = HTTPModifierConfig{}
|
||||
}
|
||||
|
||||
func TestOutputHTTPSSL(t *testing.T) {
|
||||
func TestHTTPOutputSSL(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
@@ -111,10 +127,14 @@ func TestOutputHTTPSSL(t *testing.T) {
|
||||
input := NewTestInput()
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
@@ -122,7 +142,55 @@ func TestOutputHTTPSSL(t *testing.T) {
|
||||
input.EmitGET()
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
func TestHTTPOutputSessions(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
input.skipHeader = true
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
Settings.RecognizeTCPSessions = true
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{Debug: true})
|
||||
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
uuid1 := []byte("1234567890123456789a0000")
|
||||
uuid2 := []byte("1234567890123456789d0000")
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1) // OPTIONS should be ignored
|
||||
copy(uuid1[20:], randByte(4))
|
||||
input.EmitBytes([]byte("1 " + string(uuid1) + " 1\n" + "GET / HTTP/1.1\r\n\r\n"))
|
||||
}
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1) // OPTIONS should be ignored
|
||||
copy(uuid2[20:], randByte(4))
|
||||
input.EmitBytes([]byte("1 " + string(uuid2) + " 1\n" + "GET / HTTP/1.1\r\n\r\n"))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if output.(*HTTPOutput).activeWorkers != 2 {
|
||||
t.Error("Should have only 2 workers", output.(*HTTPOutput).activeWorkers)
|
||||
}
|
||||
|
||||
emitter.Close()
|
||||
|
||||
Settings.RecognizeTCPSessions = false
|
||||
}
|
||||
|
||||
func BenchmarkHTTPOutput(b *testing.B) {
|
||||
@@ -138,10 +206,14 @@ func BenchmarkHTTPOutput(b *testing.B) {
|
||||
input := NewTestInput()
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []io.Reader{input},
|
||||
Outputs: []io.Writer{output},
|
||||
}
|
||||
plugins.All = append(plugins.All, input, output)
|
||||
|
||||
go Start(quit)
|
||||
emitter := NewEmitter(quit)
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
wg.Add(1)
|
||||
@@ -149,6 +221,5 @@ func BenchmarkHTTPOutput(b *testing.B) {
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user