mirror of
https://github.com/buger/goreplay.git
synced 2026-10-19 16:15:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5098e77a9a | ||
|
|
1fdcc54121 | ||
|
|
5378ecca71 | ||
|
|
9d861af3a2 | ||
|
|
5c272661e9 | ||
|
|
a4c40cd8b4 | ||
|
|
d23848167d | ||
|
|
44506785cb | ||
|
|
16c5b6f15b | ||
|
|
5cf8f7dfd0 | ||
|
|
2b522285c6 | ||
|
|
5dcec259d6 | ||
|
|
766bbc6127 | ||
|
|
6d2a895db3 | ||
|
|
d81e39d25c | ||
|
|
ed94985346 | ||
|
|
f378def0b3 | ||
|
|
cd83e6f9ba | ||
|
|
60ddd535bc | ||
|
|
9ded255788 | ||
|
|
323f85bcfb | ||
|
|
07a3c191d7 | ||
|
|
4f3354507c | ||
|
|
661dd2026f | ||
|
|
67505d99d8 | ||
|
|
ac0438995c | ||
|
|
b5aa7d7c2d | ||
|
|
8daa7fdcab | ||
|
|
1fea397d54 | ||
|
|
abde6ecfcb | ||
|
|
43262f3336 | ||
|
|
fe91a20629 | ||
|
|
3c81723ac3 | ||
|
|
929e027907 | ||
|
|
f0b96af420 | ||
|
|
f3ea30c6df | ||
|
|
d5a1eb0b9d | ||
|
|
02bf7a86de | ||
|
|
cbc27bd662 | ||
|
|
16d7f23d98 | ||
|
|
6533b50ab0 | ||
|
|
9309fb86ef | ||
|
|
76d5e22ba1 | ||
|
|
81d3508653 | ||
|
|
3c45595a96 | ||
|
|
6974a6e278 | ||
|
|
33cdcd4d13 | ||
|
|
9550b31fde | ||
|
|
6cb4c265dd | ||
|
|
e54f77f229 | ||
|
|
5d2f6eceea | ||
|
|
a6e29c684c | ||
|
|
78456d6cd1 | ||
|
|
c4037e0c57 | ||
|
|
7cff78174a | ||
|
|
b53fba0415 | ||
|
|
d6c776fa0c | ||
|
|
ae35f88004 | ||
|
|
8da4e54cbb | ||
|
|
7790b33cea | ||
|
|
a971d8a843 | ||
|
|
3e763c43cf | ||
|
|
ea94dd0162 | ||
|
|
d96e3393ec | ||
|
|
69424bf670 | ||
|
|
211be0cddd | ||
|
|
d1091102ba | ||
|
|
0d445722b9 | ||
|
|
879ed48b80 | ||
|
|
ddd5190622 | ||
|
|
a18079fa84 | ||
|
|
1a36374c2d | ||
|
|
41c1421a1b | ||
|
|
4f2341b2d3 | ||
|
|
56f0a3fcc0 | ||
|
|
70e7d8dd29 | ||
|
|
bb319b5fd2 | ||
|
|
491541c2df | ||
|
|
2990fab635 | ||
|
|
8046468ea4 | ||
|
|
0009555d96 | ||
|
|
5098e388a2 | ||
|
|
5667cd35c6 | ||
|
|
9ab8b3c113 | ||
|
|
5e526af47b | ||
|
|
3a7badc508 | ||
|
|
9a76e354c5 | ||
|
|
d6b5c675cf | ||
|
|
de6c41c731 | ||
|
|
08933e148a | ||
|
|
259b70a7df | ||
|
|
35854196f0 | ||
|
|
a9be1811ea | ||
|
|
9185453453 | ||
|
|
87ad851ad8 | ||
|
|
ce193d0d4f | ||
|
|
f92a0b1242 | ||
|
|
f6864d05a0 | ||
|
|
3316447bf5 | ||
|
|
961d5a754c | ||
|
|
dd68d07c75 | ||
|
|
5c46db9220 | ||
|
|
ed7c5210be | ||
|
|
00ad6bd92b | ||
|
|
587b9ad2d5 | ||
|
|
083c97a9c4 | ||
|
|
8f2e9bd1dc | ||
|
|
a79e54c5f1 | ||
|
|
27f83a6c0c | ||
|
|
5158f75dc8 | ||
|
|
126957c599 | ||
|
|
d62eccb655 | ||
|
|
5cc4228dcd | ||
|
|
5e6f7e03a7 | ||
|
|
0a30582131 | ||
|
|
5c21600bf9 | ||
|
|
107982394e | ||
|
|
ac3e209787 | ||
|
|
e5d9f73937 | ||
|
|
df3b410e98 | ||
|
|
125d9db834 | ||
|
|
6147e14f88 | ||
|
|
fa003f5fec | ||
|
|
28540f43a1 | ||
|
|
6158ee3025 | ||
|
|
0859642b38 | ||
|
|
32081b2d17 | ||
|
|
6647481fe0 | ||
|
|
0551b19ddd | ||
|
|
a5f2d2fd6d | ||
|
|
f1c341e2ac | ||
|
|
becb4b958f | ||
|
|
db7f5c6a9c | ||
|
|
7af0eb2d65 | ||
|
|
32d0557a28 | ||
|
|
6ba0bfc614 | ||
|
|
34fe137d19 | ||
|
|
bb74704f9d | ||
|
|
d0873d1453 | ||
|
|
313c382adb | ||
|
|
6b9c9b91e3 | ||
|
|
531bd1da67 | ||
|
|
acaf935cae | ||
|
|
0fe72cc432 | ||
|
|
f812120dfc | ||
|
|
e64aac9364 | ||
|
|
8db14e334d | ||
|
|
b38cb458ff | ||
|
|
d380b71945 | ||
|
|
fd5388b6c9 | ||
|
|
48492dc8cb | ||
|
|
29035d4518 | ||
|
|
1ce10249be | ||
|
|
5a23573cfe | ||
|
|
413f50129f | ||
|
|
686d7436ae | ||
|
|
18c0473239 | ||
|
|
4d00ff8757 | ||
|
|
8f73233717 | ||
|
|
07c65117ad | ||
|
|
31fa612fef | ||
|
|
9ddb718525 | ||
|
|
4f114f067b | ||
|
|
9a0d9166c3 | ||
|
|
bc48e1ad49 | ||
|
|
53c27cf374 | ||
|
|
a859ae02ad | ||
|
|
2b17989fd9 | ||
|
|
96fd713453 | ||
|
|
43fd14b561 | ||
|
|
72b15db93f | ||
|
|
557cdbebb6 | ||
|
|
925ff151dc | ||
|
|
481e4f2e74 | ||
|
|
416586a365 | ||
|
|
a08ecc79fc | ||
|
|
decb9483a5 | ||
|
|
21c544ef67 | ||
|
|
122f06f6c2 | ||
|
|
3d3c6a34b1 | ||
|
|
04d727915d | ||
|
|
1e7b603ae0 | ||
|
|
41294f9470 | ||
|
|
3e86456e36 | ||
|
|
98f941b69a | ||
|
|
674ffc6202 | ||
|
|
4f03f81d04 | ||
|
|
dcb8a1dee4 | ||
|
|
451a1cd11a | ||
|
|
6fe7ba8e86 | ||
|
|
534e9b29fa | ||
|
|
390f75fd3c | ||
|
|
a059648008 | ||
|
|
b2e1eea6bf | ||
|
|
92ba321569 | ||
|
|
2b497e6177 | ||
|
|
09f9cac6fb | ||
|
|
d05bc0d5e9 | ||
|
|
1c0182188c | ||
|
|
cef8b75496 | ||
|
|
e9fd5eb273 | ||
|
|
85fbfff308 | ||
|
|
a7aaf7df65 | ||
|
|
c1c87ed4e2 | ||
|
|
072f332956 | ||
|
|
7c68920a3b | ||
|
|
b6351a8525 | ||
|
|
f557d412c4 | ||
|
|
ed5c2215f5 | ||
|
|
208faec7d2 | ||
|
|
ffe1646d8e | ||
|
|
ae293320ad | ||
|
|
eb7885cd56 | ||
|
|
b6977df90d | ||
|
|
15417894a9 | ||
|
|
7f49314ec7 | ||
|
|
80572f5f18 | ||
|
|
b7567bbd15 | ||
|
|
e4b7c8c763 | ||
|
|
62ced2e1ba | ||
|
|
79ed6a7548 | ||
|
|
3793a84e59 | ||
|
|
f2cf81c5bf | ||
|
|
aae57733a8 | ||
|
|
00df525b6a | ||
|
|
7864808e5e | ||
|
|
58c7e12d1f | ||
|
|
de143e4268 | ||
|
|
67cfca0ede | ||
|
|
43920a96c0 | ||
|
|
4cf3bf0ae4 | ||
|
|
095c717f6a | ||
|
|
abc44c2fa5 | ||
|
|
2d66ccc62d | ||
|
|
b257231d51 | ||
|
|
1cab1ecb0e | ||
|
|
983bc728fa | ||
|
|
4b592e0e2d | ||
|
|
1637dd491f | ||
|
|
9e63fb5b36 | ||
|
|
04a8cceb35 | ||
|
|
6ddf4df21a | ||
|
|
51f5a3e9ec | ||
|
|
4c0c9e97c5 | ||
|
|
4f64b71adb | ||
|
|
be7e27010d | ||
|
|
758995d8f5 | ||
|
|
5f883e08ce | ||
|
|
3a43e697d5 | ||
|
|
468d94610f | ||
|
|
d0266303d1 | ||
|
|
82ed628d45 | ||
|
|
97dd1520f1 | ||
|
|
972a80356d | ||
|
|
91a0282755 | ||
|
|
2c03d3ee28 | ||
|
|
b2f90789d2 | ||
|
|
e68332b897 | ||
|
|
cf0dfecac2 | ||
|
|
4f9c983bc7 | ||
|
|
988c6abdad | ||
|
|
864723fe47 | ||
|
|
2c2d5439bf | ||
|
|
c04b2528b8 | ||
|
|
58944fd42d | ||
|
|
6e125a7b7b | ||
|
|
1a7cf2bbab | ||
|
|
e022fdff98 | ||
|
|
c0dc1b3a53 | ||
|
|
674d6036d1 | ||
|
|
6f6c6aba95 | ||
|
|
2a6e9f7a92 | ||
|
|
007e0f3019 | ||
|
|
115a37e0e9 | ||
|
|
b2c87ff2a4 | ||
|
|
3a1efccbca | ||
|
|
a2873116a3 | ||
|
|
ccb309170f | ||
|
|
defc017ade | ||
|
|
9e2f7feec7 | ||
|
|
c56bbd98ce | ||
|
|
787a906bb8 | ||
|
|
d608d18057 | ||
|
|
cd27667246 | ||
|
|
bae1a84f40 | ||
|
|
057e642b87 | ||
|
|
c94cc88608 | ||
|
|
731a3745a4 | ||
|
|
c4307b750a | ||
|
|
9d341738de | ||
|
|
c8d623580a | ||
|
|
2e29e3ab9e | ||
|
|
bf786e5922 | ||
|
|
a9a08860b4 | ||
|
|
c6c021d65f | ||
|
|
49da801b33 | ||
|
|
a4a4c605a5 | ||
|
|
11d9531829 | ||
|
|
a6af811c50 | ||
|
|
7bf6ca4c87 | ||
|
|
31aef9ee1d | ||
|
|
41fe11c12f | ||
|
|
4720070996 | ||
|
|
dbfea7ceac | ||
|
|
c49c19428d | ||
|
|
7bf2015e90 | ||
|
|
db14154eb9 | ||
|
|
b52d562dad | ||
|
|
9461217a0e | ||
|
|
40d835e143 | ||
|
|
c97f02ff4c | ||
|
|
62254b6265 | ||
|
|
d5535ee7f3 | ||
|
|
959ac29661 | ||
|
|
b5c2d7747c | ||
|
|
610552be88 | ||
|
|
c250c3e30c | ||
|
|
ce5ee74d74 | ||
|
|
c4d0eb0ae1 | ||
|
|
840ca61de9 | ||
|
|
5cb3562796 | ||
|
|
384b3c5614 | ||
|
|
e97827d600 | ||
|
|
3eed7c2546 | ||
|
|
fa8f612a4f | ||
|
|
b00ea39d3e | ||
|
|
06b4892b33 | ||
|
|
06b08105bf | ||
|
|
8ccc0249ca | ||
|
|
e9bb6ef8f3 | ||
|
|
adf7ed6b84 | ||
|
|
0dc315b343 | ||
|
|
48ae0e6812 | ||
|
|
0d398dbf3d | ||
|
|
e373073742 | ||
|
|
296034ef00 | ||
|
|
a786c78d26 | ||
|
|
ed68b27153 | ||
|
|
e6fe38cfda | ||
|
|
441f0ade04 | ||
|
|
8d8c10f3ea | ||
|
|
fc52decebf | ||
|
|
f5d74e84f6 | ||
|
|
983eb7c6a8 | ||
|
|
8edbb7fb08 | ||
|
|
89f1b50b8d | ||
|
|
9b0153f45f | ||
|
|
832bff26d1 | ||
|
|
3d3cb1dbf5 | ||
|
|
ca952b7a8b | ||
|
|
872a4dcae9 | ||
|
|
b70290274e | ||
|
|
787d92d94a | ||
|
|
ae5f7e48b0 | ||
|
|
29deb996bd | ||
|
|
02fe5a976f | ||
|
|
c8e39b8f47 | ||
|
|
c5150cd911 | ||
|
|
7a657e273e | ||
|
|
cc2dd53de8 | ||
|
|
cad94ead29 | ||
|
|
28f7078dbb | ||
|
|
841bba116a | ||
|
|
3a3ae5b283 | ||
|
|
3299955888 | ||
|
|
3aa04783f9 | ||
|
|
5a7c817f0b | ||
|
|
b800896b04 | ||
|
|
e7a50ac5d4 | ||
|
|
0fb1d83284 | ||
|
|
1784516771 | ||
|
|
9fa5ef74fb | ||
|
|
d67bea5e10 | ||
|
|
263d1347a0 | ||
|
|
19008c2a2a | ||
|
|
49cdbbe41b | ||
|
|
59a2b7ca1d | ||
|
|
9a32742653 | ||
|
|
38b171d357 | ||
|
|
90261550c4 | ||
|
|
7814bd8bc5 | ||
|
|
40cd01630b | ||
|
|
19eadf49b9 | ||
|
|
227e95467a | ||
|
|
14d9ca6d54 | ||
|
|
b9c97eabaf | ||
|
|
d9c54d31ad | ||
|
|
796f192e71 | ||
|
|
eb6d0cddd9 | ||
|
|
f82dade127 | ||
|
|
5f35f41637 | ||
|
|
26ddfcd3cd | ||
|
|
ac3f255d38 | ||
|
|
68ba87c19e | ||
|
|
fd9f6cd056 | ||
|
|
9bba1296bd | ||
|
|
f7b3adc8cd | ||
|
|
dd806640f9 | ||
|
|
824c51248c | ||
|
|
f8a8444b27 | ||
|
|
c811dc017e | ||
|
|
c2424b2b8f | ||
|
|
ccf6723743 | ||
|
|
fa9c9c3387 | ||
|
|
8620e4a467 | ||
|
|
457e92ed11 | ||
|
|
9da6f908fd | ||
|
|
4406794a28 | ||
|
|
92c8dd19f4 | ||
|
|
860189b1c5 | ||
|
|
0f27a45fbb | ||
|
|
8ffdd10988 | ||
|
|
6a136b4686 | ||
|
|
af3a7a7541 | ||
|
|
602448accd | ||
|
|
8b9c72ef9e | ||
|
|
95d3676d83 | ||
|
|
72952fddd2 | ||
|
|
faa3cb7307 | ||
|
|
ce76453fa6 | ||
|
|
9dddab58d3 | ||
|
|
c42a42928b | ||
|
|
2a4efa91af | ||
|
|
2fd36f03f1 | ||
|
|
3b890c6d7f | ||
|
|
bc4fabd0da | ||
|
|
24a240c819 | ||
|
|
2f049dddbd | ||
|
|
e7c1d86bfa | ||
|
|
714307890e | ||
|
|
69df96e55d | ||
|
|
9c6802f5e8 | ||
|
|
d54da99667 | ||
|
|
f4af3a9bd4 | ||
|
|
1f9c9a6154 | ||
|
|
9e175434d6 | ||
|
|
9a126103fc | ||
|
|
9a978422b2 | ||
|
|
e02c145ac6 | ||
|
|
7d482d47de | ||
|
|
f7cf05c257 | ||
|
|
4af1230d83 | ||
|
|
18597d9e7a | ||
|
|
fe02bfa8c5 | ||
|
|
48d4406893 | ||
|
|
f07c7f5eaf | ||
|
|
db4b2c26b0 | ||
|
|
8ecda0fdb5 | ||
|
|
06befe3b4b | ||
|
|
8c41c01d4b | ||
|
|
4e41151963 | ||
|
|
18ec957e62 | ||
|
|
857a9b4835 | ||
|
|
12ae1455f8 | ||
|
|
da2478ca3c | ||
|
|
c32deec006 | ||
|
|
f5a8274fcd | ||
|
|
d1708ff1f3 | ||
|
|
fd642bded1 | ||
|
|
40203d2519 | ||
|
|
fd27e13e85 | ||
|
|
d46e587c08 | ||
|
|
fa2dedceb3 | ||
|
|
983e2a0e79 | ||
|
|
42173e4687 | ||
|
|
8757373a7a | ||
|
|
b6e7b18803 | ||
|
|
e21f9b024d | ||
|
|
20901ec6ae | ||
|
|
327ee32395 | ||
|
|
e32b985df2 | ||
|
|
737ea9fc68 | ||
|
|
509b27c476 | ||
|
|
94199fcb25 | ||
|
|
bba9637eed | ||
|
|
685c998e8e | ||
|
|
8adeda9a98 | ||
|
|
9077656bdf | ||
|
|
830ae237c9 | ||
|
|
88dd2602df | ||
|
|
27582841fe | ||
|
|
b53218e729 | ||
|
|
54e9de5088 | ||
|
|
5782d486f9 | ||
|
|
8b66a99665 | ||
|
|
30547f2a41 | ||
|
|
140267b6a6 | ||
|
|
e4807b7abb | ||
|
|
20ea55e6e7 | ||
|
|
47b498d4d9 | ||
|
|
4305b72608 | ||
|
|
578d785c27 | ||
|
|
6946fc6036 | ||
|
|
8535872b10 | ||
|
|
a1f9cc501b | ||
|
|
f07586a12e | ||
|
|
fa684d4645 | ||
|
|
c3330cb4be | ||
|
|
3cb98aaf94 | ||
|
|
cbc4204e50 | ||
|
|
207eea1ba7 | ||
|
|
de93abeab8 | ||
|
|
9908e8d4d9 | ||
|
|
e7b2866ca9 | ||
|
|
b562a6b62d | ||
|
|
0ac36b4fbf | ||
|
|
16ba3d1514 | ||
|
|
5b6b8a53c1 | ||
|
|
1a35a2c651 | ||
|
|
5a4c27dc9e | ||
|
|
335c86c6e9 | ||
|
|
f1eedc78a1 | ||
|
|
cfb65c06ac | ||
|
|
02abcfdf74 | ||
|
|
1e08ec3953 | ||
|
|
db369cfc89 | ||
|
|
54a3d73a98 | ||
|
|
ae792afa5e | ||
|
|
700523e0c3 | ||
|
|
95841d597e | ||
|
|
5ccebd008d | ||
|
|
e5cb5d959d | ||
|
|
4f7b201ef1 | ||
|
|
6bb24697b2 | ||
|
|
719fd471f3 | ||
|
|
2c34f291fb |
@@ -0,0 +1 @@
|
||||
*.tar.gz
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
*.swp
|
||||
*.gor
|
||||
|
||||
*.out
|
||||
|
||||
*.bin
|
||||
|
||||
*.gz
|
||||
|
||||
*.class
|
||||
|
||||
*.test
|
||||
.idea
|
||||
gor
|
||||
+7
-3
@@ -1,3 +1,7 @@
|
||||
language: go
|
||||
go: 1.1
|
||||
script: sudo -E bash -c "source /etc/profile && gvm use go1.1 && export GOPATH=$HOME/gopath:$GOPATH && go test"
|
||||
language: go
|
||||
go:
|
||||
- 1.6.1
|
||||
|
||||
install: go get ./...
|
||||
|
||||
script: go test ./... -v -timeout 60s -race
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
v0.3.5 - 15 Sep 2013
|
||||
* Significally improved test coverage
|
||||
* Fixed bug with redirect replay https://github.com/buger/gor/pull/15
|
||||
* Added limit on listener side
|
||||
* Improved stability (catch and log panic, instead of exiting)
|
||||
* Added License file
|
||||
|
||||
v0.3.3 - 22 Jun 2013
|
||||
* Using TCP instead of UDP for communication between Listener and Replay
|
||||
* Significally improved performance
|
||||
* Fixed bugs causing locking and message droping (concurrency issues)
|
||||
* Rewrote concurrency model to use more channels
|
||||
|
||||
v0.3 - 10 Jun 2013
|
||||
* Use RAW_SOCKETS instead of tcpdump
|
||||
* Own TCP stack
|
||||
* All HTTP request types support
|
||||
* Simplified request parsing
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
FROM google/golang:1.4
|
||||
|
||||
RUN cd /goroot/src/ && GOOS=linux GOARCH=386 ./make.bash --no-clean
|
||||
|
||||
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 /gopath/src/github.com/buger/gor/
|
||||
ADD . /gopath/src/github.com/buger/gor/
|
||||
|
||||
RUN javac -cp /tmp/commons-io-2.4/commons-io-2.4.jar ./examples/middleware/echo.java
|
||||
|
||||
RUN apt-get install graphviz -y
|
||||
|
||||
RUN go get -u github.com/golang/lint/golint
|
||||
RUN go get
|
||||
@@ -0,0 +1,65 @@
|
||||
gor & elasticsearch
|
||||
===================
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
- elasticsearch
|
||||
- kibana (Get it here: http://www.elasticsearch.org/overview/kibana/)
|
||||
- gor
|
||||
|
||||
|
||||
elasticsearch
|
||||
-------------
|
||||
|
||||
The default elasticsearch configuration is just fine for most workloads. You won't need clustering, sharding or something like that.
|
||||
|
||||
In this example we're installing it on our gor replay server which gives us the elasticsearch listener on _http://localhost:9200_
|
||||
|
||||
|
||||
kibana
|
||||
------
|
||||
|
||||
Kibana (elasticsearch analytics web-ui) is just as simple.
|
||||
Download it, extract it and serve it via a simple webserver.
|
||||
(Could be nginx or apache)
|
||||
|
||||
You could also use a shell, ```cd``` into the kibana directory and start a little quick and dirty python webserver with:
|
||||
|
||||
```
|
||||
python -m SimpleHTTPServer 8000
|
||||
```
|
||||
|
||||
In this example we're also choosing the gor replay server as our kibana host. If you choose a different server you'll have to point kibana to your elasticsearch host.
|
||||
|
||||
|
||||
gor
|
||||
---
|
||||
|
||||
Start your gor replay server with elasticsearch option:
|
||||
|
||||
```
|
||||
./gor --input-raw :8000 --output-http http://staging.com --output-http-elasticsearch localhost:9200/gor
|
||||
```
|
||||
|
||||
|
||||
(You don't have to create the index upfront. That will be done for you automatically)
|
||||
|
||||
|
||||
Now visit your kibana url, load the predefined dashboard from the gist https://gist.github.com/gottwald/b2c875037f24719a9616 and watch the data rush in.
|
||||
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
The replay process may complain about __too many open files__.
|
||||
That's because your typical linux shell has a small open files soft limit at 1024.
|
||||
You can easily raise that when you do this before starting your _gor replay_ process:
|
||||
|
||||
```
|
||||
ulimit -n 64000
|
||||
```
|
||||
|
||||
Please be aware, this is not a permanent setting. It's just valid for the following jobs you start from that shell.
|
||||
|
||||
We reached the 1024 limit in our tests with a ubuntu box replaying about 9000 requests per minute. (We had very slow responses there, should be way more with fast responses)
|
||||
@@ -0,0 +1,56 @@
|
||||
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 = /gopath/src/github.com/buger/gor/
|
||||
|
||||
release: release-x86 release-x64
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
dbuild:
|
||||
docker build -t gor .
|
||||
|
||||
|
||||
profile:
|
||||
go build && ./gor --output-http="http://localhost:9000" --input-dummy 0 --input-raw :9000 --input-http :9000 --memprofile=./mem.out --cpuprofile=./cpu.out --stats --output-http-stats --output-http-timeout 100ms
|
||||
|
||||
dlint:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t -i --env GORACE="halt_on_error=1" gor golint $(PKG)
|
||||
|
||||
drace:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t -i --env GORACE="halt_on_error=1" gor go test ./... $(ARGS) -v -race -timeout 15s
|
||||
|
||||
dtest:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t -i gor go test ./... -timeout 60s $(ARGS) -v
|
||||
|
||||
dcover:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t -i --env GORACE="halt_on_error=1" gor go test $(ARGS) -race -v -timeout 15s -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
dfmt:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t -i gor go fmt ./...
|
||||
|
||||
dvet:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t -i gor go vet
|
||||
|
||||
dbench:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t -i gor go test -v -run NOT_EXISTING -bench HTTP
|
||||
|
||||
# Used mainly for debugging, because docker container do not have access to parent machine ports
|
||||
drun:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t -i gor go run $(SOURCE) --input-dummy=0 --output-http="http://localhost:9000" --input-raw :9000 --input-http :9000 --verbose --debug --middleware "./examples/middleware/echo.sh"
|
||||
|
||||
drun-2:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t -i gor go run $(SOURCE) --input-file ./fixtures/requests.gor --output-dummy=0
|
||||
|
||||
drecord:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t -i gor go run $(SOURCE) --input-dummy=0 --output-file=requests.gor --verbose --debug
|
||||
|
||||
dreplay:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t -i gor go run $(SOURCE) --input-file=requests.bin --output-tcp=:9000 --verbose -h
|
||||
|
||||
dbash:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t -i gor /bin/bash
|
||||
@@ -0,0 +1,4 @@
|
||||
web: python -m SimpleHTTPServer 8000
|
||||
replayed_web: python -m SimpleHTTPServer 8001
|
||||
listener: sudo -E go run ./bin/gor.go --input-raw :8000 --output-tcp :8002 --verbose
|
||||
replay: go run ./bin/gor.go --input-tcp :8002 --output-http localhost:8001 --verbose
|
||||
@@ -1,113 +1,487 @@
|
||||
[](https://travis-ci.org/buger/gor)
|
||||
|
||||
## About
|
||||
|
||||
Gor is a simple http traffic replication tool written in Go.
|
||||
Its main goal is to replay traffic from production servers to staging and dev environments.
|
||||
|
||||
|
||||
Now you can test your code on real user sessions in an automated and repeatable fashion.
|
||||
**No more falling down in production!**
|
||||
|
||||
Gor consists of 2 parts: listener and replay servers.
|
||||
|
||||
The listener server catches http traffic from a given port in real-time
|
||||
and sends it to the replay server.
|
||||
The replay server forwards traffic to a given address.
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
## Basic example
|
||||
|
||||
```bash
|
||||
# Run on servers where you want to catch traffic. You can run it on each `web` machine.
|
||||
sudo gor listen -p 80 -r replay.server.local:28020
|
||||
|
||||
# Replay server (replay.server.local).
|
||||
gor replay -f http://staging.server -p 28020
|
||||
```
|
||||
|
||||
## Advanced use
|
||||
|
||||
### Rate limiting
|
||||
Both replay and listener supports rate limiting. It can be useful if you want
|
||||
forward only part of production traffic and not overload your staging
|
||||
environment. You can specify your desired requests per second using the
|
||||
"|" operator after the server address:
|
||||
|
||||
```
|
||||
# staging.server will not get more than 10 requests per second
|
||||
gor replay -f "http://staging.server|10"
|
||||
```
|
||||
|
||||
```
|
||||
# replay server will not get more than 10 requests per second
|
||||
# useful for high-load environments
|
||||
gor listen -p 8080 -r "replay.server.local:28020|10"
|
||||
```
|
||||
|
||||
### Forward to multiple addresses
|
||||
|
||||
You can forward traffic to multiple endpoints. Just separate the addresses by comma.
|
||||
```
|
||||
gor replay -f "http://staging.server|10,http://dev.server|5"
|
||||
```
|
||||
|
||||
## Additional help
|
||||
```
|
||||
$ gor listen -h
|
||||
Usage of ./bin/gor-linux:
|
||||
-i="any": By default it try to listen on all network interfaces.To get list of interfaces run `ifconfig`
|
||||
-p=80: Specify the http server port whose traffic you want to capture
|
||||
-r="localhost:28020": Address of replay server.
|
||||
```
|
||||
|
||||
```
|
||||
$ gor replay -h
|
||||
Usage of ./bin/gor-linux:
|
||||
-f="http://localhost:8080": http address to forward traffic.
|
||||
You can limit requests per second by adding `|#{num}` after address.
|
||||
If you have multiple addresses with different limits. For example: http://staging.example.com|100,http://dev.example.com|10
|
||||
-ip="0.0.0.0": ip addresses to listen on
|
||||
-p=28020: specify port number
|
||||
```
|
||||
|
||||
## Pre-build binaries
|
||||
|
||||
[Download binaries (linux 32/64, darwin)](https://drive.google.com/folderview?id=0B46uay48NwcfWFowc1E4a1BISVU&usp=sharing)
|
||||
|
||||
## 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 gor.go` to get binary, or `go run gor.go` to build and run (useful for development)
|
||||
|
||||
## 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 `gor listener` requires sudo or root access?
|
||||
Listener works by sniffing traffic from a given port. It's accessible
|
||||
only by using sudo or root access.
|
||||
|
||||
### Do you support all http request types?
|
||||
Yes. ~~Right now it supports only "GET" requests.~~
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork it
|
||||
2. Create your feature branch (git checkout -b my-new-feature)
|
||||
3. Commit your changes (git commit -am 'Added some feature')
|
||||
4. Push to the branch (git push origin my-new-feature)
|
||||
5. Create new Pull Request
|
||||
|
||||
## TODO
|
||||
|
||||
Use buffering for request throttling instead of simple rate limiting.
|
||||
|
||||
Better stats
|
||||
|
||||
Optimize for load testing cases
|
||||
[](https://travis-ci.org/buger/gor)
|
||||
|
||||
## About
|
||||
|
||||
Gor is an open-source tool for capturing and replaying live HTTP traffic into a test environment in order to continuously test your system with real data. It can be used to increase confidence in code deployments, configuration changes and infrastructure changes.
|
||||
|
||||
|
||||
Now you can test your code on real user sessions in an automated and repeatable fashion.
|
||||
**No more falling down in production!**
|
||||
|
||||
Here is basic workflow: The listener server catches http traffic and sends it to the replay server or saves to file. The replay server forwards traffic to a given address.
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
### 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
|
||||
|
||||
# Replay server (replay.local).
|
||||
gor --input-tcp replay.local:28020 --output-http http://staging.com
|
||||
```
|
||||
|
||||
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`
|
||||
|
||||
### 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"
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
## 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://blog.thecodingmachine.com/content/solving-too-many-open-files-exception-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
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork it
|
||||
2. Create your feature branch (git checkout -b my-new-feature)
|
||||
3. Commit your changes (git commit -am 'Added some feature')
|
||||
4. Push to the branch (git push origin my-new-feature)
|
||||
5. Create new Pull Request
|
||||
|
||||
## Companies using Gor
|
||||
|
||||
* [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
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Package byteutils probvides helpers for working with byte slices
|
||||
package byteutils
|
||||
|
||||
// Cut elements from slice for a given range
|
||||
func Cut(a []byte, from, to int) []byte {
|
||||
copy(a[from:], a[to:])
|
||||
a = a[:len(a)-to+from]
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// Insert new slice at specified position
|
||||
func Insert(a []byte, i int, b []byte) []byte {
|
||||
a = append(a, make([]byte, len(b))...)
|
||||
copy(a[i+len(b):], a[i:])
|
||||
copy(a[i:i+len(b)], b)
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// Replace function unlike bytes.Replace allows you to specify range
|
||||
func Replace(a []byte, from, to int, new []byte) []byte {
|
||||
lenDiff := len(new) - (to - from)
|
||||
|
||||
if lenDiff > 0 {
|
||||
// Extend if new segment bigger
|
||||
a = append(a, make([]byte, lenDiff)...)
|
||||
copy(a[to+lenDiff:], a[to:])
|
||||
copy(a[from:from+len(new)], new)
|
||||
|
||||
return a
|
||||
} else if lenDiff < 0 {
|
||||
copy(a[from:], new)
|
||||
copy(a[from+len(new):], a[to:])
|
||||
return a[:len(a)+lenDiff]
|
||||
} else { // same size
|
||||
copy(a[from:], new)
|
||||
return a
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package byteutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCut(t *testing.T) {
|
||||
if !bytes.Equal(Cut([]byte("123456"), 2, 4), []byte("1256")) {
|
||||
t.Error("Should properly cut")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsert(t *testing.T) {
|
||||
if !bytes.Equal(Insert([]byte("123456"), 2, []byte("abcd")), []byte("12abcd3456")) {
|
||||
t.Error("Should insert into middle of slice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplace(t *testing.T) {
|
||||
if !bytes.Equal(Replace([]byte("123456"), 2, 4, []byte("ab")), []byte("12ab56")) {
|
||||
t.Error("Should replace when same length")
|
||||
}
|
||||
|
||||
if !bytes.Equal(Replace([]byte("123456"), 2, 4, []byte("abcd")), []byte("12abcd56")) {
|
||||
t.Error("Should replace when replacement length bigger")
|
||||
}
|
||||
|
||||
if !bytes.Equal(Replace([]byte("123456"), 2, 5, []byte("ab")), []byte("12ab6")) {
|
||||
t.Error("Should replace when replacement length bigger")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/buger/elastigo/api"
|
||||
"github.com/buger/elastigo/core"
|
||||
"github.com/buger/gor/proto"
|
||||
"log"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ESUriErorr struct{}
|
||||
|
||||
func (e *ESUriErorr) Error() string {
|
||||
return "Wrong ElasticSearch URL format. Expected to be: host:port/index_name"
|
||||
}
|
||||
|
||||
type ESPlugin struct {
|
||||
Active bool
|
||||
ApiPort string
|
||||
Host string
|
||||
Index string
|
||||
indexor *core.BulkIndexer
|
||||
done chan bool
|
||||
}
|
||||
|
||||
type ESRequestResponse struct {
|
||||
ReqUrl []byte `json:"Req_URL"`
|
||||
ReqMethod []byte `json:"Req_Method"`
|
||||
ReqUserAgent []byte `json:"Req_User-Agent"`
|
||||
ReqAcceptLanguage []byte `json:"Req_Accept-Language,omitempty"`
|
||||
ReqAccept []byte `json:"Req_Accept,omitempty"`
|
||||
ReqAcceptEncoding []byte `json:"Req_Accept-Encoding,omitempty"`
|
||||
ReqIfModifiedSince []byte `json:"Req_If-Modified-Since,omitempty"`
|
||||
ReqConnection []byte `json:"Req_Connection,omitempty"`
|
||||
ReqCookies []byte `json:"Req_Cookies,omitempty"`
|
||||
RespStatus []byte `json:"Resp_Status"`
|
||||
RespStatusCode []byte `json:"Resp_Status-Code"`
|
||||
RespProto []byte `json:"Resp_Proto,omitempty"`
|
||||
RespContentLength []byte `json:"Resp_Content-Length,omitempty"`
|
||||
RespContentType []byte `json:"Resp_Content-Type,omitempty"`
|
||||
RespTransferEncoding []byte `json:"Resp_Transfer-Encoding,omitempty"`
|
||||
RespContentEncoding []byte `json:"Resp_Content-Encoding,omitempty"`
|
||||
RespExpires []byte `json:"Resp_Expires,omitempty"`
|
||||
RespCacheControl []byte `json:"Resp_Cache-Control,omitempty"`
|
||||
RespVary []byte `json:"Resp_Vary,omitempty"`
|
||||
RespSetCookie []byte `json:"Resp_Set-Cookie,omitempty"`
|
||||
Rtt int64 `json:"RTT"`
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// Parse ElasticSearch URI
|
||||
//
|
||||
// Proper format is: host:port/index_name
|
||||
func parseURI(URI string) (err error, host string, port string, index string) {
|
||||
rURI := regexp.MustCompile("(.+):([0-9]+)/(.+)")
|
||||
match := rURI.FindAllStringSubmatch(URI, -1)
|
||||
|
||||
if len(match) == 0 {
|
||||
err = new(ESUriErorr)
|
||||
} else {
|
||||
host = match[0][1]
|
||||
port = match[0][2]
|
||||
index = match[0][3]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ESPlugin) Init(URI string) {
|
||||
var err error
|
||||
|
||||
err, p.Host, p.ApiPort, p.Index = parseURI(URI)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Can't initialize ElasticSearch plugin.", err)
|
||||
}
|
||||
|
||||
api.Domain = p.Host
|
||||
api.Port = p.ApiPort
|
||||
|
||||
p.indexor = core.NewBulkIndexerErrors(50, 60)
|
||||
p.done = make(chan bool)
|
||||
p.indexor.Run(p.done)
|
||||
|
||||
// Only start the ErrorHandler goroutine when in verbose mode
|
||||
// no need to burn ressources otherwise
|
||||
// go p.ErrorHandler()
|
||||
|
||||
log.Println("Initialized Elasticsearch Plugin")
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ESPlugin) IndexerShutdown() {
|
||||
p.done <- true
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ESPlugin) ErrorHandler() {
|
||||
for {
|
||||
errBuf := <-p.indexor.ErrorChannel
|
||||
log.Println(errBuf.Err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ESPlugin) RttDurationToMs(d time.Duration) int64 {
|
||||
sec := d / time.Second
|
||||
nsec := d % time.Second
|
||||
fl := float64(sec) + float64(nsec)*1e-6
|
||||
return int64(fl)
|
||||
}
|
||||
|
||||
func (p *ESPlugin) ResponseAnalyze(req, resp []byte, start, stop time.Time) {
|
||||
if len(resp) == 0 {
|
||||
// nil http response - skipped elasticsearch export for this request
|
||||
return
|
||||
}
|
||||
t := time.Now()
|
||||
rtt := p.RttDurationToMs(stop.Sub(start))
|
||||
|
||||
esResp := ESRequestResponse{
|
||||
ReqUrl: proto.Path(req),
|
||||
ReqMethod: proto.Method(req),
|
||||
ReqUserAgent: proto.Header(req, []byte("User-Agent")),
|
||||
ReqAcceptLanguage: proto.Header(req, []byte("Accept-Language")),
|
||||
ReqAccept: proto.Header(req, []byte("Accept")),
|
||||
ReqAcceptEncoding: proto.Header(req, []byte("Accept-Encoding")),
|
||||
ReqIfModifiedSince: proto.Header(req, []byte("If-Modified-Since")),
|
||||
ReqConnection: proto.Header(req, []byte("Connection")),
|
||||
ReqCookies: proto.Header(req, []byte("Cookie")),
|
||||
RespStatus: proto.Status(resp),
|
||||
RespStatusCode: proto.Status(resp),
|
||||
RespProto: proto.Method(resp),
|
||||
RespContentLength: proto.Header(resp, []byte("Content-Length")),
|
||||
RespContentType: proto.Header(resp, []byte("Content-Type")),
|
||||
RespTransferEncoding: proto.Header(resp, []byte("Transfer-Encoding")),
|
||||
RespContentEncoding: proto.Header(resp, []byte("Content-Encoding")),
|
||||
RespExpires: proto.Header(resp, []byte("Expires")),
|
||||
RespCacheControl: proto.Header(resp, []byte("Cache-Control")),
|
||||
RespVary: proto.Header(resp, []byte("Vary")),
|
||||
RespSetCookie: proto.Header(resp, []byte("Set-Cookie")),
|
||||
Rtt: rtt,
|
||||
Timestamp: t,
|
||||
}
|
||||
j, err := json.Marshal(&esResp)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
p.indexor.Index(p.Index, "RequestResponse", "", "", &t, j, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Start initialize loop for sending data from inputs to outputs
|
||||
func Start(stop chan int) {
|
||||
if Settings.middleware != "" {
|
||||
middleware := NewMiddleware(Settings.middleware)
|
||||
|
||||
for _, in := range Plugins.Inputs {
|
||||
middleware.ReadFrom(in)
|
||||
}
|
||||
|
||||
// We going only to read responses, so using same ReadFrom method
|
||||
for _, out := range Plugins.Outputs {
|
||||
if r, ok := out.(io.Reader); ok {
|
||||
middleware.ReadFrom(r)
|
||||
}
|
||||
}
|
||||
|
||||
go CopyMulty(middleware, Plugins.Outputs...)
|
||||
} else {
|
||||
for _, in := range Plugins.Inputs {
|
||||
go CopyMulty(in, Plugins.Outputs...)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CopyMulty copies from 1 reader to multiple writers
|
||||
func CopyMulty(src io.Reader, writers ...io.Writer) (err error) {
|
||||
buf := make([]byte, 5*1024*1024)
|
||||
wIndex := 0
|
||||
modifier := NewHTTPModifier(&Settings.modifierConfig)
|
||||
|
||||
for {
|
||||
nr, er := src.Read(buf)
|
||||
|
||||
if nr > 0 && len(buf) > nr {
|
||||
payload := buf[:nr]
|
||||
|
||||
_maxN := nr
|
||||
if nr > 500 {
|
||||
_maxN = 500
|
||||
}
|
||||
|
||||
if Settings.debug {
|
||||
Debug("[EMITTER] input:", string(payload[0:_maxN]), nr, "from:", src)
|
||||
}
|
||||
|
||||
if modifier != nil && isRequestPayload(payload) {
|
||||
headSize := bytes.IndexByte(payload, '\n') + 1
|
||||
body := payload[headSize:]
|
||||
originalBodyLen := len(body)
|
||||
body = modifier.Rewrite(body)
|
||||
|
||||
// If modifier tells to skip request
|
||||
if len(body) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if originalBodyLen != len(body) {
|
||||
payload = append(payload[:headSize], body...)
|
||||
}
|
||||
|
||||
if Settings.debug {
|
||||
Debug("[EMITTER] Rewrittern input:", len(payload), "First 500 bytes:", string(payload[0:_maxN]))
|
||||
}
|
||||
}
|
||||
|
||||
if Settings.splitOutput {
|
||||
// Simple round robin
|
||||
writers[wIndex].Write(payload)
|
||||
|
||||
wIndex++
|
||||
|
||||
if wIndex >= len(writers) {
|
||||
wIndex = 0
|
||||
}
|
||||
} else {
|
||||
for _, dst := range writers {
|
||||
dst.Write(payload)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if er == io.EOF {
|
||||
break
|
||||
}
|
||||
if er != nil {
|
||||
err = er
|
||||
break
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEmitter(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestEmitterRoundRobin(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
|
||||
var counter1, counter2 int32
|
||||
|
||||
output1 := NewTestOutput(func(data []byte) {
|
||||
atomic.AddInt32(&counter1, 1)
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
output2 := NewTestOutput(func(data []byte) {
|
||||
atomic.AddInt32(&counter2, 1)
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output1, output2}
|
||||
|
||||
Settings.splitOutput = true
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
|
||||
if counter1 == 0 || counter2 == 0 {
|
||||
t.Errorf("Round robin should split traffic equally: %d vs %d", counter1, counter2)
|
||||
}
|
||||
|
||||
Settings.splitOutput = false
|
||||
}
|
||||
|
||||
func BenchmarkEmitter(b *testing.B) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
public class echo {
|
||||
public static void main(String[] args) {
|
||||
if(args != null){
|
||||
for(String arg : args){
|
||||
System.out.println(arg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BufferedReader stdin = new BufferedReader(new InputStreamReader(
|
||||
System.in));
|
||||
String line = null;
|
||||
|
||||
try {
|
||||
while ((line = stdin.readLine()) != null) {
|
||||
|
||||
System.out.println(line);
|
||||
|
||||
}
|
||||
} catch (IOException e) {
|
||||
IOUtils.closeQuietly(stdin);
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env ruby
|
||||
# encoding: utf-8
|
||||
while data = STDIN.gets
|
||||
next unless data
|
||||
data = data.chomp
|
||||
|
||||
decoded = [data].pack("H*")
|
||||
encoded = decoded.unpack("H*").first
|
||||
|
||||
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
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# `xxd` utility included into vim-common package
|
||||
# It allow hex decoding/encoding
|
||||
|
||||
function log {
|
||||
# Logging to stderr, because stdout/stdin used for data transfer
|
||||
>&2 echo "[DEBUG][ECHO] $1"
|
||||
}
|
||||
|
||||
while read line; do
|
||||
decoded=$(echo -e "$line" | xxd -r -p)
|
||||
|
||||
header=$(echo -e "$decoded" | head -n +1)
|
||||
payload=$(echo -e "$decoded" | tail -n +2)
|
||||
|
||||
encoded=$(echo -e "$header\n$payload" | xxd -p | tr -d "\\n")
|
||||
|
||||
log ""
|
||||
log "==================================="
|
||||
|
||||
case ${header:0:1} in
|
||||
"1")
|
||||
log "Request type: Request"
|
||||
;;
|
||||
"2")
|
||||
log "Request type: Original Response"
|
||||
;;
|
||||
"3")
|
||||
log "Request type: Replayed Response"
|
||||
;;
|
||||
*)
|
||||
log "Unknown request type $header"
|
||||
esac
|
||||
echo "$encoded"
|
||||
|
||||
log "==================================="
|
||||
|
||||
log "Original data: $line"
|
||||
log "Decoded request: $decoded"
|
||||
log "Encoded data: $encoded"
|
||||
done;
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
This middleware made for auth system that randomly generate access tokens, which used later for accessing secure content. Since there is no pre-defined token value, naive approach without middleware (or if middleware use only request payloads) will fail, because replayed server have own tokens, not synced with origin. To fix this, our middleware should take in account responses of replayed and origin server, store `originalToken -> replayedToken` aliases and rewrite all requests using this token to use replayed alias. See `middleware_test.go#TestTokenMiddleware` test for examples of using this middleware.
|
||||
|
||||
How middleware works:
|
||||
|
||||
Original request +--------------+
|
||||
+-------------+----------STDIN---------->+ |
|
||||
| Gor input | | Middleware |
|
||||
+-------------+----------STDIN---------->+ |
|
||||
Original response +------+---+---+
|
||||
| ^
|
||||
+-------------+ Modified request v |
|
||||
| Gor output +<---------STDOUT-----------------+ |
|
||||
+-----+-------+ |
|
||||
| |
|
||||
| Replayed response |
|
||||
+------------------STDIN----------------->----+
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/buger/gor/proto"
|
||||
"os"
|
||||
)
|
||||
|
||||
// requestID -> originalToken
|
||||
var originalTokens map[string][]byte
|
||||
|
||||
// originalToken -> replayedToken
|
||||
var tokenAliases map[string][]byte
|
||||
|
||||
func main() {
|
||||
originalTokens = make(map[string][]byte)
|
||||
tokenAliases = make(map[string][]byte)
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
||||
for scanner.Scan() {
|
||||
encoded := scanner.Bytes()
|
||||
buf := make([]byte, len(encoded)/2)
|
||||
hex.Decode(buf, encoded)
|
||||
|
||||
go process(buf)
|
||||
}
|
||||
}
|
||||
|
||||
func process(buf []byte) {
|
||||
// First byte indicate payload type, possible values:
|
||||
// 1 - Request
|
||||
// 2 - Response
|
||||
// 3 - ReplayedResponse
|
||||
payloadType := buf[0]
|
||||
headerSize := bytes.IndexByte(buf, '\n') + 1
|
||||
header := buf[:headerSize-1]
|
||||
|
||||
// Header contains space separated values of: request type, request id, and request start time (or round-trip time for responses)
|
||||
meta := bytes.Split(header, []byte(" "))
|
||||
// For each request you should receive 3 payloads (request, response, replayed response) with same request id
|
||||
reqID := string(meta[1])
|
||||
payload := buf[headerSize:]
|
||||
|
||||
Debug("Received payload:", string(buf))
|
||||
|
||||
switch payloadType {
|
||||
case '1': // Request
|
||||
if bytes.Equal(proto.Path(payload), []byte("/token")) {
|
||||
originalTokens[reqID] = []byte{}
|
||||
Debug("Found token request:", reqID)
|
||||
} else {
|
||||
token, vs, _ := proto.PathParam(payload, []byte("token"))
|
||||
|
||||
if vs != -1 { // If there is GET token param
|
||||
if alias, ok := tokenAliases[string(token)]; ok {
|
||||
// Rewrite original token to alias
|
||||
payload = proto.SetPathParam(payload, []byte("token"), alias)
|
||||
|
||||
// Copy modified payload to our buffer
|
||||
buf = append(buf[:headerSize], payload...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emitting data back
|
||||
os.Stdout.Write(encode(buf))
|
||||
case '2': // Original response
|
||||
if _, ok := originalTokens[reqID]; ok {
|
||||
// Token is inside response body
|
||||
secureToken := proto.Body(payload)
|
||||
originalTokens[reqID] = secureToken
|
||||
Debug("Remember origial token:", string(secureToken))
|
||||
}
|
||||
case '3': // Replayed response
|
||||
if originalToken, ok := originalTokens[reqID]; ok {
|
||||
delete(originalTokens, reqID)
|
||||
secureToken := proto.Body(payload)
|
||||
tokenAliases[string(originalToken)] = secureToken
|
||||
|
||||
Debug("Create alias for new token token, was:", string(originalToken), "now:", string(secureToken))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func encode(buf []byte) []byte {
|
||||
dst := make([]byte, len(buf)*2+1)
|
||||
hex.Encode(dst, buf)
|
||||
dst[len(dst)-1] = '\n'
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
func Debug(args ...interface{}) {
|
||||
fmt.Fprint(os.Stderr, "[DEBUG][TOKEN-MOD] ")
|
||||
fmt.Fprintln(os.Stderr, args...)
|
||||
}
|
||||
@@ -1,86 +1,82 @@
|
||||
// Gor is simple http traffic replication tool written in Go. Its main goal to replay traffic from production servers to staging and dev environments.
|
||||
// Now you can test your code on real user sessions in an automated and repeatable fashion.
|
||||
//
|
||||
// Gor consists of 2 parts: listener and replay servers.
|
||||
// Listener catch http traffic from given port in real-time and send it to replay server via UDP. Replay server forwards traffic to given address.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/buger/gor/listener"
|
||||
"github.com/buger/gor/replay"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
_ "runtime/debug"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
VERSION = "0.3.5"
|
||||
var (
|
||||
mode string
|
||||
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
|
||||
memprofile = flag.String("memprofile", "", "write memory profile to this file")
|
||||
)
|
||||
|
||||
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
|
||||
var memprofile = flag.String("memprofile", "", "write memory profile to this file")
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
var ok bool
|
||||
_, ok = r.(error)
|
||||
if !ok {
|
||||
fmt.Errorf("pkg: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
// // Don't exit on panic
|
||||
// defer func() {
|
||||
// if r := recover(); r != nil {
|
||||
// fmt.Printf("PANIC: pkg: %v %s \n", r, debug.Stack())
|
||||
// }
|
||||
// }()
|
||||
|
||||
// If not set via env cariable
|
||||
if len(os.Getenv("GOMAXPROCS")) == 0 {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU() * 2)
|
||||
}
|
||||
|
||||
fmt.Println("Version:", VERSION)
|
||||
|
||||
mode := "unknown"
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
mode = os.Args[1]
|
||||
}
|
||||
|
||||
if mode != "listen" && mode != "replay" {
|
||||
fmt.Println("Usage: \n\tgor listen -h\n\tgor replay -h")
|
||||
return
|
||||
}
|
||||
|
||||
// Remove mode attr
|
||||
os.Args = append(os.Args[:1], os.Args[2:]...)
|
||||
|
||||
flag.Parse()
|
||||
InitPlugins()
|
||||
|
||||
if len(Plugins.Inputs) == 0 || len(Plugins.Outputs) == 0 {
|
||||
log.Fatal("Required at least 1 input and 1 output")
|
||||
}
|
||||
|
||||
if *memprofile != "" {
|
||||
profileMEM(*memprofile)
|
||||
}
|
||||
|
||||
if *cpuprofile != "" {
|
||||
f, err := os.Create(*cpuprofile)
|
||||
profileCPU(*cpuprofile)
|
||||
}
|
||||
|
||||
Start(nil)
|
||||
}
|
||||
|
||||
func profileCPU(cpuprofile string) {
|
||||
if cpuprofile != "" {
|
||||
f, err := os.Create(cpuprofile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
pprof.StartCPUProfile(f)
|
||||
|
||||
time.AfterFunc(60*time.Second, func() {
|
||||
time.AfterFunc(30*time.Second, func() {
|
||||
pprof.StopCPUProfile()
|
||||
f.Close()
|
||||
log.Println("Stop profiling after 60 seconds")
|
||||
log.Println("Stop profiling after 30 seconds")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if *memprofile != "" {
|
||||
f, err := os.Create(*memprofile)
|
||||
func profileMEM(memprofile string) {
|
||||
if memprofile != "" {
|
||||
f, err := os.Create(memprofile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
time.AfterFunc(60*time.Second, func() {
|
||||
time.AfterFunc(30*time.Second, func() {
|
||||
pprof.WriteHeapProfile(f)
|
||||
f.Close()
|
||||
})
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case "listen":
|
||||
listener.Run()
|
||||
case "replay":
|
||||
replay.Run()
|
||||
}
|
||||
}
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
rate = 5
|
||||
)
|
||||
|
||||
type GorStat struct {
|
||||
statName string
|
||||
latest int
|
||||
mean int
|
||||
max int
|
||||
count int
|
||||
}
|
||||
|
||||
func NewGorStat(statName string) (s *GorStat) {
|
||||
s = new(GorStat)
|
||||
s.statName = statName
|
||||
s.latest = 0
|
||||
s.mean = 0
|
||||
s.max = 0
|
||||
s.count = 0
|
||||
|
||||
if Settings.stats {
|
||||
log.Println(s.statName + ":latest,mean,max,count,count/second,gcount")
|
||||
go s.reportStats()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *GorStat) Write(latest int) {
|
||||
if Settings.stats {
|
||||
if latest > s.max {
|
||||
s.max = latest
|
||||
}
|
||||
if latest != 0 {
|
||||
s.mean = (s.mean + latest) / 2
|
||||
}
|
||||
s.latest = latest
|
||||
s.count = s.count + 1
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GorStat) Reset() {
|
||||
s.latest = 0
|
||||
s.max = 0
|
||||
s.mean = 0
|
||||
s.count = 0
|
||||
}
|
||||
|
||||
func (s *GorStat) String() string {
|
||||
return s.statName + ":" + strconv.Itoa(s.latest) + "," + strconv.Itoa(s.mean) + "," + strconv.Itoa(s.max) + "," + strconv.Itoa(s.count) + "," + strconv.Itoa(s.count/rate) + "," + strconv.Itoa(runtime.NumGoroutine())
|
||||
}
|
||||
|
||||
func (s *GorStat) reportStats() {
|
||||
for {
|
||||
log.Println(s)
|
||||
s.Reset()
|
||||
time.Sleep(rate * time.Second)
|
||||
}
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/buger/gor/proto"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var defaultPorts = map[string]string{
|
||||
"http": "80",
|
||||
"https": "443",
|
||||
}
|
||||
|
||||
type HTTPClientConfig struct {
|
||||
FollowRedirects int
|
||||
Debug bool
|
||||
OriginalHost bool
|
||||
ConnectionTimeout time.Duration
|
||||
Timeout time.Duration
|
||||
ResponseBufferSize int
|
||||
}
|
||||
|
||||
type HTTPClient struct {
|
||||
baseURL string
|
||||
scheme string
|
||||
host string
|
||||
conn net.Conn
|
||||
respBuf []byte
|
||||
config *HTTPClientConfig
|
||||
redirectsCount int
|
||||
}
|
||||
|
||||
func NewHTTPClient(baseURL string, config *HTTPClientConfig) *HTTPClient {
|
||||
if !strings.HasPrefix(baseURL, "http") {
|
||||
baseURL = "http://" + baseURL
|
||||
}
|
||||
|
||||
u, _ := url.Parse(baseURL)
|
||||
if !strings.Contains(u.Host, ":") {
|
||||
if u.Scheme != "http" {
|
||||
u.Host += ":" + defaultPorts[u.Scheme]
|
||||
}
|
||||
}
|
||||
|
||||
if config.Timeout.Nanoseconds() == 0 {
|
||||
config.Timeout = 5 * time.Second
|
||||
}
|
||||
|
||||
if config.ResponseBufferSize == 0 {
|
||||
config.ResponseBufferSize = 100 * 1024 // 100kb
|
||||
}
|
||||
|
||||
client := new(HTTPClient)
|
||||
client.baseURL = u.String()
|
||||
client.host = u.Host
|
||||
client.scheme = u.Scheme
|
||||
client.respBuf = make([]byte, config.ResponseBufferSize)
|
||||
client.config = config
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Connect() (err error) {
|
||||
c.Disconnect()
|
||||
|
||||
if !strings.Contains(c.host, ":") {
|
||||
c.conn, err = net.DialTimeout("tcp", c.host+":80", c.config.ConnectionTimeout)
|
||||
} else {
|
||||
c.conn, err = net.DialTimeout("tcp", c.host, c.config.ConnectionTimeout)
|
||||
}
|
||||
|
||||
if c.scheme == "https" {
|
||||
tlsConn := tls.Client(c.conn, &tls.Config{InsecureSkipVerify: true})
|
||||
|
||||
if err = tlsConn.Handshake(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.conn = tlsConn
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Disconnect() {
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
Debug("[HTTP] Disconnected: ", c.baseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPClient) isAlive() bool {
|
||||
one := make([]byte, 1)
|
||||
|
||||
// Ready 1 byte from socket without timeout to check if it not closed
|
||||
c.conn.SetReadDeadline(time.Now().Add(time.Millisecond))
|
||||
if _, err := c.conn.Read(one); err == io.EOF {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Send(data []byte) (response []byte, err error) {
|
||||
// Don't exit on panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
Debug("[HTTPClient]", r, string(data))
|
||||
|
||||
if _, ok := r.(error); !ok {
|
||||
log.Println("[HTTPClient] Failed to send request: ", string(data))
|
||||
log.Println("PANIC: pkg:", r, debug.Stack())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if c.conn == nil || !c.isAlive() {
|
||||
Debug("[HTTPClient] Connecting:", c.baseURL)
|
||||
if err = c.Connect(); err != nil {
|
||||
log.Println("[HTTPClient] Connection error:", err)
|
||||
response = errorPayload(HTTP_CONNECTION_ERROR)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
timeout := time.Now().Add(c.config.Timeout)
|
||||
|
||||
c.conn.SetWriteDeadline(timeout)
|
||||
|
||||
if !c.config.OriginalHost {
|
||||
data = proto.SetHost(data, []byte(c.baseURL), []byte(c.host))
|
||||
}
|
||||
|
||||
if c.config.Debug {
|
||||
Debug("[HTTPClient] Sending:", string(data))
|
||||
}
|
||||
|
||||
if _, err = c.conn.Write(data); err != nil {
|
||||
Debug("[HTTPClient] Write error:", err, c.baseURL)
|
||||
response = errorPayload(HTTP_TIMEOUT)
|
||||
return
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
if c.config.Debug {
|
||||
Debug("[HTTPClient] Received:", string(payload))
|
||||
}
|
||||
|
||||
if c.config.FollowRedirects > 0 && c.redirectsCount < c.config.FollowRedirects {
|
||||
status := payload[9:12]
|
||||
|
||||
// 3xx requests
|
||||
if status[0] == '3' {
|
||||
c.redirectsCount++
|
||||
|
||||
location := proto.Header(payload, []byte("Location"))
|
||||
redirectPayload := []byte("GET " + string(location) + " HTTP/1.1\r\n\r\n")
|
||||
|
||||
if c.config.Debug {
|
||||
Debug("[HTTPClient] Redirecting to: " + string(location))
|
||||
}
|
||||
|
||||
return c.Send(redirectPayload)
|
||||
}
|
||||
}
|
||||
|
||||
c.redirectsCount = 0
|
||||
|
||||
return payload, err
|
||||
}
|
||||
|
||||
func (c *HTTPClient) Get(path string) (response []byte, err error) {
|
||||
payload := "GET " + path + " HTTP/1.1\r\n\r\n"
|
||||
|
||||
return c.Send([]byte(payload))
|
||||
}
|
||||
|
||||
const (
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200171936-Error-520-Web-server-is-returning-an-unknown-error
|
||||
HTTP_UNKNOWN_ERROR = "520"
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200171916-Error-521-Web-server-is-down
|
||||
HTTP_CONNECTION_ERROR = "521"
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200171906-Error-522-Connection-timed-out
|
||||
HTTP_CONNECTION_TIMEOUT = "522"
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200171946-Error-523-Origin-is-unreachable
|
||||
HTTP_UNREACHABLE = "523"
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200171926-Error-524-A-timeout-occurred
|
||||
HTTP_TIMEOUT = "524"
|
||||
)
|
||||
|
||||
var errorPayloadTemplate = "HTTP/1.1 202 Accepted\r\nDate: Mon, 17 Aug 2015 14:10:11 GMT\r\nContent-Length: 0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n"
|
||||
|
||||
func errorPayload(errorCode string) []byte {
|
||||
payload := make([]byte, len(errorPayloadTemplate))
|
||||
copy(payload, errorPayloadTemplate)
|
||||
|
||||
copy(payload[29:58], []byte(time.Now().Format(time.RFC1123)))
|
||||
copy(payload[9:12], errorCode)
|
||||
|
||||
return payload
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"github.com/buger/gor/proto"
|
||||
"io/ioutil"
|
||||
_ "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
_ "reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
_ "time"
|
||||
)
|
||||
|
||||
func TestHTTPClientURLPort(t *testing.T) {
|
||||
c1 := NewHTTPClient("http://example.com", &HTTPClientConfig{})
|
||||
if c1.baseURL != "http://example.com" {
|
||||
t.Error("Sould not add 80 port for http:", c1.baseURL)
|
||||
}
|
||||
|
||||
c2 := NewHTTPClient("https://example.com", &HTTPClientConfig{})
|
||||
if c2.baseURL != "https://example.com:443" {
|
||||
t.Error("Sould add 443 port for https:", c2.baseURL)
|
||||
}
|
||||
|
||||
c3 := NewHTTPClient("https://example.com:1", &HTTPClientConfig{})
|
||||
if c3.baseURL != "https://example.com:1" {
|
||||
t.Error("Sould use specified port:", c3.baseURL)
|
||||
}
|
||||
|
||||
c4 := NewHTTPClient("example.com", &HTTPClientConfig{})
|
||||
if c4.baseURL != "http://example.com" {
|
||||
t.Error("Sould not add default protocol:", c4.baseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientSend(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
payload := func(reqType string) []byte {
|
||||
switch reqType {
|
||||
case "GET":
|
||||
return []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
case "POST":
|
||||
return []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
case "POST_CHUNKED":
|
||||
return []byte("POST / HTTP/1.1\r\nHost: www.w3.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
|
||||
}
|
||||
|
||||
return []byte("")
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method == "POST" {
|
||||
defer r.Body.Close()
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
|
||||
if len(r.TransferEncoding) > 0 && r.TransferEncoding[0] == "chunked" {
|
||||
if string(body) != "Wikipedia in\r\n\r\nchunks." {
|
||||
t.Error("Wrong POST body:", body, string(body))
|
||||
}
|
||||
} else {
|
||||
if string(body) != "a=1&b=2" {
|
||||
buf, _ := httputil.DumpRequest(r, true)
|
||||
t.Error("Wrong POST body:", string(body), string(buf))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true})
|
||||
|
||||
wg.Add(4)
|
||||
client.Send(payload("POST"))
|
||||
client.Send(payload("GET"))
|
||||
client.Send(payload("POST_CHUNKED"))
|
||||
client.Send(payload("POST"))
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// https://github.com/buger/gor/issues/184
|
||||
func TestHTTPClientResponseBuffer(t *testing.T) {
|
||||
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 := 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]))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientHTTPSSend(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
payload := func(reqType string) []byte {
|
||||
switch reqType {
|
||||
case "GET":
|
||||
return []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
case "POST":
|
||||
return []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
case "POST_CHUNKED":
|
||||
return []byte("POST / HTTP/1.1\r\nHost: www.w3.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
|
||||
}
|
||||
|
||||
return []byte("")
|
||||
}
|
||||
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method == "POST" {
|
||||
defer r.Body.Close()
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
|
||||
if len(r.TransferEncoding) > 0 && r.TransferEncoding[0] == "chunked" {
|
||||
if string(body) != "Wikipedia in\r\n\r\nchunks." {
|
||||
t.Error("Wrong POST body:", body, string(body))
|
||||
}
|
||||
} else {
|
||||
if string(body) != "a=1&b=2" {
|
||||
buf, _ := httputil.DumpRequest(r, true)
|
||||
t.Error("Wrong POST body:", string(body), string(buf))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{})
|
||||
|
||||
wg.Add(4)
|
||||
client.Send(payload("POST"))
|
||||
client.Send(payload("GET"))
|
||||
client.Send(payload("POST_CHUNKED"))
|
||||
client.Send(payload("POST"))
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientServerInstantDisconnect(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
ln, _ := net.Listen("tcp", ":0")
|
||||
defer ln.Close()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
conn.Close()
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
client := NewHTTPClient(ln.Addr().String(), &HTTPClientConfig{})
|
||||
|
||||
wg.Add(2)
|
||||
client.Send(GETPayload)
|
||||
client.Send(GETPayload)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientServerNoKeepAlive(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
ln, _ := net.Listen("tcp", ":0")
|
||||
defer ln.Close()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
reqLen, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
t.Error("Error reading:", err.Error())
|
||||
}
|
||||
Debug("Received: ", string(buf[0:reqLen]))
|
||||
conn.Write([]byte("OK"))
|
||||
|
||||
// No keep-alive connections
|
||||
conn.Close()
|
||||
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
client := NewHTTPClient(ln.Addr().String(), &HTTPClientConfig{})
|
||||
|
||||
wg.Add(2)
|
||||
client.Send(GETPayload)
|
||||
client.Send(GETPayload)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientRedirect(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.URL.Path == "/" {
|
||||
http.Redirect(w, r, "/new", 301)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{FollowRedirects: 1, Debug: false})
|
||||
|
||||
// Should do 2 queries
|
||||
wg.Add(2)
|
||||
client.Send(GETPayload)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientRedirectLimit(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.URL.Path == "/" {
|
||||
http.Redirect(w, r, "/r1", 301)
|
||||
}
|
||||
|
||||
if r.URL.Path == "/r1" {
|
||||
http.Redirect(w, r, "/r2", 301)
|
||||
}
|
||||
|
||||
if r.URL.Path == "/r2" {
|
||||
http.Redirect(w, r, "/new", 301)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{FollowRedirects: 2, Debug: false})
|
||||
|
||||
// Have 3 redirects + 1 GET, but should do only 2 redirects + GET
|
||||
wg.Add(3)
|
||||
client.Send(GETPayload)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientHandleHTTP10(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
GETPayload := []byte("GET http://foobar.com/path HTTP/1.0\r\n\r\n")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.URL.Path != "/path" {
|
||||
t.Error("Path not match:", r.URL.Path)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true})
|
||||
|
||||
wg.Add(1)
|
||||
client.Send(GETPayload)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHTTPClientErrors(t *testing.T) {
|
||||
req := []byte("GET http://foobar.com/path HTTP/1.0\r\n\r\n")
|
||||
|
||||
// Port not exists
|
||||
client := NewHTTPClient("http://127.0.0.1:1", &HTTPClientConfig{Debug: true})
|
||||
if resp, err := client.Send(req); err != nil {
|
||||
if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) {
|
||||
t.Error("Should return status 521 for connection refused, instead:", string(s))
|
||||
}
|
||||
} else {
|
||||
t.Error("Should throw error")
|
||||
}
|
||||
|
||||
client = NewHTTPClient("http://not.existing", &HTTPClientConfig{Debug: true})
|
||||
if resp, err := client.Send(req); err != nil {
|
||||
if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) {
|
||||
t.Error("Should return status 521 for no such host, instead:", string(s))
|
||||
}
|
||||
} else {
|
||||
t.Error("Should throw error")
|
||||
}
|
||||
|
||||
// Non routable IP address to simulate connection timeout
|
||||
client = NewHTTPClient("http://10.255.255.1", &HTTPClientConfig{Debug: true, ConnectionTimeout: 100 * time.Millisecond})
|
||||
|
||||
if resp, err := client.Send(req); err != nil {
|
||||
if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) {
|
||||
t.Error("Should return status 521 for io/timeout:", string(s))
|
||||
}
|
||||
} else {
|
||||
t.Error("Should throw error")
|
||||
}
|
||||
|
||||
// Connecting but io timeout on read
|
||||
ln, _ := net.Listen("tcp", ":0")
|
||||
client = NewHTTPClient("http://"+ln.Addr().String(), &HTTPClientConfig{Debug: true, Timeout: 10 * time.Millisecond})
|
||||
defer ln.Close()
|
||||
|
||||
if resp, err := client.Send(req); err != nil {
|
||||
if s := proto.Status(resp); !bytes.Equal(s, []byte("524")) {
|
||||
t.Error("Should return status 524 for io read, instead:", string(s))
|
||||
}
|
||||
} else {
|
||||
t.Error("Should throw error")
|
||||
}
|
||||
|
||||
// Response read error read tcp [::1]:51128: connection reset by peer &{{0xc20802a000}}
|
||||
ln1, _ := net.Listen("tcp", ":0")
|
||||
go func() {
|
||||
ln1.Accept()
|
||||
}()
|
||||
defer ln1.Close()
|
||||
|
||||
client = NewHTTPClient("http://"+ln1.Addr().String(), &HTTPClientConfig{Debug: true, Timeout: 10 * time.Millisecond})
|
||||
|
||||
if resp, err := client.Send(req); err != nil {
|
||||
if s := proto.Status(resp); !bytes.Equal(s, []byte("524")) {
|
||||
t.Error("Should return status 524 for connection reset by peer, instead:", string(s))
|
||||
}
|
||||
} else {
|
||||
t.Error("Should throw error")
|
||||
}
|
||||
|
||||
ln2, _ := net.Listen("tcp", ":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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/buger/gor/proto"
|
||||
)
|
||||
|
||||
type HTTPModifier struct {
|
||||
config *HTTPModifierConfig
|
||||
}
|
||||
|
||||
func NewHTTPModifier(config *HTTPModifierConfig) *HTTPModifier {
|
||||
// Optimization to skip modifier completely if we do not need it
|
||||
if len(config.urlRegexp) == 0 &&
|
||||
len(config.urlNegativeRegexp) == 0 &&
|
||||
len(config.urlRewrite) == 0 &&
|
||||
len(config.headerFilters) == 0 &&
|
||||
len(config.headerNegativeFilters) == 0 &&
|
||||
len(config.headerHashFilters) == 0 &&
|
||||
len(config.paramHashFilters) == 0 &&
|
||||
len(config.params) == 0 &&
|
||||
len(config.headers) == 0 &&
|
||||
len(config.methods) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &HTTPModifier{config: config}
|
||||
}
|
||||
|
||||
func (m *HTTPModifier) Rewrite(payload []byte) (response []byte) {
|
||||
if !proto.IsHTTPPayload(payload) {
|
||||
return payload
|
||||
}
|
||||
|
||||
if len(m.config.methods) > 0 {
|
||||
method := proto.Method(payload)
|
||||
|
||||
matched := false
|
||||
|
||||
for _, m := range m.config.methods {
|
||||
if bytes.Equal(method, m) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !matched {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.headers) > 0 {
|
||||
for _, header := range m.config.headers {
|
||||
payload = proto.SetHeader(payload, []byte(header.Name), []byte(header.Value))
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.params) > 0 {
|
||||
for _, param := range m.config.params {
|
||||
payload = proto.SetPathParam(payload, param.Name, param.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.urlRegexp) > 0 {
|
||||
path := proto.Path(payload)
|
||||
|
||||
matched := false
|
||||
|
||||
for _, f := range m.config.urlRegexp {
|
||||
if f.regexp.Match(path) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !matched {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.urlNegativeRegexp) > 0 {
|
||||
path := proto.Path(payload)
|
||||
|
||||
for _, f := range m.config.urlNegativeRegexp {
|
||||
if f.regexp.Match(path) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.headerFilters) > 0 {
|
||||
for _, f := range m.config.headerFilters {
|
||||
value := proto.Header(payload, f.name)
|
||||
|
||||
if len(value) > 0 && !f.regexp.Match(value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.headerNegativeFilters) > 0 {
|
||||
for _, f := range m.config.headerNegativeFilters {
|
||||
value := proto.Header(payload, f.name)
|
||||
|
||||
if len(value) > 0 && f.regexp.Match(value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.headerHashFilters) > 0 {
|
||||
for _, f := range m.config.headerHashFilters {
|
||||
value := proto.Header(payload, f.name)
|
||||
|
||||
if len(value) > 0 {
|
||||
hasher := fnv.New32a()
|
||||
hasher.Write(value)
|
||||
|
||||
if (hasher.Sum32() % 100) >= f.percent {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.paramHashFilters) > 0 {
|
||||
for _, f := range m.config.paramHashFilters {
|
||||
value, s, _ := proto.PathParam(payload, f.name)
|
||||
|
||||
if s != -1 {
|
||||
hasher := fnv.New32a()
|
||||
hasher.Write(value)
|
||||
|
||||
if (hasher.Sum32() % 100) >= f.percent {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.config.urlRewrite) > 0 {
|
||||
path := proto.Path(payload)
|
||||
|
||||
for _, f := range m.config.urlRewrite {
|
||||
if f.src.Match(path) {
|
||||
path = f.src.ReplaceAll(path, f.target)
|
||||
payload = proto.SetPath(payload, path)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HTTPModifierConfig holds configuration options for built-in traffic modifier
|
||||
type HTTPModifierConfig struct {
|
||||
urlNegativeRegexp HTTPUrlRegexp
|
||||
urlRegexp HTTPUrlRegexp
|
||||
urlRewrite UrlRewriteMap
|
||||
headerFilters HTTPHeaderFilters
|
||||
headerNegativeFilters HTTPHeaderFilters
|
||||
headerHashFilters HTTPHashFilters
|
||||
paramHashFilters HTTPHashFilters
|
||||
|
||||
params HTTPParams
|
||||
headers HTTPHeaders
|
||||
methods HTTPMethods
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-header, --http-disallow-header options
|
||||
//
|
||||
type headerFilter struct {
|
||||
name []byte
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
// HTTPHeaderFilters holds list of headers and their regexps
|
||||
type HTTPHeaderFilters []headerFilter
|
||||
|
||||
func (h *HTTPHeaderFilters) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
func (h *HTTPHeaderFilters) Set(value string) error {
|
||||
valArr := strings.SplitN(value, ":", 2)
|
||||
if len(valArr) < 2 {
|
||||
return errors.New("need both header and value, colon-delimited (ex. user_id:^169$).")
|
||||
}
|
||||
r, err := regexp.Compile(valArr[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*h = append(*h, headerFilter{name: []byte(valArr[0]), regexp: r})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-header-hash and --http-allow-param-hash options
|
||||
//
|
||||
type hashFilter struct {
|
||||
name []byte
|
||||
percent uint32
|
||||
}
|
||||
|
||||
type HTTPHashFilters []hashFilter
|
||||
|
||||
func (h *HTTPHashFilters) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
func (h *HTTPHashFilters) Set(value string) error {
|
||||
valArr := strings.SplitN(value, ":", 2)
|
||||
if len(valArr) < 2 {
|
||||
return errors.New("need both header and value, colon-delimited (ex. user_id:50%)")
|
||||
}
|
||||
|
||||
f := hashFilter{name: []byte(valArr[0])}
|
||||
|
||||
if strings.Contains(valArr[1], "%") {
|
||||
p, _ := strconv.ParseInt(valArr[1][:len(valArr[1])-1], 0, 0)
|
||||
f.percent = uint32(p)
|
||||
} else if strings.Contains(valArr[1], "/") {
|
||||
// DEPRECATED format
|
||||
var num, den uint64
|
||||
|
||||
fracArr := strings.Split(valArr[1], "/")
|
||||
num, _ = strconv.ParseUint(fracArr[0], 10, 64)
|
||||
den, _ = strconv.ParseUint(fracArr[1], 10, 64)
|
||||
|
||||
f.percent = uint32((float64(num) / float64(den)) * 100)
|
||||
} else {
|
||||
return errors.New("Value should be percent and contain '%'")
|
||||
}
|
||||
|
||||
*h = append(*h, f)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-set-header option
|
||||
//
|
||||
type HTTPHeaders []HTTPHeader
|
||||
type HTTPHeader struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (h *HTTPHeaders) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
func (h *HTTPHeaders) Set(value string) error {
|
||||
v := strings.SplitN(value, ":", 2)
|
||||
if len(v) != 2 {
|
||||
return errors.New("Expected `Key: Value`")
|
||||
}
|
||||
|
||||
header := HTTPHeader{
|
||||
strings.TrimSpace(v[0]),
|
||||
strings.TrimSpace(v[1]),
|
||||
}
|
||||
|
||||
*h = append(*h, header)
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-set-param option
|
||||
//
|
||||
type HTTPParams []HTTPParam
|
||||
type HTTPParam struct {
|
||||
Name []byte
|
||||
Value []byte
|
||||
}
|
||||
|
||||
func (h *HTTPParams) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
func (h *HTTPParams) Set(value string) error {
|
||||
v := strings.SplitN(value, "=", 2)
|
||||
if len(v) != 2 {
|
||||
return errors.New("Expected `Key=Value`")
|
||||
}
|
||||
|
||||
param := HTTPParam{
|
||||
[]byte(strings.TrimSpace(v[0])),
|
||||
[]byte(strings.TrimSpace(v[1])),
|
||||
}
|
||||
|
||||
*h = append(*h, param)
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-method option
|
||||
//
|
||||
type HTTPMethods [][]byte
|
||||
|
||||
func (h *HTTPMethods) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
func (h *HTTPMethods) Set(value string) error {
|
||||
*h = append(*h, []byte(value))
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-rewrite-url option
|
||||
//
|
||||
type urlRewrite struct {
|
||||
src *regexp.Regexp
|
||||
target []byte
|
||||
}
|
||||
|
||||
type UrlRewriteMap []urlRewrite
|
||||
|
||||
func (r *UrlRewriteMap) String() string {
|
||||
return fmt.Sprint(*r)
|
||||
}
|
||||
|
||||
func (r *UrlRewriteMap) Set(value string) error {
|
||||
valArr := strings.SplitN(value, ":", 2)
|
||||
if len(valArr) < 2 {
|
||||
return errors.New("need both src and target, colon-delimited (ex. /a:/b)")
|
||||
}
|
||||
regexp, err := regexp.Compile(valArr[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*r = append(*r, urlRewrite{src: regexp, target: []byte(valArr[1])})
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-url option
|
||||
//
|
||||
type urlRegexp struct {
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
type HTTPUrlRegexp []urlRegexp
|
||||
|
||||
func (r *HTTPUrlRegexp) String() string {
|
||||
return fmt.Sprint(*r)
|
||||
}
|
||||
|
||||
func (r *HTTPUrlRegexp) Set(value string) error {
|
||||
regexp, err := regexp.Compile(value)
|
||||
|
||||
*r = append(*r, urlRegexp{regexp: regexp})
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPHeaderFilters(t *testing.T) {
|
||||
filters := HTTPHeaderFilters{}
|
||||
|
||||
err := filters.Set("Header1:^$")
|
||||
if err != nil {
|
||||
t.Error("Should not error on Header1:^$")
|
||||
}
|
||||
|
||||
err = filters.Set("Header2:^:$")
|
||||
if err != nil {
|
||||
t.Error("Should not error on Header2:^:$")
|
||||
}
|
||||
|
||||
// Missing colon
|
||||
err = filters.Set("Header3-^$")
|
||||
if err == nil {
|
||||
t.Error("Should error on Header2:^:$")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPHashFilters(t *testing.T) {
|
||||
filters := HTTPHashFilters{}
|
||||
|
||||
err := filters.Set("Header1:1/2")
|
||||
if err != nil {
|
||||
t.Error("Should support old syntax")
|
||||
}
|
||||
|
||||
if filters[0].percent != 50 {
|
||||
t.Error("Wrong percentage", filters[0].percent)
|
||||
}
|
||||
|
||||
err = filters.Set("Header2:1")
|
||||
if err == nil {
|
||||
t.Error("Should error on Header2 because no % symbol")
|
||||
}
|
||||
|
||||
err = filters.Set("Header2:10%")
|
||||
if err != nil {
|
||||
t.Error("Should pass")
|
||||
}
|
||||
|
||||
if filters[1].percent != 10 {
|
||||
t.Error("Wrong percentage", filters[1].percent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUrlRewriteMap(t *testing.T) {
|
||||
var err error
|
||||
rewrites := UrlRewriteMap{}
|
||||
|
||||
if err = rewrites.Set("/v1/user/([^\\/]+)/ping:/v2/user/$1/ping"); err != nil {
|
||||
t.Error("Should set mapping", err)
|
||||
}
|
||||
|
||||
if err = rewrites.Set("/v1/user/([^\\/]+)/ping"); err == nil {
|
||||
t.Error("Should not set mapping without :")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/buger/gor/proto"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPModifierWithoutConfig(t *testing.T) {
|
||||
if NewHTTPModifier(&HTTPModifierConfig{}) != nil {
|
||||
t.Error("If no config specified should not be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierHeaderFilters(t *testing.T) {
|
||||
filters := HTTPHeaderFilters{}
|
||||
filters.Set("Host:^www.w3.org$")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerFilters: filters,
|
||||
})
|
||||
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if len(modifier.Rewrite(payload)) == 0 {
|
||||
t.Error("Request should pass filters")
|
||||
}
|
||||
|
||||
filters = HTTPHeaderFilters{}
|
||||
// Setting filter that not match our header
|
||||
filters.Set("Host:^www.w4.org$")
|
||||
|
||||
modifier = NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerFilters: filters,
|
||||
})
|
||||
|
||||
if len(modifier.Rewrite(payload)) != 0 {
|
||||
t.Error("Request should not pass filters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierHeaderNegativeFilters(t *testing.T) {
|
||||
filters := HTTPHeaderFilters{}
|
||||
filters.Set("Host:^www.w3.org$")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerNegativeFilters: filters,
|
||||
})
|
||||
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w4.org\r\n\r\na=1&b=2")
|
||||
|
||||
if len(modifier.Rewrite(payload)) == 0 {
|
||||
t.Error("Request should pass filters")
|
||||
}
|
||||
|
||||
filters = HTTPHeaderFilters{}
|
||||
// Setting filter that not match our header
|
||||
filters.Set("Host:^www.w4.org$")
|
||||
|
||||
modifier = NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerNegativeFilters: filters,
|
||||
})
|
||||
|
||||
if len(modifier.Rewrite(payload)) != 0 {
|
||||
t.Error("Request should not pass filters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierURLRewrite(t *testing.T) {
|
||||
var url, newURL []byte
|
||||
|
||||
rewrites := UrlRewriteMap{}
|
||||
|
||||
payload := func(url []byte) []byte {
|
||||
return []byte("POST " + string(url) + " HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
}
|
||||
|
||||
err := rewrites.Set("/v1/user/([^\\/]+)/ping:/v2/user/$1/ping")
|
||||
if err != nil {
|
||||
t.Error("Should not error on /v1/user/([^\\/]+)/ping:/v2/user/$1/ping")
|
||||
}
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
urlRewrite: rewrites,
|
||||
})
|
||||
|
||||
url = []byte("/v1/user/joe/ping")
|
||||
if newURL = proto.Path(modifier.Rewrite(payload(url))); bytes.Equal(newURL, url) {
|
||||
t.Error("Request url should have been rewritten, wasn't", string(newURL))
|
||||
}
|
||||
|
||||
url = []byte("/v1/user/ping")
|
||||
if newURL = proto.Path(modifier.Rewrite(payload(url))); !bytes.Equal(newURL, url) {
|
||||
t.Error("Request url should have been rewritten, wasn't", string(newURL))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierHeaderHashFilters(t *testing.T) {
|
||||
filters := HTTPHashFilters{}
|
||||
filters.Set("Header2:1/2")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
headerHashFilters: filters,
|
||||
})
|
||||
|
||||
payload := func(header []byte) []byte {
|
||||
return []byte("POST / HTTP/1.1\r\n" + string(header) + "Content-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
}
|
||||
|
||||
if p := modifier.Rewrite(payload([]byte(""))); len(p) == 0 {
|
||||
t.Error("Request should pass filters if Header does not exist")
|
||||
}
|
||||
|
||||
if p := modifier.Rewrite(payload([]byte("Header2: 3\r\n"))); len(p) > 0 {
|
||||
t.Error("Request should not pass filters, Header2 hash too high")
|
||||
}
|
||||
|
||||
if p := modifier.Rewrite(payload([]byte("Header2: 1\r\n"))); len(p) == 0 {
|
||||
t.Error("Request should pass filters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierParamHashFilters(t *testing.T) {
|
||||
filters := HTTPHashFilters{}
|
||||
filters.Set("user_id:1/2")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
paramHashFilters: filters,
|
||||
})
|
||||
|
||||
payload := func(value []byte) []byte {
|
||||
return []byte("POST /" + string(value) + " HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
}
|
||||
|
||||
if p := modifier.Rewrite(payload([]byte(""))); len(p) == 0 {
|
||||
t.Error("Request should pass filters if param does not exist")
|
||||
}
|
||||
|
||||
if p := modifier.Rewrite(payload([]byte("?user_id=3"))); len(p) > 0 {
|
||||
t.Error("Request should not pass filters", string(p))
|
||||
}
|
||||
|
||||
if p := modifier.Rewrite(payload([]byte("?user_id=1"))); len(p) == 0 {
|
||||
t.Error("Request should pass filters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierHeaders(t *testing.T) {
|
||||
headers := HTTPHeaders{}
|
||||
headers.Set("Header1:1")
|
||||
headers.Set("Host:localhost")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
headers: headers,
|
||||
})
|
||||
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
newPayload := []byte("POST /post HTTP/1.1\r\nHeader1: 1\r\nContent-Length: 7\r\nHost: localhost\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = modifier.Rewrite(payload); !bytes.Equal(payload, newPayload) {
|
||||
t.Error("Should update request headers", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierURLRegexp(t *testing.T) {
|
||||
filters := HTTPUrlRegexp{}
|
||||
filters.Set("/v1/app")
|
||||
filters.Set("/v1/api")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
urlRegexp: filters,
|
||||
})
|
||||
|
||||
payload := func(url string) []byte {
|
||||
return []byte("POST " + url + " HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
}
|
||||
|
||||
if len(modifier.Rewrite(payload("/v1/app/test"))) == 0 {
|
||||
t.Error("Should pass url")
|
||||
}
|
||||
|
||||
if len(modifier.Rewrite(payload("/v1/api/test"))) == 0 {
|
||||
t.Error("Should pass url")
|
||||
}
|
||||
|
||||
if len(modifier.Rewrite(payload("/other"))) > 0 {
|
||||
t.Error("Should not pass url")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierURLNegativeRegexp(t *testing.T) {
|
||||
filters := HTTPUrlRegexp{}
|
||||
filters.Set("/restricted1")
|
||||
filters.Set("/some/restricted2")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
urlNegativeRegexp: filters,
|
||||
})
|
||||
|
||||
payload := func(url string) []byte {
|
||||
return []byte("POST " + url + " HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
}
|
||||
|
||||
if len(modifier.Rewrite(payload("/v1/app/test"))) == 0 {
|
||||
t.Error("Should pass url")
|
||||
}
|
||||
|
||||
if len(modifier.Rewrite(payload("/restricted1"))) > 0 {
|
||||
t.Error("Should not pass url")
|
||||
}
|
||||
|
||||
if len(modifier.Rewrite(payload("/some/restricted2"))) > 0 {
|
||||
t.Error("Should not pass url")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierSetHeader(t *testing.T) {
|
||||
filters := HTTPHeaders{}
|
||||
filters.Set("User-Agent:Gor")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
headers: filters,
|
||||
})
|
||||
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter := []byte("POST /post HTTP/1.1\r\nUser-Agent: Gor\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = modifier.Rewrite(payload); !bytes.Equal(payloadAfter, payload) {
|
||||
t.Error("Should add new header", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPModifierSetParam(t *testing.T) {
|
||||
filters := HTTPParams{}
|
||||
filters.Set("api_key=1")
|
||||
|
||||
modifier := NewHTTPModifier(&HTTPModifierConfig{
|
||||
params: filters,
|
||||
})
|
||||
|
||||
payload := []byte("POST /post?api_key=1234 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter := []byte("POST /post?api_key=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = modifier.Rewrite(payload); !bytes.Equal(payloadAfter, payload) {
|
||||
t.Error("Should override param", string(payload))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DummyInput used for debugging. It generate 1 "GET /"" request per second.
|
||||
type DummyInput struct {
|
||||
data chan []byte
|
||||
}
|
||||
|
||||
// NewDummyInput constructor for DummyInput
|
||||
func NewDummyInput(options string) (di *DummyInput) {
|
||||
di = new(DummyInput)
|
||||
di.data = make(chan []byte)
|
||||
|
||||
go di.emit()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *DummyInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
|
||||
copy(data, buf)
|
||||
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (i *DummyInput) emit() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
uuid := uuid()
|
||||
reqh := payloadHeader(RequestPayload, uuid, time.Now().UnixNano())
|
||||
i.data <- append(reqh, []byte("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")...)
|
||||
|
||||
resh := payloadHeader(ResponsePayload, uuid, 1)
|
||||
i.data <- append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *DummyInput) String() string {
|
||||
return "Dummy Input"
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FileInput can read requests generated by FileOutput
|
||||
type FileInput struct {
|
||||
data chan []byte
|
||||
path string
|
||||
file *os.File
|
||||
speedFactor float64
|
||||
}
|
||||
|
||||
// NewFileInput constructor for FileInput. Accepts file path as argument.
|
||||
func NewFileInput(path string) (i *FileInput) {
|
||||
i = new(FileInput)
|
||||
i.data = make(chan []byte)
|
||||
i.path = path
|
||||
i.speedFactor = 1
|
||||
i.init(path)
|
||||
|
||||
go i.emit()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *FileInput) init(path string) {
|
||||
file, err := os.Open(path)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(i, "Cannot open file %q. Error: %s", path, err)
|
||||
}
|
||||
|
||||
i.file = file
|
||||
}
|
||||
|
||||
func (i *FileInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
copy(data, buf)
|
||||
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (i *FileInput) String() string {
|
||||
return "File input: " + i.path
|
||||
}
|
||||
|
||||
func (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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
log.Println("FileInput: end of file")
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTPInput used for sending requests to Gor via http
|
||||
type HTTPInput struct {
|
||||
data chan []byte
|
||||
address string
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewHTTPInput constructor for HTTPInput. Accepts address with port which he will listen on.
|
||||
func NewHTTPInput(address string) (i *HTTPInput) {
|
||||
i = new(HTTPInput)
|
||||
i.data = make(chan []byte, 10000)
|
||||
i.address = address
|
||||
|
||||
i.listen(address)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *HTTPInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
|
||||
header := payloadHeader(RequestPayload, uuid(), time.Now().UnixNano())
|
||||
|
||||
copy(data[0:len(header)], header)
|
||||
copy(data[len(header):], buf)
|
||||
|
||||
return len(buf) + len(header), nil
|
||||
}
|
||||
|
||||
func (i *HTTPInput) handler(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Scheme = "http"
|
||||
r.URL.Host = i.listener.Addr().String()
|
||||
|
||||
buf, _ := httputil.DumpRequestOut(r, true)
|
||||
http.Error(w, http.StatusText(200), 200)
|
||||
|
||||
select {
|
||||
case i.data <- buf:
|
||||
default:
|
||||
Debug("[INPUT-HTTP] Dropping requests because output can't process them fast enough")
|
||||
}
|
||||
}
|
||||
|
||||
func (i *HTTPInput) listen(address string) {
|
||||
var err error
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/", i.handler)
|
||||
|
||||
i.listener, err = net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Fatal("HTTP input listener failure:", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = http.Serve(i.listener, mux)
|
||||
if err != nil {
|
||||
log.Fatal("HTTP input serve failure:", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *HTTPInput) String() string {
|
||||
return "HTTP input: " + i.address
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/buger/gor/proto"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPInput(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewHTTPInput(":0")
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
address := strings.Replace(input.listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
http.Get("http://" + address + "/")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestInputHTTPLargePayload(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
// Generate 1000kb file
|
||||
dd := exec.Command("dd", "if=/dev/urandom", "of=/tmp/large", "bs=1MB", "count=4")
|
||||
err := dd.Run()
|
||||
if err != nil {
|
||||
log.Fatal("dd error:", err)
|
||||
}
|
||||
|
||||
input := NewHTTPInput(":0")
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
if len(proto.Body(payloadBody(data))) != 4000000 {
|
||||
t.Error("Should receive full file")
|
||||
}
|
||||
wg.Done()
|
||||
})
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
wg.Add(1)
|
||||
address := strings.Replace(input.listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
curl := exec.Command("curl", "http://"+address, "--data-binary", "@/tmp/large")
|
||||
err = curl.Run()
|
||||
if err != nil {
|
||||
log.Fatal("curl error:", err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
raw "github.com/buger/gor/raw_socket_listener"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// NewRAWInput constructor for RAWInput. Accepts address with port as argument.
|
||||
func NewRAWInput(address string, expire time.Duration) (i *RAWInput) {
|
||||
i = new(RAWInput)
|
||||
i.data = make(chan *raw.TCPMessage)
|
||||
i.address = address
|
||||
i.expire = expire
|
||||
i.quit = make(chan bool)
|
||||
|
||||
go i.listen(address)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *RAWInput) Read(data []byte) (int, error) {
|
||||
msg := <-i.data
|
||||
buf := msg.Bytes()
|
||||
|
||||
var header []byte
|
||||
|
||||
if msg.IsIncoming {
|
||||
header = payloadHeader(RequestPayload, msg.UUID(), msg.Start.UnixNano())
|
||||
} else {
|
||||
header = payloadHeader(ResponsePayload, msg.UUID(), msg.End.UnixNano()-msg.RequestStart.UnixNano())
|
||||
}
|
||||
|
||||
copy(data[0:len(header)], header)
|
||||
copy(data[len(header):], buf)
|
||||
|
||||
return len(buf) + len(header), nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
i.listener = raw.NewListener(host, port, i.expire, true)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-i.quit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Receiving TCPMessage object
|
||||
m := i.listener.Receive()
|
||||
|
||||
i.data <- m
|
||||
}
|
||||
}
|
||||
|
||||
func (i *RAWInput) String() string {
|
||||
return "RAW Socket input: " + i.address
|
||||
}
|
||||
|
||||
func (i *RAWInput) Close() {
|
||||
i.listener.Close()
|
||||
close(i.quit)
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const testRawExpire = time.Millisecond * 200
|
||||
|
||||
func TestRAWInput(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)
|
||||
|
||||
var respCounter, reqCounter int64
|
||||
|
||||
input := NewRAWInput(originAddr, testRawExpire)
|
||||
defer input.Close()
|
||||
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
if data[0] == '1' {
|
||||
atomic.AddInt64(&reqCounter, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&respCounter, 1)
|
||||
}
|
||||
|
||||
if Settings.debug {
|
||||
log.Println(reqCounter, respCounter)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
client := NewHTTPClient(origin.URL, &HTTPClientConfig{})
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
// request + response
|
||||
wg.Add(2)
|
||||
client.Get("/")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestInputRAW100Expect(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
fileContent, _ := ioutil.ReadFile("README.md")
|
||||
|
||||
// Origing and Replay server initialization
|
||||
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
ioutil.ReadAll(r.Body)
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer origin.Close()
|
||||
|
||||
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
|
||||
input := NewRAWInput(originAddr, time.Second)
|
||||
defer input.Close()
|
||||
|
||||
// We will use it to get content of raw HTTP request
|
||||
testOutput := NewTestOutput(func(data []byte) {
|
||||
switch data[0] {
|
||||
case RequestPayload:
|
||||
if strings.Contains(string(data), "Expect: 100-continue") {
|
||||
t.Error("Should not contain 100-continue header")
|
||||
}
|
||||
wg.Done()
|
||||
case ResponsePayload:
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
replay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
|
||||
if !bytes.Equal(body, fileContent) {
|
||||
buf, _ := httputil.DumpRequest(r, true)
|
||||
t.Error("Wrong POST body:", string(buf))
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer replay.Close()
|
||||
|
||||
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{testOutput, httpOutput}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
// Origin + Response/Request Test Output + Request Http Output
|
||||
wg.Add(4)
|
||||
curl := exec.Command("curl", "http://"+originAddr, "--data-binary", "@README.md")
|
||||
err := curl.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestInputRAWChunkedEncoding(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
fileContent, _ := ioutil.ReadFile("README.md")
|
||||
|
||||
// Origing and Replay server initialization
|
||||
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
ioutil.ReadAll(r.Body)
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
|
||||
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
input := NewRAWInput(originAddr, time.Second)
|
||||
defer input.Close()
|
||||
|
||||
replay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
|
||||
if !bytes.Equal(body, fileContent) {
|
||||
buf, _ := httputil.DumpRequest(r, true)
|
||||
t.Error("Wrong POST body:", string(buf))
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer replay.Close()
|
||||
|
||||
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{Debug: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{httpOutput}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
curl := exec.Command("curl", "http://"+originAddr, "--header", "Transfer-Encoding: chunked", "--data-binary", "@README.md")
|
||||
err := curl.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestInputRAWLargePayload(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
// Generate 200kb file
|
||||
dd := exec.Command("dd", "if=/dev/urandom", "of=/tmp/large", "bs=1KB", "count=100")
|
||||
err := dd.Run()
|
||||
if err != nil {
|
||||
log.Fatal("dd error:", err)
|
||||
}
|
||||
|
||||
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
defer req.Body.Close()
|
||||
body, _ := ioutil.ReadAll(req.Body)
|
||||
|
||||
if len(body) != 100*1000 {
|
||||
t.Error("File size should be 1mb:", len(body))
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
|
||||
input := NewRAWInput(originAddr, time.Second)
|
||||
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]
|
||||
|
||||
if len(body) != 100*1000 {
|
||||
t.Error("File size should be 100000 bytes:", len(body))
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer replay.Close()
|
||||
|
||||
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{Debug: false})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{httpOutput}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
wg.Add(2)
|
||||
curl := exec.Command("curl", "http://"+originAddr, "--header", "Transfer-Encoding: chunked", "--data-binary", "@/tmp/large")
|
||||
err = curl.Run()
|
||||
if err != nil {
|
||||
log.Fatal("curl error:", err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
// TCPInput used for internal communication
|
||||
type TCPInput struct {
|
||||
data chan []byte
|
||||
address string
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewTCPInput constructor for TCPInput, accepts address with port
|
||||
func NewTCPInput(address string) (i *TCPInput) {
|
||||
i = new(TCPInput)
|
||||
i.data = make(chan []byte)
|
||||
i.address = address
|
||||
|
||||
i.listen(address)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *TCPInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
copy(data, buf)
|
||||
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (i *TCPInput) listen(address string) {
|
||||
listener, err := net.Listen("tcp", address)
|
||||
i.listener = listener
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Can't start:", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
|
||||
if err != nil {
|
||||
log.Println("Error while Accept()", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go i.handleConnection(conn)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *TCPInput) handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Split(payloadScanner)
|
||||
|
||||
for scanner.Scan() {
|
||||
i.data <- scanner.Bytes()
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Unexpected error in input tcp connection:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *TCPInput) String() string {
|
||||
return "TCP input: " + i.address
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTCPInput(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTCPInput(":0")
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", input.listener.Addr().String())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
conn, err := net.DialTCP("tcp", nil, tcpAddr)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
msg := []byte("1 1 1\nGET / HTTP/1.1\r\n\r\n")
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
conn.Write(msg)
|
||||
conn.Write([]byte(payloadSeparator))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func BenchmarkTCPInput(b *testing.B) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTCPInput(":0")
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", input.listener.Addr().String())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var connections []net.Conn
|
||||
|
||||
// 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++ {
|
||||
wg.Add(1)
|
||||
dataChan <- msg
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, conn := range connections {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
close(quit)
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/buger/gor/listener"
|
||||
"github.com/buger/gor/replay"
|
||||
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
func isEqual(t *testing.T, a interface{}, b interface{}) {
|
||||
if a != b {
|
||||
t.Error("Original and Replayed request not match\n", a, "!=", b)
|
||||
}
|
||||
}
|
||||
|
||||
var envs int
|
||||
|
||||
type Env struct {
|
||||
Verbose bool
|
||||
|
||||
ListenHandler http.HandlerFunc
|
||||
ReplayHandler http.HandlerFunc
|
||||
|
||||
ReplayLimit int
|
||||
ListenerLimit int
|
||||
}
|
||||
|
||||
func (e *Env) start() (p int) {
|
||||
p = 50000 + envs*10
|
||||
|
||||
go e.startHTTP(p, http.HandlerFunc(e.ListenHandler))
|
||||
go e.startHTTP(p+2, http.HandlerFunc(e.ReplayHandler))
|
||||
go e.startListener(p, p+1)
|
||||
go e.startReplay(p+1, p+2)
|
||||
|
||||
// Time to start http and gor instances
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
envs++
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (e *Env) startListener(port int, replayPort int) {
|
||||
listener.Settings.Verbose = e.Verbose
|
||||
listener.Settings.Address = "127.0.0.1"
|
||||
listener.Settings.ReplayAddress = "127.0.0.1:" + strconv.Itoa(replayPort)
|
||||
listener.Settings.Port = port
|
||||
|
||||
if e.ListenerLimit != 0 {
|
||||
listener.Settings.ReplayAddress += "|" + strconv.Itoa(e.ListenerLimit)
|
||||
}
|
||||
|
||||
listener.Run()
|
||||
}
|
||||
|
||||
func (e *Env) startReplay(port int, forwardPort int) {
|
||||
replay.Settings.Verbose = e.Verbose
|
||||
replay.Settings.Host = "127.0.0.1"
|
||||
replay.Settings.ForwardAddress = "127.0.0.1:" + strconv.Itoa(forwardPort)
|
||||
replay.Settings.Port = port
|
||||
|
||||
if e.ReplayLimit != 0 {
|
||||
replay.Settings.ForwardAddress += "|" + strconv.Itoa(e.ReplayLimit)
|
||||
}
|
||||
|
||||
replay.Run()
|
||||
}
|
||||
|
||||
func (e *Env) startHTTP(port int, handler http.Handler) {
|
||||
err := http.ListenAndServe(":"+strconv.Itoa(port), handler)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error while starting http server:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getRequest(port int) *http.Request {
|
||||
req, _ := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(port)+"/test", nil)
|
||||
ck1 := new(http.Cookie)
|
||||
ck1.Name = "test"
|
||||
ck1.Value = "value"
|
||||
|
||||
req.AddCookie(ck1)
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func TestReplay(t *testing.T) {
|
||||
var request *http.Request
|
||||
received := make(chan int)
|
||||
|
||||
listenHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "OK", http.StatusAccepted)
|
||||
}
|
||||
|
||||
replayHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
isEqual(t, r.URL.Path, request.URL.Path)
|
||||
|
||||
if len(r.Cookies()) > 0 {
|
||||
isEqual(t, r.Cookies()[0].Value, request.Cookies()[0].Value)
|
||||
} else {
|
||||
t.Error("Cookies should not be blank")
|
||||
}
|
||||
|
||||
http.Error(w, "OK", http.StatusAccepted)
|
||||
|
||||
if t.Failed() {
|
||||
fmt.Println("\nReplayed:", r, "\nOriginal:", request)
|
||||
}
|
||||
|
||||
received <- 1
|
||||
}
|
||||
|
||||
env := &Env{
|
||||
Verbose: true,
|
||||
ListenHandler: listenHandler,
|
||||
ReplayHandler: replayHandler,
|
||||
}
|
||||
p := env.start()
|
||||
|
||||
request = getRequest(p)
|
||||
|
||||
_, err := http.DefaultClient.Do(request)
|
||||
|
||||
if err != nil {
|
||||
t.Error("Can't make request", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-received:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("Timeout error")
|
||||
}
|
||||
}
|
||||
|
||||
func rateLimitEnv(replayLimit int, listenerLimit int, connCount int) int32 {
|
||||
var processed int32
|
||||
|
||||
listenHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "OK", http.StatusAccepted)
|
||||
}
|
||||
|
||||
replayHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddInt32(&processed, 1)
|
||||
http.Error(w, "OK", http.StatusAccepted)
|
||||
}
|
||||
|
||||
env := &Env{
|
||||
ListenHandler: listenHandler,
|
||||
ReplayHandler: replayHandler,
|
||||
ReplayLimit: replayLimit,
|
||||
ListenerLimit: listenerLimit,
|
||||
}
|
||||
|
||||
p := env.start()
|
||||
req := getRequest(p)
|
||||
|
||||
for i := 0; i < connCount; i++ {
|
||||
go func() {
|
||||
http.DefaultClient.Do(req)
|
||||
}()
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
|
||||
return processed
|
||||
}
|
||||
|
||||
func TestWithoutReplayRateLimit(t *testing.T) {
|
||||
processed := rateLimitEnv(0, 0, 10)
|
||||
|
||||
if processed != 10 {
|
||||
t.Error("It should forward all requests without rate-limiting", processed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayRateLimit(t *testing.T) {
|
||||
processed := rateLimitEnv(5, 0, 10)
|
||||
|
||||
if processed != 5 {
|
||||
t.Error("It should forward only 5 requests with rate-limiting", processed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerRateLimit(t *testing.T) {
|
||||
processed := rateLimitEnv(0, 3, 100)
|
||||
|
||||
if processed != 3 {
|
||||
t.Error("It should forward only 3 requests with rate-limiting", processed)
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Limiter is a wrapper for input or output plugin which adds rate limiting
|
||||
type Limiter struct {
|
||||
plugin interface{}
|
||||
limit int
|
||||
isPercent bool
|
||||
|
||||
currentRPS int
|
||||
currentTime int64
|
||||
}
|
||||
|
||||
func parseLimitOptions(options string) (limit int, isPercent bool) {
|
||||
if strings.Contains(options, "%") {
|
||||
limit, _ = strconv.Atoi(strings.Split(options, "%")[0])
|
||||
isPercent = true
|
||||
} else {
|
||||
limit, _ = strconv.Atoi(options)
|
||||
isPercent = false
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewLimiter constructor for Limiter, accepts plugin and options
|
||||
// `options` allow to sprcify relatve or absolute limiting
|
||||
func NewLimiter(plugin interface{}, options string) io.ReadWriter {
|
||||
l := new(Limiter)
|
||||
l.limit, l.isPercent = parseLimitOptions(options)
|
||||
l.plugin = plugin
|
||||
l.currentTime = time.Now().UnixNano()
|
||||
|
||||
// FileInput have its own rate limiting. Unlike other inputs we not just dropping requests, we can slow down or speed up request emittion.
|
||||
if fi, ok := l.plugin.(*FileInput); ok && l.isPercent {
|
||||
fi.speedFactor = float64(l.limit) / float64(100)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Limiter) isLimited() bool {
|
||||
// File input have its own limiting algorithm
|
||||
if _, ok := l.plugin.(*FileInput); ok && l.isPercent {
|
||||
return false
|
||||
}
|
||||
|
||||
if l.isPercent {
|
||||
return l.limit <= rand.Intn(100)
|
||||
}
|
||||
|
||||
if (time.Now().UnixNano() - l.currentTime) > time.Second.Nanoseconds() {
|
||||
l.currentTime = time.Now().UnixNano()
|
||||
l.currentRPS = 0
|
||||
}
|
||||
|
||||
if l.currentRPS >= l.limit {
|
||||
return true
|
||||
}
|
||||
|
||||
l.currentRPS++
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *Limiter) Write(data []byte) (n int, err error) {
|
||||
if l.isLimited() {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
n, err = l.plugin.(io.Writer).Write(data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Limiter) Read(data []byte) (n int, err error) {
|
||||
n, err = l.plugin.(io.Reader).Read(data)
|
||||
|
||||
if l.isLimited() {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Limiter) String() string {
|
||||
return fmt.Sprintf("Limiting %s to: %d (isPercent: %b)", l.plugin, l.limit, l.isPercent)
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// +build !race
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOutputLimiter(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewLimiter(NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
}), "10")
|
||||
wg.Add(10)
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func TestInputLimiter(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewLimiter(NewTestInput(), "10")
|
||||
output := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
wg.Add(10)
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
input.(*Limiter).plugin.(*TestInput).EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
// Should limit all requests
|
||||
func TestPercentLimiter1(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewLimiter(NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
}), "0%")
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
// Should not limit at all
|
||||
func TestPercentLimiter2(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewLimiter(NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
}), "100%")
|
||||
wg.Add(100)
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
// Listener capture TCP traffic using RAW SOCKETS.
|
||||
// Note: it requires sudo or root access.
|
||||
//
|
||||
// Rigt now it suport only HTTP
|
||||
package listener
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Enable debug logging only if "--verbose" flag passed
|
||||
func Debug(v ...interface{}) {
|
||||
if Settings.Verbose {
|
||||
log.Println(v...)
|
||||
}
|
||||
}
|
||||
|
||||
func ReplayServer() (conn net.Conn, err error) {
|
||||
// Connection to reaplay server
|
||||
conn, err = net.Dial("tcp", Settings.ReplayServer())
|
||||
|
||||
if err != nil {
|
||||
log.Println("Connection error ", err, Settings.ReplayAddress)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Because its sub-program, Run acts as `main`
|
||||
func Run() {
|
||||
if os.Getuid() != 0 {
|
||||
fmt.Println("Please start the listener as root or sudo!")
|
||||
fmt.Println("This is required since listener sniff traffic on given port.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Listening for HTTP traffic on", Settings.Address+":"+strconv.Itoa(Settings.Port))
|
||||
fmt.Println("Forwarding requests to replay server:", Settings.ReplayServer(), "Limit:", Settings.ReplayLimit)
|
||||
|
||||
// Sniffing traffic from given address
|
||||
listener := RAWTCPListen(Settings.Address, Settings.Port)
|
||||
|
||||
currentTime := time.Now().UnixNano()
|
||||
currentRPS := 0
|
||||
|
||||
for {
|
||||
// Receiving TCPMessage object
|
||||
m := listener.Receive()
|
||||
|
||||
if Settings.ReplayLimit != 0 {
|
||||
if (time.Now().UnixNano() - currentTime) > time.Second.Nanoseconds() {
|
||||
currentTime = time.Now().UnixNano()
|
||||
currentRPS = 0
|
||||
}
|
||||
|
||||
if currentRPS >= Settings.ReplayLimit {
|
||||
continue
|
||||
}
|
||||
|
||||
currentRPS++
|
||||
}
|
||||
|
||||
go sendMessage(m)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(m *TCPMessage) {
|
||||
conn, err := ReplayServer()
|
||||
|
||||
if err != nil {
|
||||
log.Println("Failed to send message. Replay server not respond.")
|
||||
return
|
||||
} else {
|
||||
defer conn.Close()
|
||||
}
|
||||
|
||||
// For debugging purpose
|
||||
// Usually request parsing happens in replay part
|
||||
if Settings.Verbose {
|
||||
buf := bytes.NewBuffer(m.Bytes())
|
||||
reader := bufio.NewReader(buf)
|
||||
|
||||
request, err := http.ReadRequest(reader)
|
||||
|
||||
if err != nil {
|
||||
Debug("Error while parsing request:", err, string(m.Bytes()))
|
||||
} else {
|
||||
request.ParseMultipartForm(32 << 20)
|
||||
Debug("Forwarding request:", request)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = conn.Write(m.Bytes())
|
||||
|
||||
if err != nil {
|
||||
log.Println("Error while sending requests", err)
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func getTCPMessage() (msg *TCPMessage) {
|
||||
packet := &TCPPacket{Data: []byte("GET /pub/WWW/ HTTP/1.1\nHost: www.w3.org\r\n\r\n")}
|
||||
|
||||
return &TCPMessage{packets: []*TCPPacket{packet}}
|
||||
}
|
||||
|
||||
func mockReplayServer() (listener net.Listener) {
|
||||
listener, _ = net.Listen("tcp", "127.0.0.1:0")
|
||||
|
||||
Settings.ReplayAddress = listener.Addr().String()
|
||||
|
||||
fmt.Println(listener.Addr().String())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestSendMessage(t *testing.T) {
|
||||
Settings.Verbose = true
|
||||
|
||||
listener := mockReplayServer()
|
||||
|
||||
msg := getTCPMessage()
|
||||
|
||||
sendMessage(msg)
|
||||
|
||||
conn, _ := listener.Accept()
|
||||
defer conn.Close()
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, _ := conn.Read(buf)
|
||||
buf = buf[0:n]
|
||||
|
||||
if bytes.Compare(buf, msg.Bytes()) != 0 {
|
||||
t.Errorf("Original and reveived requests does not match")
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Capture traffic from socket using RAW_SOCKET's
|
||||
// http://en.wikipedia.org/wiki/Raw_socket
|
||||
//
|
||||
// RAW_SOCKET allow you listen for traffic on any port (e.g. sniffing) because they operate on IP level.
|
||||
// Ports is TCP feature, same as flow control, reliable transmission and etc.
|
||||
// Since we can't use default TCP libraries RAWTCPLitener implements own TCP layer
|
||||
// TCP packets is parsed using tcp_packet.go, and flow control is managed by tcp_message.go
|
||||
type RAWTCPListener struct {
|
||||
messages []*TCPMessage // buffer of TCPMessages waiting to be send
|
||||
|
||||
c_packets chan *TCPPacket
|
||||
c_messages chan *TCPMessage // Messages ready to be send to client
|
||||
|
||||
c_del_message chan *TCPMessage // Used for notifications about completed or expired messages
|
||||
|
||||
addr string // IP to listen
|
||||
port int // Port to listen
|
||||
}
|
||||
|
||||
func RAWTCPListen(addr string, port int) (listener *RAWTCPListener) {
|
||||
listener = &RAWTCPListener{}
|
||||
|
||||
listener.c_packets = make(chan *TCPPacket)
|
||||
listener.c_messages = make(chan *TCPMessage)
|
||||
listener.c_del_message = make(chan *TCPMessage)
|
||||
|
||||
listener.addr = addr
|
||||
listener.port = port
|
||||
|
||||
go listener.listen()
|
||||
go listener.readRAWSocket()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *RAWTCPListener) listen() {
|
||||
for {
|
||||
select {
|
||||
// If message ready for deletion it means that its also complete or expired by timeout
|
||||
case message := <-t.c_del_message:
|
||||
t.c_messages <- message
|
||||
t.deleteMessage(message)
|
||||
|
||||
// We need to use channgels to process each packet to avoid data races
|
||||
case packet := <-t.c_packets:
|
||||
t.processTCPPacket(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deleting messages that came from t.c_del_message channel
|
||||
func (t *RAWTCPListener) deleteMessage(message *TCPMessage) bool {
|
||||
var idx int = -1
|
||||
|
||||
// Searching for given message in messages buffer
|
||||
for i, m := range t.messages {
|
||||
if m.Ack == message.Ack {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Delete element from array
|
||||
// Note: that this version for arrays that consist of pointers
|
||||
// https://code.google.com/p/go-wiki/wiki/SliceTricks
|
||||
copy(t.messages[idx:], t.messages[idx+1:])
|
||||
t.messages[len(t.messages)-1] = nil // Ensure that value will be garbage-collected.
|
||||
t.messages = t.messages[:len(t.messages)-1]
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *RAWTCPListener) readRAWSocket() {
|
||||
conn, e := net.ListenPacket("ip4:tcp", t.addr)
|
||||
defer conn.Close()
|
||||
|
||||
if e != nil {
|
||||
log.Fatal(e)
|
||||
}
|
||||
|
||||
buf := make([]byte, 4096*2)
|
||||
|
||||
for {
|
||||
// Note: ReadFrom receive messages without IP header
|
||||
n, _, err := conn.ReadFrom(buf)
|
||||
|
||||
if err != nil {
|
||||
Debug("Error:", err)
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
// To avoid full packet parsing every time, we manually parsing values needed for packet filtering
|
||||
// http://en.wikipedia.org/wiki/Transmission_Control_Protocol
|
||||
dest_port := binary.BigEndian.Uint16(buf[2:4])
|
||||
|
||||
// Because RAW_SOCKET can't be bound to port, we have to control it by ourself
|
||||
if int(dest_port) == t.port {
|
||||
// Check TCPPacket code for more description
|
||||
flags := binary.BigEndian.Uint16(buf[12:14]) & 0x1FF
|
||||
f_psh := (flags & TCP_PSH) != 0
|
||||
|
||||
// We need only packets with data inside
|
||||
// TCP PSH flag indicate that packet have data inside
|
||||
if f_psh {
|
||||
// We should create new buffer because go slices is pointers. So buffer data shoud be immutable.
|
||||
new_buf := make([]byte, n)
|
||||
copy(new_buf, buf[:n])
|
||||
|
||||
// To avoid socket locking processing packet in new goroutine
|
||||
go func(buf []byte) {
|
||||
packet := NewTCPPacket(new_buf)
|
||||
t.c_packets <- packet
|
||||
}(new_buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 *RAWTCPListener) processTCPPacket(packet *TCPPacket) {
|
||||
var message *TCPMessage
|
||||
|
||||
// Searching for message with same Ack
|
||||
for _, msg := range t.messages {
|
||||
if msg.Ack == packet.Ack {
|
||||
message = msg
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if message == nil {
|
||||
// We sending c_del_message channel, so message object can communicate with Listener and notify it if message completed
|
||||
message = NewTCPMessage(packet.Ack, t.c_del_message)
|
||||
|
||||
t.messages = append(t.messages, message)
|
||||
}
|
||||
|
||||
// Adding packet to message
|
||||
message.c_packets <- packet
|
||||
}
|
||||
|
||||
func (t *RAWTCPListener) Receive() *TCPMessage {
|
||||
return <-t.c_messages
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPort = 80
|
||||
defaultAddress = "0.0.0.0"
|
||||
|
||||
defaultReplayAddress = "localhost:28020"
|
||||
)
|
||||
|
||||
type ListenerSettings struct {
|
||||
Port int
|
||||
Address string
|
||||
|
||||
ReplayAddress string
|
||||
|
||||
ReplayLimit int
|
||||
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
var Settings ListenerSettings = ListenerSettings{}
|
||||
|
||||
func (s *ListenerSettings) ReplayServer() string {
|
||||
host_info := strings.Split(s.ReplayAddress, "|")
|
||||
|
||||
if len(host_info) > 1 {
|
||||
s.ReplayLimit, _ = strconv.Atoi(host_info[1])
|
||||
}
|
||||
|
||||
return host_info[0]
|
||||
}
|
||||
|
||||
func init() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "listen" {
|
||||
return
|
||||
}
|
||||
|
||||
flag.IntVar(&Settings.Port, "p", defaultPort, "Specify the http server port whose traffic you want to capture")
|
||||
|
||||
flag.StringVar(&Settings.Address, "ip", defaultAddress, "Specifi IP address to listen")
|
||||
|
||||
flag.StringVar(&Settings.ReplayAddress, "r", defaultReplayAddress, "Address of replay server.")
|
||||
|
||||
flag.BoolVar(&Settings.Verbose, "verbose", false, "Log requests")
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
const MSG_EXPIRE = 200 * time.Millisecond
|
||||
|
||||
// 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: acknowledgement - message_id, and sequence - packet_id
|
||||
// Message can be compiled from unique packets with same message_id which sorted by sequence
|
||||
// Message is received if we did't receive any packets for 200ms
|
||||
type TCPMessage struct {
|
||||
Ack uint32 // Message ID
|
||||
packets []*TCPPacket
|
||||
|
||||
timer *time.Timer // Used for expire check
|
||||
|
||||
expired bool
|
||||
|
||||
c_packets chan *TCPPacket
|
||||
c_closing chan int
|
||||
|
||||
c_del_message chan *TCPMessage
|
||||
}
|
||||
|
||||
func NewTCPMessage(Ack uint32, c_del chan *TCPMessage) (msg *TCPMessage) {
|
||||
msg = &TCPMessage{Ack: Ack}
|
||||
|
||||
msg.c_packets = make(chan *TCPPacket)
|
||||
msg.c_closing = make(chan int)
|
||||
msg.c_del_message = c_del // used for notifying that message completed or expired
|
||||
|
||||
// Every time we receive packet we reset this timer
|
||||
msg.timer = time.AfterFunc(MSG_EXPIRE, msg.Timeout)
|
||||
|
||||
go msg.listen()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *TCPMessage) listen() {
|
||||
for {
|
||||
select {
|
||||
case <-t.c_closing:
|
||||
close(t.c_packets)
|
||||
return // Stop loop if message completed/expired
|
||||
case packet := <-t.c_packets:
|
||||
t.AddPacket(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TCPMessage) Timeout() {
|
||||
t.c_closing <- 1 // Notify to stop listen loop and close channel
|
||||
t.c_del_message <- t // Notify RAWListener that message is ready to be send to replay server
|
||||
}
|
||||
|
||||
// Sort packets in right orders and return message content
|
||||
func (t *TCPMessage) Bytes() (output []byte) {
|
||||
mk := make([]int, len(t.packets))
|
||||
|
||||
i := 0
|
||||
for k, _ := range t.packets {
|
||||
mk[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
sort.Ints(mk)
|
||||
|
||||
for _, k := range mk {
|
||||
output = append(output, t.packets[k].Data...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Add packet to the message and ensure packet uniquiness
|
||||
// TCP allows that packet can be re-send multiple times
|
||||
func (t *TCPMessage) AddPacket(packet *TCPPacket) {
|
||||
if t.expired {
|
||||
Debug("Adding packet to expired message")
|
||||
return
|
||||
}
|
||||
|
||||
packetFound := false
|
||||
|
||||
for _, pkt := range t.packets {
|
||||
if packet.Seq == pkt.Seq {
|
||||
packetFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if packetFound {
|
||||
Debug("Received packet with same sequence")
|
||||
} else {
|
||||
t.packets = append(t.packets, packet)
|
||||
}
|
||||
|
||||
// Reset message timeout timer
|
||||
t.timer.Reset(MSG_EXPIRE)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TCP Flags
|
||||
const (
|
||||
TCP_FIN = 1 << iota
|
||||
TCP_SYN
|
||||
TCP_RST
|
||||
TCP_PSH
|
||||
TCP_ACK
|
||||
TCP_URG
|
||||
TCP_ECE
|
||||
TCP_CWR
|
||||
TCP_NS
|
||||
)
|
||||
|
||||
// Simple TCP packet parser
|
||||
//
|
||||
// Packet structure: http://en.wikipedia.org/wiki/Transmission_Control_Protocol
|
||||
type TCPPacket struct {
|
||||
SrcPort uint16
|
||||
DestPort uint16
|
||||
Seq uint32
|
||||
Ack uint32
|
||||
DataOffset uint8
|
||||
Flags uint16
|
||||
Window uint16
|
||||
Checksum uint16
|
||||
Urgent uint16
|
||||
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func NewTCPPacket(b []byte) (p *TCPPacket) {
|
||||
p = &TCPPacket{Data: b}
|
||||
p.ParseFast()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Inspired by: https://github.com/miekg/pcap/blob/master/packet.go
|
||||
func (t *TCPPacket) Parse() {
|
||||
t.SrcPort = binary.BigEndian.Uint16(t.Data[0:2])
|
||||
t.DestPort = binary.BigEndian.Uint16(t.Data[2:4])
|
||||
t.Seq = binary.BigEndian.Uint32(t.Data[4:8])
|
||||
t.Ack = binary.BigEndian.Uint32(t.Data[8:12])
|
||||
t.DataOffset = (t.Data[12] & 0xF0) >> 4
|
||||
t.Flags = binary.BigEndian.Uint16(t.Data[12:14]) & 0x1FF
|
||||
t.Window = binary.BigEndian.Uint16(t.Data[14:16])
|
||||
t.Checksum = binary.BigEndian.Uint16(t.Data[16:18])
|
||||
t.Urgent = binary.BigEndian.Uint16(t.Data[18:20])
|
||||
|
||||
t.Data = t.Data[t.DataOffset*4:]
|
||||
}
|
||||
|
||||
// Parse only needed set of fields
|
||||
func (t *TCPPacket) ParseFast() {
|
||||
t.Seq = binary.BigEndian.Uint32(t.Data[4:8])
|
||||
t.Ack = binary.BigEndian.Uint32(t.Data[8:12])
|
||||
|
||||
t.DataOffset = (t.Data[12] & 0xF0) >> 4
|
||||
t.Data = t.Data[t.DataOffset*4:]
|
||||
}
|
||||
|
||||
func (t *TCPPacket) String() string {
|
||||
return strings.Join([]string{
|
||||
"Source port: " + strconv.Itoa(int(t.SrcPort)),
|
||||
"Dest port:" + strconv.Itoa(int(t.DestPort)),
|
||||
"Sequence:" + strconv.Itoa(int(t.Seq)),
|
||||
"Acknowledgement:" + strconv.Itoa(int(t.Ack)),
|
||||
"Header len:" + strconv.Itoa(int(t.DataOffset)),
|
||||
|
||||
"Flag ns:" + strconv.FormatBool(t.Flags&TCP_NS != 0),
|
||||
"Flag crw:" + strconv.FormatBool(t.Flags&TCP_CWR != 0),
|
||||
"Flag ece:" + strconv.FormatBool(t.Flags&TCP_ECE != 0),
|
||||
"Flag urg:" + strconv.FormatBool(t.Flags&TCP_URG != 0),
|
||||
"Flag ack:" + strconv.FormatBool(t.Flags&TCP_ACK != 0),
|
||||
"Flag psh:" + strconv.FormatBool(t.Flags&TCP_PSH != 0),
|
||||
"Flag rst:" + strconv.FormatBool(t.Flags&TCP_RST != 0),
|
||||
"Flag syn:" + strconv.FormatBool(t.Flags&TCP_SYN != 0),
|
||||
"Flag fin:" + strconv.FormatBool(t.Flags&TCP_FIN != 0),
|
||||
|
||||
"Window size:" + strconv.Itoa(int(t.Window)),
|
||||
"Checksum:" + strconv.Itoa(int(t.Checksum)),
|
||||
|
||||
"Data:" + string(t.Data),
|
||||
}, "\n")
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Middleware struct {
|
||||
command string
|
||||
|
||||
data chan []byte
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
Stdin io.Writer
|
||||
Stdout io.Reader
|
||||
}
|
||||
|
||||
func NewMiddleware(command string) *Middleware {
|
||||
m := new(Middleware)
|
||||
m.command = command
|
||||
m.data = make(chan []byte, 1000)
|
||||
|
||||
commands := strings.Split(command, " ")
|
||||
cmd := exec.Command(commands[0], commands[1:]...)
|
||||
|
||||
m.Stdout, _ = cmd.StdoutPipe()
|
||||
m.Stdin, _ = cmd.StdinPipe()
|
||||
|
||||
if Settings.verbose {
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
go m.read(m.Stdout)
|
||||
|
||||
go func() {
|
||||
err := cmd.Start()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Middleware) ReadFrom(plugin io.Reader) {
|
||||
Debug("[MIDDLEWARE-MASTER] Starting reading from", plugin)
|
||||
go m.copy(m.Stdin, plugin)
|
||||
}
|
||||
|
||||
func (m *Middleware) copy(to io.Writer, from io.Reader) {
|
||||
buf := make([]byte, 5*1024*1024)
|
||||
dst := make([]byte, len(buf)*2)
|
||||
|
||||
for {
|
||||
nr, _ := from.Read(buf)
|
||||
if nr > 0 && len(buf) > nr {
|
||||
|
||||
hex.Encode(dst, buf[0:nr])
|
||||
dst[nr*2] = '\n'
|
||||
|
||||
m.mu.Lock()
|
||||
to.Write(dst[0 : nr*2+1])
|
||||
m.mu.Unlock()
|
||||
|
||||
if Settings.debug {
|
||||
Debug("[MIDDLEWARE-MASTER] Sending:", string(buf[0:nr]), "From:", from)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Middleware) read(from io.Reader) {
|
||||
scanner := bufio.NewScanner(from)
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Middleware) Read(data []byte) (int, error) {
|
||||
buf := <-m.data
|
||||
copy(data, buf)
|
||||
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (m *Middleware) String() string {
|
||||
return fmt.Sprintf("Modifying traffic using '%s' command", m.command)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"github.com/buger/gor/proto"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeServiceCb func(string, int, []byte)
|
||||
|
||||
// Simple service that generate token on request, and require this token for accesing to secure area
|
||||
func NewFakeSecureService(wg *sync.WaitGroup, cb fakeServiceCb) *httptest.Server {
|
||||
active_tokens := make([]string, 0)
|
||||
var mu sync.Mutex
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
Debug("Received request: " + req.URL.String())
|
||||
|
||||
switch req.URL.Path {
|
||||
case "/token":
|
||||
// Generate random token
|
||||
token_length := 10
|
||||
buf := make([]byte, token_length)
|
||||
rand.Read(buf)
|
||||
token := hex.EncodeToString(buf)
|
||||
active_tokens = append(active_tokens, token)
|
||||
|
||||
w.Write([]byte(token))
|
||||
|
||||
cb(req.URL.Path, 200, []byte(token))
|
||||
case "/secure":
|
||||
token := req.URL.Query().Get("token")
|
||||
token_found := false
|
||||
|
||||
for _, t := range active_tokens {
|
||||
if t == token {
|
||||
token_found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if token_found {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
cb(req.URL.Path, 202, []byte(nil))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
cb(req.URL.Path, 403, []byte(nil))
|
||||
}
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func TestFakeSecureService(t *testing.T) {
|
||||
var resp, token []byte
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
server := NewFakeSecureService(wg, func(path string, status int, resp []byte) {
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
wg.Add(3)
|
||||
|
||||
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true})
|
||||
resp, _ = client.Get("/token")
|
||||
token = proto.Body(resp)
|
||||
|
||||
// Right token
|
||||
resp, _ = client.Get("/secure?token=" + string(token))
|
||||
if !bytes.Equal(proto.Status(resp), []byte("202")) {
|
||||
t.Error("Valid token should return status 202:", string(proto.Status(resp)))
|
||||
}
|
||||
|
||||
// Wrong tokens forbidden
|
||||
resp, _ = client.Get("/secure?token=wrong")
|
||||
if !bytes.Equal(proto.Status(resp), []byte("403")) {
|
||||
t.Error("Wrong token should returns status 403:", string(proto.Status(resp)))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestEchoMiddleware(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
from := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Env", "prod")
|
||||
w.Header().Set("RequestPath", r.URL.Path)
|
||||
wg.Done()
|
||||
}))
|
||||
defer from.Close()
|
||||
|
||||
to := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Env", "test")
|
||||
w.Header().Set("RequestPath", r.URL.Path)
|
||||
wg.Done()
|
||||
}))
|
||||
defer to.Close()
|
||||
|
||||
quit := make(chan int)
|
||||
|
||||
Settings.middleware = "./examples/middleware/echo.sh"
|
||||
|
||||
// Catch traffic from one service
|
||||
input := NewRAWInput(from.Listener.Addr().String(), testRawExpire)
|
||||
defer input.Close()
|
||||
|
||||
// And redirect to another
|
||||
output := NewHTTPOutput(to.URL, &HTTPOutputConfig{Debug: false})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
// Start Gor
|
||||
go Start(quit)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
// Should receive 2 requests from original + 2 from replayed
|
||||
client := NewHTTPClient(from.URL, &HTTPClientConfig{Debug: false})
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(4)
|
||||
// Request should be echoed
|
||||
client.Get("/a")
|
||||
client.Get("/b")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
Settings.middleware = ""
|
||||
}
|
||||
|
||||
func TestTokenMiddleware(t *testing.T) {
|
||||
var resp, token []byte
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
from := NewFakeSecureService(wg, func(path string, status int, tok []byte) {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
})
|
||||
defer from.Close()
|
||||
|
||||
to := NewFakeSecureService(wg, func(path string, status int, tok []byte) {
|
||||
switch path {
|
||||
case "/secure":
|
||||
if status != 202 {
|
||||
t.Error("Server should receive valid rewritten token")
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
})
|
||||
defer to.Close()
|
||||
|
||||
quit := make(chan int)
|
||||
|
||||
Settings.middleware = "go run ./examples/middleware/token_modifier.go"
|
||||
|
||||
fromAddr := strings.Replace(from.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
// Catch traffic from one service
|
||||
input := NewRAWInput(fromAddr, testRawExpire)
|
||||
defer input.Close()
|
||||
|
||||
// And redirect to another
|
||||
output := NewHTTPOutput(to.URL, &HTTPOutputConfig{Debug: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
// Start Gor
|
||||
go Start(quit)
|
||||
|
||||
// Wait for middleware to initialize
|
||||
// Give go compiller time to build programm
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Should receive 2 requests from original + 2 from replayed
|
||||
wg.Add(4)
|
||||
|
||||
client := NewHTTPClient(from.URL, &HTTPClientConfig{Debug: false})
|
||||
|
||||
// Sending traffic to original service
|
||||
resp, _ = client.Get("/token")
|
||||
token = proto.Body(resp)
|
||||
|
||||
// When delay is too smal, middleware does not always rewrite requests in time
|
||||
// Hopefuly client will have delay more then 10ms :)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
resp, _ = client.Get("/secure?token=" + string(token))
|
||||
if !bytes.Equal(proto.Status(resp), []byte("202")) {
|
||||
t.Error("Valid token should return 202:", proto.Status(resp))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
Settings.middleware = ""
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// DummyOutput used for debugging, prints all incoming requests
|
||||
type DummyOutput struct {
|
||||
}
|
||||
|
||||
// NewDummyOutput constructor for DummyOutput
|
||||
func NewDummyOutput(options string) (di *DummyOutput) {
|
||||
di = new(DummyOutput)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *DummyOutput) Write(data []byte) (int, error) {
|
||||
fmt.Println("Writing message: ", string(data))
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (i *DummyOutput) String() string {
|
||||
return "Dummy Output"
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// FileOutput output plugin
|
||||
type FileOutput struct {
|
||||
path string
|
||||
file *os.File
|
||||
}
|
||||
|
||||
// NewFileOutput constructor for FileOutput, accepts path
|
||||
func NewFileOutput(path string) io.Writer {
|
||||
o := new(FileOutput)
|
||||
o.path = path
|
||||
o.init(path)
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *FileOutput) init(path string) {
|
||||
var err error
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *FileOutput) Write(data []byte) (n int, err error) {
|
||||
if !isOriginPayload(data) {
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
o.file.Write(data)
|
||||
o.file.Write([]byte(payloadSeparator))
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (o *FileOutput) String() string {
|
||||
return "File output: " + o.path
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileOutput(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewFileOutput("/tmp/test_requests.gor")
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(2)
|
||||
input.EmitGET()
|
||||
input.EmitPOST()
|
||||
}
|
||||
close(quit)
|
||||
|
||||
quit = make(chan int)
|
||||
|
||||
input2 := NewFileInput("/tmp/test_requests.gor")
|
||||
output2 := NewTestOutput(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input2}
|
||||
Plugins.Outputs = []io.Writer{output2}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/buger/gor/proto"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const initialDynamicWorkers = 10
|
||||
|
||||
type response struct {
|
||||
payload []byte
|
||||
uuid []byte
|
||||
roundTripTime int64
|
||||
}
|
||||
|
||||
// HTTPOutputConfig struct for holding http output configuration
|
||||
type HTTPOutputConfig struct {
|
||||
redirectLimit int
|
||||
|
||||
stats bool
|
||||
workers int
|
||||
|
||||
elasticSearch string
|
||||
|
||||
Timeout time.Duration
|
||||
OriginalHost bool
|
||||
|
||||
Debug bool
|
||||
|
||||
TrackResponses bool
|
||||
}
|
||||
|
||||
// HTTPOutput plugin manage pool of workers which send request to replayed server
|
||||
// By default workers pool is dynamic and starts with 10 workers
|
||||
// You can specify fixed number of workers using `--output-http-workers`
|
||||
type HTTPOutput struct {
|
||||
// Keep this as first element of struct because it guarantees 64bit
|
||||
// alignment. atomic.* functions crash on 32bit machines if operand is not
|
||||
// aligned at 64bit. See https://github.com/golang/go/issues/599
|
||||
activeWorkers int64
|
||||
|
||||
address string
|
||||
limit int
|
||||
queue chan []byte
|
||||
|
||||
responses chan response
|
||||
|
||||
needWorker chan int
|
||||
|
||||
config *HTTPOutputConfig
|
||||
|
||||
queueStats *GorStat
|
||||
|
||||
elasticSearch *ESPlugin
|
||||
}
|
||||
|
||||
// NewHTTPOutput constructor for HTTPOutput
|
||||
// Initialize workers
|
||||
func NewHTTPOutput(address string, config *HTTPOutputConfig) io.Writer {
|
||||
o := new(HTTPOutput)
|
||||
|
||||
o.address = address
|
||||
o.config = config
|
||||
|
||||
if o.config.stats {
|
||||
o.queueStats = NewGorStat("output_http")
|
||||
}
|
||||
|
||||
o.queue = make(chan []byte, 1000)
|
||||
o.responses = make(chan response, 1000)
|
||||
o.needWorker = make(chan int, 1)
|
||||
|
||||
// Initial workers count
|
||||
if o.config.workers == 0 {
|
||||
o.needWorker <- initialDynamicWorkers
|
||||
} else {
|
||||
o.needWorker <- o.config.workers
|
||||
}
|
||||
|
||||
if o.config.elasticSearch != "" {
|
||||
o.elasticSearch = new(ESPlugin)
|
||||
o.elasticSearch.Init(o.config.elasticSearch)
|
||||
}
|
||||
|
||||
if len(Settings.middleware) > 0 {
|
||||
o.config.TrackResponses = true
|
||||
}
|
||||
|
||||
go o.workerMaster()
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) workerMaster() {
|
||||
for {
|
||||
newWorkers := <-o.needWorker
|
||||
for i := 0; i < newWorkers; i++ {
|
||||
go o.startWorker()
|
||||
}
|
||||
|
||||
// Disable dynamic scaling if workers poll fixed size
|
||||
if o.config.workers != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) startWorker() {
|
||||
client := NewHTTPClient(o.address, &HTTPClientConfig{
|
||||
FollowRedirects: o.config.redirectLimit,
|
||||
Debug: o.config.Debug,
|
||||
OriginalHost: o.config.OriginalHost,
|
||||
Timeout: o.config.Timeout,
|
||||
})
|
||||
|
||||
deathCount := 0
|
||||
|
||||
atomic.AddInt64(&o.activeWorkers, 1)
|
||||
|
||||
for {
|
||||
select {
|
||||
case data := <-o.queue:
|
||||
o.sendRequest(client, data)
|
||||
deathCount = 0
|
||||
case <-time.After(time.Millisecond * 100):
|
||||
// When dynamic scaling enabled workers die after 2s of inactivity
|
||||
if o.config.workers == 0 {
|
||||
deathCount++
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if deathCount > 20 {
|
||||
workersCount := atomic.LoadInt64(&o.activeWorkers)
|
||||
|
||||
// At least 1 startWorker should be alive
|
||||
if workersCount != 1 {
|
||||
atomic.AddInt64(&o.activeWorkers, -1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) Write(data []byte) (n int, err error) {
|
||||
if !isRequestPayload(data) {
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
buf := make([]byte, len(data))
|
||||
copy(buf, data)
|
||||
|
||||
o.queue <- buf
|
||||
|
||||
if o.config.stats {
|
||||
o.queueStats.Write(len(o.queue))
|
||||
}
|
||||
|
||||
if o.config.workers == 0 {
|
||||
workersCount := atomic.LoadInt64(&o.activeWorkers)
|
||||
|
||||
if len(o.queue) > int(workersCount) {
|
||||
o.needWorker <- len(o.queue)
|
||||
}
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) Read(data []byte) (int, error) {
|
||||
resp := <-o.responses
|
||||
|
||||
Debug("[OUTPUT-HTTP] Received response:", string(resp.payload))
|
||||
|
||||
header := payloadHeader(ReplayedResponsePayload, resp.uuid, resp.roundTripTime)
|
||||
copy(data[0:len(header)], header)
|
||||
copy(data[len(header):], resp.payload)
|
||||
|
||||
return len(resp.payload) + len(header), nil
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) sendRequest(client *HTTPClient, request []byte) {
|
||||
meta := payloadMeta(request)
|
||||
if len(meta) < 2 {
|
||||
return
|
||||
}
|
||||
uuid := meta[1]
|
||||
|
||||
body := payloadBody(request)
|
||||
if !proto.IsHTTPPayload(body) {
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.Send(body)
|
||||
stop := time.Now()
|
||||
|
||||
if err != nil {
|
||||
Debug("Request error:", err)
|
||||
}
|
||||
|
||||
if o.config.TrackResponses {
|
||||
o.responses <- response{resp, uuid, stop.UnixNano() - start.UnixNano()}
|
||||
}
|
||||
|
||||
if o.elasticSearch != nil {
|
||||
o.elasticSearch.ResponseAnalyze(request, resp, start, stop)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *HTTPOutput) String() string {
|
||||
return "HTTP output: " + o.address
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
_ "net/http/httputil"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestHTTPOutput(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Header.Get("User-Agent") != "Gor" {
|
||||
t.Error("Wrong header")
|
||||
}
|
||||
|
||||
if req.Method == "OPTIONS" {
|
||||
t.Error("Wrong method")
|
||||
}
|
||||
|
||||
if req.Method == "POST" {
|
||||
defer req.Body.Close()
|
||||
body, _ := ioutil.ReadAll(req.Body)
|
||||
|
||||
if string(body) != "a=1&b=2" {
|
||||
t.Error("Wrong POST body:", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
headers := HTTPHeaders{HTTPHeader{"User-Agent", "Gor"}}
|
||||
methods := HTTPMethods{[]byte("GET"), []byte("PUT"), []byte("POST")}
|
||||
Settings.modifierConfig = HTTPModifierConfig{headers: headers, methods: methods}
|
||||
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{Debug: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(2) // OPTIONS should be ignored
|
||||
input.EmitPOST()
|
||||
input.EmitOPTIONS()
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
|
||||
Settings.modifierConfig = HTTPModifierConfig{}
|
||||
}
|
||||
|
||||
func TestHTTPOutputKeepOriginalHost(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
input := NewTestInput()
|
||||
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Host != "custom-host.com" {
|
||||
t.Error("Wrong header", req.Host)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
headers := HTTPHeaders{HTTPHeader{"Host", "custom-host.com"}}
|
||||
Settings.modifierConfig = HTTPModifierConfig{headers: headers}
|
||||
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{Debug: false, OriginalHost: true})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
|
||||
Settings.modifierConfig = HTTPModifierConfig{}
|
||||
}
|
||||
|
||||
func TestOutputHTTPSSL(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
// Origing and Replay server initialization
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
wg.Done()
|
||||
}))
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
input.EmitPOST()
|
||||
input.EmitGET()
|
||||
|
||||
wg.Wait()
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func BenchmarkHTTPOutput(b *testing.B) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
wg.Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
input := NewTestInput()
|
||||
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{})
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitPOST()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TCPOutput used for sending raw tcp payloads
|
||||
// Currently used for internal communication between listener and replay server
|
||||
// Can be used for transfering binary payloads like protocol buffers
|
||||
type TCPOutput struct {
|
||||
address string
|
||||
limit int
|
||||
buf chan []byte
|
||||
bufStats *GorStat
|
||||
}
|
||||
|
||||
// NewTCPOutput constructor for TCPOutput
|
||||
// Initialize 10 workers which hold keep-alive connection
|
||||
func NewTCPOutput(address string) io.Writer {
|
||||
o := new(TCPOutput)
|
||||
|
||||
o.address = address
|
||||
|
||||
o.buf = make(chan []byte, 100)
|
||||
if Settings.outputTCPStats {
|
||||
o.bufStats = NewGorStat("output_tcp")
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go o.worker()
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *TCPOutput) worker() {
|
||||
conn, err := o.connect(o.address)
|
||||
for ; err != nil; conn, err = o.connect(o.address) {
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
for {
|
||||
conn.Write(<-o.buf)
|
||||
_, err := conn.Write([]byte(payloadSeparator))
|
||||
|
||||
if err != nil {
|
||||
log.Println("Worker failed on write, exitings and starting new worker")
|
||||
go o.worker()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *TCPOutput) Write(data []byte) (n int, err error) {
|
||||
if !isOriginPayload(data) {
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// We have to copy, because sending data in multiple threads
|
||||
newBuf := make([]byte, len(data))
|
||||
copy(newBuf, data)
|
||||
|
||||
o.buf <- newBuf
|
||||
|
||||
if Settings.outputTCPStats {
|
||||
o.bufStats.Write(len(o.buf))
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (o *TCPOutput) connect(address string) (conn net.Conn, err error) {
|
||||
conn, err = net.Dial("tcp", address)
|
||||
|
||||
if err != nil {
|
||||
log.Println("Connection error ", err, o.address)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (o *TCPOutput) String() string {
|
||||
return fmt.Sprintf("TCP output %s, limit: %d", o.address, o.limit)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTCPOutput(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
listener := startTCP(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
input := NewTestInput()
|
||||
output := NewTCPOutput(listener.Addr().String())
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func startTCP(cb func([]byte)) net.Listener {
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Can't start:", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, _ := listener.Accept()
|
||||
defer conn.Close()
|
||||
|
||||
go func() {
|
||||
reader := bufio.NewReader(conn)
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Split(payloadScanner)
|
||||
|
||||
for scanner.Scan() {
|
||||
cb(scanner.Bytes())
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
return listener
|
||||
}
|
||||
|
||||
func BenchmarkTCPOutput(b *testing.B) {
|
||||
wg := new(sync.WaitGroup)
|
||||
quit := make(chan int)
|
||||
|
||||
listener := startTCP(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
input := NewTestInput()
|
||||
output := NewTCPOutput(listener.Addr().String())
|
||||
|
||||
Plugins.Inputs = []io.Reader{input}
|
||||
Plugins.Outputs = []io.Writer{output}
|
||||
|
||||
go Start(quit)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(quit)
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InOutPlugins struct for holding references to plugins
|
||||
type InOutPlugins struct {
|
||||
Inputs []io.Reader
|
||||
Outputs []io.Writer
|
||||
}
|
||||
|
||||
// Plugins holds all the plugin objects
|
||||
var Plugins *InOutPlugins = new(InOutPlugins)
|
||||
|
||||
// extractLimitOptions detects if plugin get called with limiter support
|
||||
// Returns address and limit
|
||||
func extractLimitOptions(options string) (string, string) {
|
||||
split := strings.Split(options, "|")
|
||||
|
||||
if len(split) > 1 {
|
||||
return split[0], split[1]
|
||||
}
|
||||
|
||||
return split[0], ""
|
||||
}
|
||||
|
||||
// Automatically detects type of plugin and initialize it
|
||||
//
|
||||
// See this article if curious about relfect stuff below: http://blog.burntsushi.net/type-parametric-functions-golang
|
||||
func registerPlugin(constructor interface{}, options ...interface{}) {
|
||||
vc := reflect.ValueOf(constructor)
|
||||
|
||||
// Pre-processing options to make it work with reflect
|
||||
vo := []reflect.Value{}
|
||||
for _, oi := range options {
|
||||
vo = append(vo, reflect.ValueOf(oi))
|
||||
}
|
||||
|
||||
// Removing limit options from path
|
||||
path, limit := extractLimitOptions(vo[0].String())
|
||||
|
||||
// Writing value back without limiter "|" options
|
||||
vo[0] = reflect.ValueOf(path)
|
||||
|
||||
// Calling our constructor with list of given options
|
||||
plugin := vc.Call(vo)[0].Interface()
|
||||
pluginWrapper := plugin
|
||||
|
||||
if limit != "" {
|
||||
pluginWrapper = NewLimiter(plugin, limit)
|
||||
} else {
|
||||
pluginWrapper = plugin
|
||||
}
|
||||
|
||||
_, isR := plugin.(io.Reader)
|
||||
_, isW := plugin.(io.Writer)
|
||||
|
||||
// Some of the output can be Readers as well because return responses
|
||||
if isR && !isW {
|
||||
Plugins.Inputs = append(Plugins.Inputs, pluginWrapper.(io.Reader))
|
||||
}
|
||||
|
||||
if isW {
|
||||
Plugins.Outputs = append(Plugins.Outputs, pluginWrapper.(io.Writer))
|
||||
}
|
||||
}
|
||||
|
||||
// InitPlugins specify and initialize all available plugins
|
||||
func InitPlugins() {
|
||||
for _, options := range Settings.inputDummy {
|
||||
registerPlugin(NewDummyInput, options)
|
||||
}
|
||||
|
||||
for _, options := range Settings.outputDummy {
|
||||
registerPlugin(NewDummyOutput, options)
|
||||
}
|
||||
|
||||
for _, options := range Settings.inputRAW {
|
||||
registerPlugin(NewRAWInput, options, time.Duration(0))
|
||||
}
|
||||
|
||||
for _, options := range Settings.inputTCP {
|
||||
registerPlugin(NewTCPInput, options)
|
||||
}
|
||||
|
||||
for _, options := range Settings.outputTCP {
|
||||
registerPlugin(NewTCPOutput, options)
|
||||
}
|
||||
|
||||
for _, options := range Settings.inputFile {
|
||||
registerPlugin(NewFileInput, options)
|
||||
}
|
||||
|
||||
for _, options := range Settings.outputFile {
|
||||
registerPlugin(NewFileOutput, options)
|
||||
}
|
||||
|
||||
for _, options := range Settings.inputHTTP {
|
||||
registerPlugin(NewHTTPInput, options)
|
||||
}
|
||||
|
||||
// If we explicitly set Host header http output should not rewrite it
|
||||
// Fix: https://github.com/buger/gor/issues/174
|
||||
for _, header := range Settings.modifierConfig.headers {
|
||||
if header.Name == "Host" {
|
||||
Settings.outputHTTPConfig.OriginalHost = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, options := range Settings.outputHTTP {
|
||||
registerPlugin(NewHTTPOutput, options, &Settings.outputHTTPConfig)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPluginsRegistration(t *testing.T) {
|
||||
Plugins.Inputs = []io.Reader{}
|
||||
Plugins.Outputs = []io.Writer{}
|
||||
|
||||
Settings.inputDummy = MultiOption{"[]"}
|
||||
Settings.outputDummy = MultiOption{"[]"}
|
||||
Settings.outputHTTP = MultiOption{"www.example.com|10"}
|
||||
Settings.inputFile = MultiOption{"/dev/null"}
|
||||
|
||||
InitPlugins()
|
||||
|
||||
if len(Plugins.Inputs) != 2 {
|
||||
t.Errorf("Should be 2 inputs %d", len(Plugins.Inputs))
|
||||
}
|
||||
|
||||
if _, ok := Plugins.Inputs[0].(*DummyInput); !ok {
|
||||
t.Errorf("First input should be DummyInput")
|
||||
}
|
||||
|
||||
if _, ok := Plugins.Inputs[1].(*FileInput); !ok {
|
||||
t.Errorf("Second input should be FileInput")
|
||||
}
|
||||
|
||||
if len(Plugins.Outputs) != 2 {
|
||||
t.Errorf("Should be 2 output %d", len(Plugins.Outputs))
|
||||
}
|
||||
|
||||
if _, ok := Plugins.Outputs[0].(*DummyOutput); !ok {
|
||||
t.Errorf("First output should be DummyOutput")
|
||||
}
|
||||
|
||||
if l, ok := Plugins.Outputs[1].(*Limiter); ok {
|
||||
if _, ok := l.plugin.(*HTTPOutput); !ok {
|
||||
t.Errorf("HTTPOutput should be wrapped in limiter")
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Second output should be Limiter")
|
||||
}
|
||||
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
Package proto provides byte-level interaction with HTTP request payload.
|
||||
|
||||
Example of HTTP payload for future references, new line symbols escaped:
|
||||
|
||||
POST /upload HTTP/1.1\r\n
|
||||
User-Agent: Gor\r\n
|
||||
Content-Length: 11\r\n
|
||||
\r\n
|
||||
Hello world
|
||||
|
||||
GET /index.html HTTP/1.1\r\n
|
||||
User-Agent: Gor\r\n
|
||||
\r\n
|
||||
\r\n
|
||||
*/
|
||||
package proto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/buger/gor/byteutils"
|
||||
)
|
||||
|
||||
// In HTTP newline defined by 2 bytes (for both windows and *nix support)
|
||||
var CLRF = []byte("\r\n")
|
||||
|
||||
// New line acts as separator: end of Headers or Body (in some cases)
|
||||
var EmptyLine = []byte("\r\n\r\n")
|
||||
|
||||
// Separator for Header line. Header looks like: `HeaderName: value`
|
||||
var HeaderDelim = []byte(": ")
|
||||
|
||||
// MIMEHeadersEndPos finds end of the Headers section, which should end with empty line.
|
||||
func MIMEHeadersEndPos(payload []byte) int {
|
||||
return bytes.Index(payload, EmptyLine)
|
||||
}
|
||||
|
||||
// MIMEHeadersStartPos finds start of Headers section
|
||||
// It just finds position of second line (first contains location and method).
|
||||
func MIMEHeadersStartPos(payload []byte) int {
|
||||
return bytes.Index(payload, CLRF) + 2 // Find first line end
|
||||
}
|
||||
|
||||
// header return value and positions of header/value start/end.
|
||||
// If not found, value will be blank, and headerStart will be -1
|
||||
// Do not support multi-line headers.
|
||||
func header(payload []byte, name []byte) (value []byte, headerStart, valueStart, headerEnd int) {
|
||||
headerStart = bytes.Index(payload, name)
|
||||
|
||||
if headerStart == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
valueStart = headerStart + len(name) + 1 // Skip ":" after header name
|
||||
if payload[valueStart] == ' ' { // Ignore empty space after ':'
|
||||
valueStart++
|
||||
}
|
||||
|
||||
headerEnd = valueStart + bytes.IndexByte(payload[valueStart:], '\n')
|
||||
|
||||
if payload[headerEnd - 1] == '\r' {
|
||||
headerEnd -= 1
|
||||
}
|
||||
|
||||
value = payload[valueStart:headerEnd]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Header returns header value, if header not found, value will be blank
|
||||
func Header(payload, name []byte) []byte {
|
||||
val, _, _, _ := header(payload, name)
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// SetHeader sets header value. If header not found it creates new one.
|
||||
// Returns modified request payload
|
||||
func SetHeader(payload, name, value []byte) []byte {
|
||||
_, hs, vs, he := header(payload, name)
|
||||
|
||||
if hs != -1 {
|
||||
// If header found we just repace its value
|
||||
return byteutils.Replace(payload, vs, he, value)
|
||||
}
|
||||
|
||||
return AddHeader(payload, name, value)
|
||||
}
|
||||
|
||||
// AddHeader takes http payload and appends new header to the start of headers section
|
||||
// Returns modified request payload
|
||||
func AddHeader(payload, name, value []byte) []byte {
|
||||
header := make([]byte, len(name)+2+len(value)+2)
|
||||
copy(header[0:], name)
|
||||
copy(header[len(name):], HeaderDelim)
|
||||
copy(header[len(name)+2:], value)
|
||||
copy(header[len(header)-2:], CLRF)
|
||||
|
||||
mimeStart := MIMEHeadersStartPos(payload)
|
||||
|
||||
return byteutils.Insert(payload, mimeStart, header)
|
||||
}
|
||||
|
||||
// Body returns request/response body
|
||||
func Body(payload []byte) []byte {
|
||||
// 4 -> len(EMPTY_LINE)
|
||||
return payload[MIMEHeadersEndPos(payload)+4:]
|
||||
}
|
||||
|
||||
// Path takes payload and retuns request path: Split(firstLine, ' ')[1]
|
||||
func Path(payload []byte) []byte {
|
||||
start := bytes.IndexByte(payload, ' ') + 1
|
||||
end := bytes.IndexByte(payload[start:], ' ')
|
||||
|
||||
return payload[start : start+end]
|
||||
}
|
||||
|
||||
// SetPath takes payload, sets new path and returns modified payload
|
||||
func SetPath(payload, path []byte) []byte {
|
||||
start := bytes.IndexByte(payload, ' ') + 1
|
||||
end := bytes.IndexByte(payload[start:], ' ')
|
||||
|
||||
return byteutils.Replace(payload, start, start+end, path)
|
||||
}
|
||||
|
||||
// PathParam returns URL query attribute by given name, if no found: valueStart will be -1
|
||||
func PathParam(payload, name []byte) (value []byte, valueStart, valueEnd int) {
|
||||
path := Path(payload)
|
||||
|
||||
if paramStart := bytes.Index(path, append(name, '=')); paramStart != -1 {
|
||||
valueStart := paramStart + len(name) + 1
|
||||
paramEnd := bytes.IndexByte(path[valueStart:], '&')
|
||||
|
||||
// Param can end with '&' (another param), or end of line
|
||||
if paramEnd == -1 { // It is final param
|
||||
paramEnd = len(path)
|
||||
} else {
|
||||
paramEnd += valueStart
|
||||
}
|
||||
|
||||
return path[valueStart:paramEnd], valueStart, paramEnd
|
||||
}
|
||||
|
||||
return []byte(""), -1, -1
|
||||
}
|
||||
|
||||
// SetPathParam takes payload and updates path Query attribute
|
||||
// If query param not found, it will append new
|
||||
// Returns modified payload
|
||||
func SetPathParam(payload, name, value []byte) []byte {
|
||||
path := Path(payload)
|
||||
_, vs, ve := PathParam(payload, name)
|
||||
|
||||
if vs != -1 { // If param found, replace its value and set new Path
|
||||
newPath := make([]byte, len(path))
|
||||
copy(newPath, path)
|
||||
newPath = byteutils.Replace(newPath, vs, ve, value)
|
||||
|
||||
return SetPath(payload, newPath)
|
||||
}
|
||||
|
||||
// if param not found append to end of url
|
||||
// Adding 2 because of '?' or '&' at start, and '=' in middle
|
||||
newParam := make([]byte, len(name)+len(value)+2)
|
||||
|
||||
if bytes.IndexByte(path, '?') == -1 {
|
||||
newParam[0] = '?'
|
||||
} else {
|
||||
newParam[0] = '&'
|
||||
}
|
||||
|
||||
// Copy "param=value" into buffer, after it looks like "?param=value"
|
||||
copy(newParam[1:], name)
|
||||
newParam[1+len(name)] = '='
|
||||
copy(newParam[2+len(name):], value)
|
||||
|
||||
// Append param to the end of path
|
||||
newPath := make([]byte, len(path)+len(newParam))
|
||||
copy(newPath, path)
|
||||
copy(newPath[len(path):], newParam)
|
||||
|
||||
return SetPath(payload, newPath)
|
||||
}
|
||||
|
||||
// SetHost updates Host header for HTTP/1.1 or updates host in path for HTTP/1.0 or Proxy requests
|
||||
// Returns modified payload
|
||||
func SetHost(payload, url, host []byte) []byte {
|
||||
// If this is HTTP 1.0 traffic or proxy traffic it may include host right into path variable, so instead of setting Host header we rewrite Path
|
||||
// Fix for https://github.com/buger/gor/issues/156
|
||||
if path := Path(payload); bytes.HasPrefix(path, []byte("http")) {
|
||||
hostStart := bytes.IndexByte(path, ':') // : position "https?:"
|
||||
hostStart += 3 // Skip 1 ':' and 2 '\'
|
||||
hostEnd := hostStart + bytes.IndexByte(path[hostStart:], '/')
|
||||
|
||||
newPath := make([]byte, len(path))
|
||||
copy(newPath, path)
|
||||
newPath = byteutils.Replace(newPath, 0, hostEnd, url)
|
||||
|
||||
return SetPath(payload, newPath)
|
||||
}
|
||||
|
||||
return SetHeader(payload, []byte("Host"), host)
|
||||
}
|
||||
|
||||
// Method returns HTTP method
|
||||
func Method(payload []byte) []byte {
|
||||
end := bytes.IndexByte(payload, ' ')
|
||||
|
||||
return payload[:end]
|
||||
}
|
||||
|
||||
// Status returns response status.
|
||||
// It happend to be in same position as request payload path
|
||||
func Status(payload []byte) []byte {
|
||||
return Path(payload)
|
||||
}
|
||||
|
||||
var httpMethods []string = []string{
|
||||
"GET ", "OPTI", "HEAD", "POST", "PUT ", "DELE", "TRAC", "CONN",
|
||||
}
|
||||
|
||||
func IsHTTPPayload(payload []byte) bool {
|
||||
if len(payload) < 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
method := string(payload[0:4])
|
||||
|
||||
for _, m := range httpMethods {
|
||||
if method == m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHeader(t *testing.T) {
|
||||
var payload, val []byte
|
||||
var headerStart int
|
||||
|
||||
// Value with space at start
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if val = Header(payload, []byte("Content-Length")); !bytes.Equal(val, []byte("7")) {
|
||||
t.Error("Should find header value")
|
||||
}
|
||||
|
||||
// Value without space at start
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length:7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if val = Header(payload, []byte("Content-Length")); !bytes.Equal(val, []byte("7")) {
|
||||
t.Error("Should find header value without space after :")
|
||||
}
|
||||
|
||||
// Value is empty
|
||||
payload = []byte("GET /p HTTP/1.1\r\nCookie:\r\nHost: www.w3.org\r\n\r\n")
|
||||
|
||||
if val = Header(payload, []byte("Cookie")); len(val) > 0 {
|
||||
t.Error("Should return empty value")
|
||||
}
|
||||
|
||||
// Wrong delimeter
|
||||
payload = []byte("GET /p HTTP/1.1\r\nCookie: 123\nHost: www.w3.org\r\n\r\n")
|
||||
|
||||
if val = Header(payload, []byte("Cookie")); !bytes.Equal(val, []byte("123")) {
|
||||
t.Error("Should handle wrong header delimeter")
|
||||
}
|
||||
|
||||
|
||||
// Header not found
|
||||
if _, headerStart, _, _ = header(payload, []byte("Not-Found")); headerStart != -1 {
|
||||
t.Error("Should not found header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMIMEHeadersEndPos(t *testing.T) {
|
||||
head := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org")
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
end := MIMEHeadersEndPos(payload)
|
||||
|
||||
if !bytes.Equal(payload[:end], head) {
|
||||
t.Error("Wrong headers end position:", end)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMIMEHeadersStartPos(t *testing.T) {
|
||||
headers := []byte("Content-Length: 7\r\nHost: www.w3.org")
|
||||
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
start := MIMEHeadersStartPos(payload)
|
||||
end := MIMEHeadersEndPos(payload)
|
||||
|
||||
if !bytes.Equal(payload[start:end], headers) {
|
||||
t.Error("Wrong headers end position:", start, end)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetHeader(t *testing.T) {
|
||||
var payload, payloadAfter []byte
|
||||
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post HTTP/1.1\r\nContent-Length: 14\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetHeader(payload, []byte("Content-Length"), []byte("14")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should update header if it exists", string(payload))
|
||||
}
|
||||
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post HTTP/1.1\r\nUser-Agent: Gor\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetHeader(payload, []byte("User-Agent"), []byte("Gor")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should add header if not found", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
var path, payload []byte
|
||||
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if path = Path(payload); !bytes.Equal(path, []byte("/post")) {
|
||||
t.Error("Should find path", string(path))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPath(t *testing.T) {
|
||||
var payload, payloadAfter []byte
|
||||
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /new_path HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetPath(payload, []byte("/new_path")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should replace path", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathParam(t *testing.T) {
|
||||
var payload []byte
|
||||
|
||||
payload = []byte("POST /post?param=test&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if val, _, _ := PathParam(payload, []byte("param")); !bytes.Equal(val, []byte("test")) {
|
||||
t.Error("Should detect attribute", string(val))
|
||||
}
|
||||
|
||||
if val, _, _ := PathParam(payload, []byte("user_id")); !bytes.Equal(val, []byte("1")) {
|
||||
t.Error("Should detect attribute", string(val))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPathParam(t *testing.T) {
|
||||
var payload, payloadAfter []byte
|
||||
|
||||
payload = []byte("POST /post?param=test&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post?param=new&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetPathParam(payload, []byte("param"), []byte("new")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should replace existing value", string(payload))
|
||||
}
|
||||
|
||||
payload = []byte("POST /post?param=test&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post?param=test&user_id=2 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetPathParam(payload, []byte("user_id"), []byte("2")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should replace existing value", string(payload))
|
||||
}
|
||||
|
||||
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post?param=test HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetPathParam(payload, []byte("param"), []byte("test")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should set param if url have no params", string(payload))
|
||||
}
|
||||
|
||||
payload = []byte("POST /post?param=test HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST /post?param=test&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetPathParam(payload, []byte("user_id"), []byte("1")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should set param at the end if url params", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetHostHTTP10(t *testing.T) {
|
||||
var payload, payloadAfter []byte
|
||||
|
||||
payload = []byte("POST http://example.com/post HTTP/1.0\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
payloadAfter = []byte("POST http://new.com/post HTTP/1.0\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
|
||||
if payload = SetHost(payload, []byte("http://new.com"), []byte("new.com")); !bytes.Equal(payload, payloadAfter) {
|
||||
t.Error("Should replace host", string(payload))
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
RequestPayload = '1'
|
||||
ResponsePayload = '2'
|
||||
ReplayedResponsePayload = '3'
|
||||
)
|
||||
|
||||
func uuid() []byte {
|
||||
b := make([]byte, 20)
|
||||
rand.Read(b)
|
||||
|
||||
uuid := make([]byte, 40)
|
||||
hex.Encode(uuid, b)
|
||||
|
||||
return uuid
|
||||
}
|
||||
|
||||
var payloadSeparator = "\n🐵🙈🙉\n"
|
||||
|
||||
func payloadScanner(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
if i := bytes.Index(data, []byte(payloadSeparator)); i >= 0 {
|
||||
// We have a full newline-terminated line.
|
||||
return i + len([]byte(payloadSeparator)), data[0:i], nil
|
||||
}
|
||||
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// Timing is request start or round-trip time, depending on payloadType
|
||||
func payloadHeader(payloadType byte, uuid []byte, timing int64) (header []byte) {
|
||||
sTime := strconv.FormatInt(timing, 10)
|
||||
|
||||
//Example:
|
||||
// 3 f45590522cd1838b4a0d5c5aab80b77929dea3b3 1231\n
|
||||
// `+ 1` indicates space characters or end of line
|
||||
header = make([]byte, 1+1+len(uuid)+1+len(sTime)+1)
|
||||
header[0] = payloadType
|
||||
header[1] = ' '
|
||||
header[2+len(uuid)] = ' '
|
||||
header[len(header)-1] = '\n'
|
||||
|
||||
copy(header[2:], uuid)
|
||||
copy(header[3+len(uuid):], sTime)
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
func payloadBody(payload []byte) []byte {
|
||||
headerSize := bytes.IndexByte(payload, '\n')
|
||||
return payload[headerSize+1:]
|
||||
}
|
||||
|
||||
func payloadMeta(payload []byte) [][]byte {
|
||||
headerSize := bytes.IndexByte(payload, '\n')
|
||||
if headerSize < 0 {
|
||||
headerSize = 0;
|
||||
}
|
||||
return bytes.Split(payload[:headerSize], []byte{' '})
|
||||
}
|
||||
|
||||
func isOriginPayload(payload []byte) bool {
|
||||
switch payload[0] {
|
||||
case RequestPayload, ResponsePayload:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isRequestPayload(payload []byte) bool {
|
||||
return payload[0] == RequestPayload
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
Package rawSocket provides traffic sniffier using RAW sockets.
|
||||
|
||||
Capture traffic from socket using RAW_SOCKET's
|
||||
http://en.wikipedia.org/wiki/Raw_socket
|
||||
|
||||
RAW_SOCKET allow you listen for traffic on any port (e.g. sniffing) because they operate on IP level.
|
||||
|
||||
Ports is TCP feature, same as flow control, reliable transmission and etc.
|
||||
|
||||
This package implements own TCP layer: TCP packets is parsed using tcp_packet.go, and flow control is managed by tcp_message.go
|
||||
*/
|
||||
package rawSocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"net"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Listener handle traffic capture
|
||||
type Listener struct {
|
||||
// buffer of TCPMessages waiting to be send
|
||||
messages map[string]*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
|
||||
|
||||
respAliases map[uint32]*request
|
||||
|
||||
// Messages ready to be send to client
|
||||
packetsChan chan *TCPPacket
|
||||
|
||||
// Messages ready to be send to client
|
||||
messagesChan chan *TCPMessage
|
||||
|
||||
addr string // IP to listen
|
||||
port uint16 // Port to listen
|
||||
|
||||
messageExpire time.Duration
|
||||
|
||||
captureResponse bool
|
||||
|
||||
conn net.PacketConn
|
||||
quit chan bool
|
||||
}
|
||||
|
||||
type request struct {
|
||||
start time.Time
|
||||
ack uint32
|
||||
}
|
||||
|
||||
// NewListener creates and initializes new Listener object
|
||||
func NewListener(addr string, port string, expire time.Duration, captureResponse bool) (l *Listener) {
|
||||
l = &Listener{captureResponse: captureResponse}
|
||||
|
||||
l.packetsChan = make(chan *TCPPacket, 10000)
|
||||
l.messagesChan = make(chan *TCPMessage, 10000)
|
||||
l.quit = make(chan bool)
|
||||
|
||||
l.messages = make(map[string]*TCPMessage)
|
||||
l.ackAliases = make(map[uint32]uint32)
|
||||
l.seqWithData = make(map[uint32]uint32)
|
||||
l.respAliases = make(map[uint32]*request)
|
||||
|
||||
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()
|
||||
go l.readRAWSocket()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *Listener) listen() {
|
||||
gcTicker := time.Tick(t.messageExpire / 2)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.quit:
|
||||
t.conn.Close()
|
||||
return
|
||||
// We need to use channels to process each packet to avoid data races
|
||||
case packet := <-t.packetsChan:
|
||||
t.processTCPPacket(packet)
|
||||
|
||||
case <- gcTicker:
|
||||
now := time.Now()
|
||||
|
||||
for _, message := range t.messages {
|
||||
if now.Sub(message.Start) >= t.messageExpire {
|
||||
t.dispatchMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Listener) dispatchMessage(message *TCPMessage) {
|
||||
delete(t.ackAliases, message.Ack)
|
||||
delete(t.messages, message.ID)
|
||||
|
||||
if !message.IsIncoming {
|
||||
delete(t.respAliases, message.Ack)
|
||||
|
||||
// Do not track responses which have no associated requests
|
||||
if message.RequestAck == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.messagesChan <- message
|
||||
}
|
||||
|
||||
func (t *Listener) readRAWSocket() {
|
||||
conn, e := net.ListenPacket("ip4:tcp", t.addr)
|
||||
t.conn = conn
|
||||
|
||||
if e != nil {
|
||||
log.Fatal(e)
|
||||
}
|
||||
|
||||
defer t.conn.Close()
|
||||
|
||||
buf := make([]byte, 64*1024) // 64kb
|
||||
|
||||
for {
|
||||
// Note: ReadFrom receive messages without IP header
|
||||
n, addr, err := t.conn.ReadFrom(buf)
|
||||
|
||||
if err != nil {
|
||||
if strings.HasSuffix(err.Error(), "closed network connection") {
|
||||
return
|
||||
} else {
|
||||
log.Println("Raw listener error:", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
if t.isValidPacket(buf[:n]) {
|
||||
newBuf := make([]byte, n)
|
||||
copy(newBuf, buf[:n])
|
||||
|
||||
go func(newBuf []byte) {
|
||||
t.packetsChan <- ParseTCPPacket(addr, newBuf)
|
||||
}(newBuf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Listener) isValidPacket(buf []byte) bool {
|
||||
// To avoid full packet parsing every time, we manually parsing values needed for packet filtering
|
||||
// http://en.wikipedia.org/wiki/Transmission_Control_Protocol
|
||||
destPort := binary.BigEndian.Uint16(buf[2:4])
|
||||
srcPort := binary.BigEndian.Uint16(buf[0:2])
|
||||
|
||||
// Because RAW_SOCKET can't be bound to port, we have to control it by ourself
|
||||
if destPort == t.port || (t.captureResponse && srcPort == t.port) {
|
||||
// Get the 'data offset' (size of the TCP header in 32-bit words)
|
||||
dataOffset := (buf[12] & 0xF0) >> 4
|
||||
|
||||
// We need only packets with data inside
|
||||
// Check that the buffer is larger than the size of the TCP header
|
||||
if len(buf) > int(dataOffset*4) {
|
||||
// We should create new buffer because go slices is pointers. So buffer data shoud be immutable.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var bExpect100ContinueCheck = []byte("Expect: 100-continue")
|
||||
var bPOST = []byte("POST")
|
||||
|
||||
// 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 message *TCPMessage
|
||||
|
||||
isIncoming := packet.DestPort == t.port
|
||||
|
||||
if parentAck, ok := t.seqWithData[packet.Seq]; ok {
|
||||
t.ackAliases[packet.Ack] = parentAck
|
||||
delete(t.seqWithData, packet.Seq)
|
||||
}
|
||||
|
||||
if alias, ok := t.ackAliases[packet.Ack]; ok {
|
||||
packet.Ack = alias
|
||||
}
|
||||
|
||||
var responseRequest *request
|
||||
|
||||
if t.captureResponse && !isIncoming {
|
||||
responseRequest, _ = t.respAliases[packet.Ack]
|
||||
}
|
||||
|
||||
mID := packet.Addr.String() + strconv.Itoa(int(packet.DestPort)) + strconv.Itoa(int(packet.Ack))
|
||||
|
||||
message, ok := t.messages[mID]
|
||||
|
||||
if !ok {
|
||||
message = NewTCPMessage(mID, packet.Ack, isIncoming)
|
||||
t.messages[mID] = message
|
||||
|
||||
if !isIncoming && responseRequest != nil {
|
||||
message.RequestStart = responseRequest.start
|
||||
message.RequestAck = responseRequest.ack
|
||||
}
|
||||
}
|
||||
|
||||
// Handling Expect: 100-continue requests
|
||||
if len(packet.Data) > 4 && bytes.Equal(packet.Data[0:4], bPOST) {
|
||||
// reading last 20 bytes (not counting CRLF): last header value (if no body presented)
|
||||
if bytes.Equal(packet.Data[len(packet.Data)-24:len(packet.Data)-4], bExpect100ContinueCheck) {
|
||||
t.seqWithData[packet.Seq+uint32(len(packet.Data))] = packet.Ack
|
||||
|
||||
// Removing `Expect: 100-continue` header
|
||||
packet.Data = append(packet.Data[:len(packet.Data)-24], packet.Data[len(packet.Data)-2:]...)
|
||||
}
|
||||
}
|
||||
|
||||
if t.captureResponse && isIncoming {
|
||||
// If message have multiple packets, delete previous alias
|
||||
if len(message.packets) > 0 {
|
||||
delete(t.respAliases, message.ResponseAck)
|
||||
}
|
||||
|
||||
responseAck := packet.Seq + uint32(len(packet.Data))
|
||||
t.respAliases[responseAck] = &request{message.Start, message.Ack}
|
||||
message.ResponseAck = responseAck
|
||||
}
|
||||
|
||||
// Adding packet to message
|
||||
message.AddPacket(packet)
|
||||
|
||||
// If message contains only single packet immediately dispatch it
|
||||
if !message.IsMultipart() {
|
||||
t.dispatchMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
// Receive TCP messages from the listener channel
|
||||
func (t *Listener) Receive() *TCPMessage {
|
||||
return <-t.messagesChan
|
||||
}
|
||||
|
||||
func (t *Listener) Close() {
|
||||
close(t.quit)
|
||||
t.conn.Close()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package rawSocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"github.com/buger/gor/proto"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TCPMessage ensure that all TCP packets for given request is received, and processed in right sequence
|
||||
// Its needed because all TCP message can be fragmented or re-transmitted
|
||||
//
|
||||
// Each TCP Packet have 2 ids: 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 {
|
||||
ID string // Message ID
|
||||
Ack uint32
|
||||
ResponseAck uint32
|
||||
RequestStart time.Time
|
||||
RequestAck uint32
|
||||
Start time.Time
|
||||
End time.Time
|
||||
IsIncoming bool
|
||||
|
||||
packets []*TCPPacket
|
||||
|
||||
delChan chan *TCPMessage
|
||||
}
|
||||
|
||||
// NewTCPMessage pointer created from a Acknowledgment number and a channel of messages readuy to be deleted
|
||||
func NewTCPMessage(ID string, Ack uint32, IsIncoming bool) (msg *TCPMessage) {
|
||||
msg = &TCPMessage{ID: ID, Ack: Ack, IsIncoming: IsIncoming}
|
||||
msg.Start = time.Now()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Bytes return message content
|
||||
func (t *TCPMessage) Bytes() (output []byte) {
|
||||
for _, p := range t.packets {
|
||||
output = append(output, p.Data...)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// Size returns total size of message
|
||||
func (t *TCPMessage) Size() (size int) {
|
||||
size += len(proto.Body(t.packets[0].Data))
|
||||
|
||||
for _, p := range t.packets[1:] {
|
||||
size += len(p.Data)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AddPacket to the message and ensure packet uniqueness
|
||||
// TCP allows that packet can be re-send multiple times
|
||||
func (t *TCPMessage) AddPacket(packet *TCPPacket) {
|
||||
packetFound := false
|
||||
|
||||
for _, pkt := range t.packets {
|
||||
if packet.Seq == pkt.Seq {
|
||||
packetFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if packetFound {
|
||||
log.Println("Received packet with same sequence")
|
||||
} else {
|
||||
// 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 {
|
||||
t.packets = append([]*TCPPacket{packet}, t.packets...)
|
||||
}
|
||||
|
||||
t.End = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// isMultipart returns true if message contains from multiple tcp packets
|
||||
func (t *TCPMessage) IsMultipart() bool {
|
||||
if len(t.packets) > 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
payload := t.packets[0].Data
|
||||
|
||||
if len(payload) < 4 {
|
||||
return true
|
||||
}
|
||||
|
||||
m := payload[:4]
|
||||
|
||||
if t.IsIncoming {
|
||||
// If one GET, OPTIONS, or HEAD request
|
||||
if bytes.Equal(m, []byte("GET ")) || bytes.Equal(m, []byte("OPTI")) || bytes.Equal(m, []byte("HEAD")) {
|
||||
return false
|
||||
} else {
|
||||
// Sometimes header comes after the body :(
|
||||
if bytes.Equal(m, []byte("POST")) || bytes.Equal(m, []byte("PUT ")) || bytes.Equal(m, []byte("PATC")) {
|
||||
if length := proto.Header(payload, []byte("Content-Length")); len(length) > 0 {
|
||||
l, _ := strconv.Atoi(string(length))
|
||||
|
||||
// If content-length equal current body length
|
||||
if l > 0 && l == t.Size() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if length := proto.Header(payload, []byte("Content-Length")); len(length) > 0 {
|
||||
if length[0] == '0' {
|
||||
return false
|
||||
}
|
||||
|
||||
l, _ := strconv.Atoi(string(length))
|
||||
|
||||
// If content-length equal current body length
|
||||
if l > 0 && l == t.Size() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *TCPMessage) UUID() []byte {
|
||||
var key []byte
|
||||
|
||||
if t.IsIncoming {
|
||||
key = strconv.AppendInt(key, t.Start.UnixNano(), 10)
|
||||
key = strconv.AppendUint(key, uint64(t.Ack), 10)
|
||||
} else {
|
||||
key = strconv.AppendInt(key, t.RequestStart.UnixNano(), 10)
|
||||
key = strconv.AppendUint(key, uint64(t.RequestAck), 10)
|
||||
}
|
||||
|
||||
uuid := make([]byte, 40)
|
||||
sha := sha1.Sum(key)
|
||||
hex.Encode(uuid, sha[:20])
|
||||
|
||||
return uuid
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package rawSocket
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TCP Flags
|
||||
const (
|
||||
fFIN = 1 << iota
|
||||
fSYN
|
||||
fRST
|
||||
fPSH
|
||||
fACK
|
||||
fURG
|
||||
fECE
|
||||
fCWR
|
||||
fNS
|
||||
)
|
||||
|
||||
// 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
|
||||
DataOffset uint8
|
||||
Flags uint16
|
||||
Window uint16
|
||||
Checksum uint16
|
||||
Urgent uint16
|
||||
|
||||
Data []byte
|
||||
|
||||
Addr net.Addr
|
||||
}
|
||||
|
||||
// ParseTCPPacket takes address and tcp payload and returns parsed TCPPacket
|
||||
func ParseTCPPacket(addr net.Addr, b []byte) (p *TCPPacket) {
|
||||
p = &TCPPacket{Data: b}
|
||||
p.ParseBasic()
|
||||
p.Addr = addr
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Parse TCP Packet, inspired by: https://github.com/miekg/pcap/blob/master/packet.go
|
||||
func (t *TCPPacket) Parse() {
|
||||
t.ParseBasic()
|
||||
t.Flags = binary.BigEndian.Uint16(t.Data[12:14]) & 0x1FF
|
||||
t.Window = binary.BigEndian.Uint16(t.Data[14:16])
|
||||
t.Checksum = binary.BigEndian.Uint16(t.Data[16:18])
|
||||
t.Urgent = binary.BigEndian.Uint16(t.Data[18:20])
|
||||
}
|
||||
|
||||
// ParseBasic set of fields
|
||||
func (t *TCPPacket) ParseBasic() {
|
||||
t.DestPort = binary.BigEndian.Uint16(t.Data[2:4])
|
||||
t.SrcPort = binary.BigEndian.Uint16(t.Data[0:2])
|
||||
t.Seq = binary.BigEndian.Uint32(t.Data[4:8])
|
||||
t.Ack = binary.BigEndian.Uint32(t.Data[8:12])
|
||||
t.DataOffset = (t.Data[12] & 0xF0) >> 4
|
||||
|
||||
t.Data = t.Data[t.DataOffset*4:]
|
||||
}
|
||||
|
||||
// String output for a TCP Packet
|
||||
func (t *TCPPacket) String() string {
|
||||
maxLen := len(t.Data)
|
||||
if maxLen > 500 {
|
||||
maxLen = 500
|
||||
}
|
||||
|
||||
return strings.Join([]string{
|
||||
"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)),
|
||||
|
||||
"Flag ns:" + strconv.FormatBool(t.Flags&fNS != 0),
|
||||
"Flag crw:" + strconv.FormatBool(t.Flags&fCWR != 0),
|
||||
"Flag ece:" + strconv.FormatBool(t.Flags&fECE != 0),
|
||||
"Flag urg:" + strconv.FormatBool(t.Flags&fURG != 0),
|
||||
"Flag ack:" + strconv.FormatBool(t.Flags&fACK != 0),
|
||||
"Flag psh:" + strconv.FormatBool(t.Flags&fPSH != 0),
|
||||
"Flag rst:" + strconv.FormatBool(t.Flags&fRST != 0),
|
||||
"Flag syn:" + strconv.FormatBool(t.Flags&fSYN != 0),
|
||||
"Flag fin:" + strconv.FormatBool(t.Flags&fFIN != 0),
|
||||
|
||||
"Window size:" + strconv.Itoa(int(t.Window)),
|
||||
"Checksum:" + strconv.Itoa(int(t.Checksum)),
|
||||
|
||||
"Data size:" + strconv.Itoa(len(t.Data)),
|
||||
"Data:" + string(t.Data[:maxLen]),
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
type sortBySeq []*TCPPacket
|
||||
|
||||
func (a sortBySeq) Len() int { return len(a) }
|
||||
func (a sortBySeq) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sortBySeq) Less(i, j int) bool { return a[i].Seq < a[j].Seq }
|
||||
@@ -1,120 +0,0 @@
|
||||
// Replay server receive requests objects from Listeners and forward it to given address.
|
||||
// Basic usage:
|
||||
//
|
||||
// gor replay -f http://staging.server
|
||||
//
|
||||
//
|
||||
// Rate limiting
|
||||
//
|
||||
// It can be useful if you want forward only part of production traffic, not to overload staging environment. You can specify desired request per second using "|" operator after server address:
|
||||
//
|
||||
// # staging.server not get more than 10 requests per second
|
||||
// gor replay -f "http://staging.server|10"
|
||||
//
|
||||
//
|
||||
// Forward to multiple addresses
|
||||
//
|
||||
// Just separate addresses by coma:
|
||||
// gor replay -f "http://staging.server|10,http://dev.server|20"
|
||||
//
|
||||
//
|
||||
// For more help run:
|
||||
//
|
||||
// gor replay -h
|
||||
//
|
||||
package replay
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const bufSize = 4096
|
||||
|
||||
// Enable debug logging only if "--verbose" flag passed
|
||||
func Debug(v ...interface{}) {
|
||||
if Settings.Verbose {
|
||||
log.Println(v...)
|
||||
}
|
||||
}
|
||||
|
||||
func ParseRequest(data []byte) (request *http.Request, err error) {
|
||||
buf := bytes.NewBuffer(data)
|
||||
reader := bufio.NewReader(buf)
|
||||
|
||||
request, err = http.ReadRequest(reader)
|
||||
return
|
||||
}
|
||||
|
||||
// Because its sub-program, Run acts as `main`
|
||||
// Replay server listen to UDP traffic from Listeners
|
||||
// Each request processed by RequestFactory
|
||||
func Run() {
|
||||
listener, err := net.Listen("tcp", Settings.Address())
|
||||
|
||||
log.Println("Starting replay server at:", Settings.Address())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Can't start:", err)
|
||||
}
|
||||
|
||||
for _, host := range Settings.ForwardedHosts() {
|
||||
log.Println("Forwarding requests to:", host.Url, "limit:", host.Limit)
|
||||
}
|
||||
|
||||
requestFactory := NewRequestFactory()
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
|
||||
if err != nil {
|
||||
log.Println("Error while Accept()", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go handleConnection(conn, requestFactory)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn, rf *RequestFactory) error {
|
||||
defer conn.Close()
|
||||
|
||||
var read = true
|
||||
var response []byte
|
||||
var buf []byte
|
||||
|
||||
buf = make([]byte, bufSize)
|
||||
|
||||
for read {
|
||||
n, err := conn.Read(buf)
|
||||
|
||||
switch err {
|
||||
case io.EOF:
|
||||
read = false
|
||||
case nil:
|
||||
response = append(response, buf[0:n]...)
|
||||
if n < bufSize {
|
||||
read = false
|
||||
}
|
||||
default:
|
||||
read = false
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
if request, err := ParseRequest(response); err != nil {
|
||||
Debug("Error while parsing request", err, response)
|
||||
} else {
|
||||
Debug("Adding request", request)
|
||||
|
||||
rf.Add(request)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package replay
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type HttpResponse struct {
|
||||
host *ForwardHost
|
||||
req *http.Request
|
||||
resp *http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
// Class for processing requests
|
||||
//
|
||||
// Basic workflow:
|
||||
//
|
||||
// 1. When request added via Add() it get pushed to `responses` chan
|
||||
// 2. handleRequest() listen for `responses` chan and decide where request should be forwarded, and apply rate-limit if needed
|
||||
// 3. sendRequest() forwards request and returns response info to `responses` chan
|
||||
// 4. handleRequest() listen for `response` channel and updates stats
|
||||
type RequestFactory struct {
|
||||
c_responses chan *HttpResponse
|
||||
c_requests chan *http.Request
|
||||
}
|
||||
|
||||
// RequestFactory contstuctor
|
||||
// One created, it starts listening for incoming requests: requests channel
|
||||
func NewRequestFactory() (factory *RequestFactory) {
|
||||
factory = &RequestFactory{}
|
||||
factory.c_responses = make(chan *HttpResponse)
|
||||
factory.c_requests = make(chan *http.Request)
|
||||
|
||||
go factory.handleRequests()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Disable redirects https://github.com/buger/gor/pull/15
|
||||
func customCheckRedirect(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 0 {
|
||||
return errors.New("stopped after 2 redirects")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Forward http request to given host
|
||||
func (f *RequestFactory) sendRequest(host *ForwardHost, request *http.Request) {
|
||||
client := &http.Client{
|
||||
CheckRedirect: customCheckRedirect,
|
||||
}
|
||||
|
||||
// Change HOST of original request
|
||||
URL := host.Url + request.URL.Path + "?" + request.URL.RawQuery
|
||||
|
||||
request.RequestURI = ""
|
||||
request.URL, _ = url.ParseRequestURI(URL)
|
||||
|
||||
Debug("Sending request:", host.Url, request)
|
||||
|
||||
resp, err := client.Do(request)
|
||||
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
} else {
|
||||
Debug("Request error:", err)
|
||||
}
|
||||
|
||||
f.c_responses <- &HttpResponse{host, request, resp, err}
|
||||
}
|
||||
|
||||
// Handle incoming requests, and they responses
|
||||
func (f *RequestFactory) handleRequests() {
|
||||
hosts := Settings.ForwardedHosts()
|
||||
|
||||
for {
|
||||
select {
|
||||
case req := <-f.c_requests:
|
||||
for _, host := range hosts {
|
||||
// Ensure that we have actual stats for given timestamp
|
||||
host.Stat.Touch()
|
||||
|
||||
if host.Limit == 0 || host.Stat.Count < host.Limit {
|
||||
// Increment Stat.Count
|
||||
host.Stat.IncReq()
|
||||
|
||||
go f.sendRequest(host, req)
|
||||
}
|
||||
}
|
||||
case resp := <-f.c_responses:
|
||||
// Increment returned http code stats, and elapsed time
|
||||
resp.host.Stat.IncResp(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add request to channel for further processing
|
||||
func (f *RequestFactory) Add(request *http.Request) {
|
||||
f.c_requests <- request
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package replay
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Stats stores in context of current timestamp
|
||||
type RequestStat struct {
|
||||
timestamp int64
|
||||
|
||||
Codes map[int]int // { 200: 10, 404:2, 500:1 }
|
||||
|
||||
Count int // All requests including errors
|
||||
Errors int // Rquests with errors (timeout or host not reachable). Not include 50x errors.
|
||||
|
||||
host *ForwardHost
|
||||
}
|
||||
|
||||
// Ensure that current stats is actual (for current timestamp)
|
||||
func (s *RequestStat) Touch() {
|
||||
if s.timestamp != time.Now().Unix() {
|
||||
s.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Called on request start
|
||||
func (s *RequestStat) IncReq() {
|
||||
s.Count++
|
||||
}
|
||||
|
||||
// Called after response
|
||||
func (s *RequestStat) IncResp(resp *HttpResponse) {
|
||||
s.Touch()
|
||||
|
||||
if resp.err != nil {
|
||||
s.Errors++
|
||||
return
|
||||
}
|
||||
|
||||
s.Codes[resp.resp.StatusCode]++
|
||||
}
|
||||
|
||||
// Updated stats timestamp to current time and reset to zero all stats values
|
||||
// TODO: Further on reset it should write stats to file
|
||||
func (s *RequestStat) reset() {
|
||||
if s.timestamp != 0 {
|
||||
Debug("Host:", s.host.Url, "Requests:", s.Count, "Errors:", s.Errors, "Status codes:", s.Codes)
|
||||
}
|
||||
|
||||
s.timestamp = time.Now().Unix()
|
||||
|
||||
s.Codes = make(map[int]int)
|
||||
s.Count = 0
|
||||
s.Errors = 0
|
||||
}
|
||||
|
||||
// RequestStat constructor
|
||||
func NewRequestStats(host *ForwardHost) (stat *RequestStat) {
|
||||
stat = &RequestStat{host: host}
|
||||
stat.reset()
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package replay
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ForwardHost struct {
|
||||
Url string
|
||||
Limit int
|
||||
|
||||
Stat *RequestStat
|
||||
}
|
||||
|
||||
type ReplaySettings struct {
|
||||
Port int
|
||||
Host string
|
||||
|
||||
ForwardAddress string
|
||||
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
// ForwardedHosts implements forwardAddress syntax support for multiple hosts (coma separated), and rate limiting by specifing "|maxRps" after host name.
|
||||
//
|
||||
// -f "host1,http://host2|10,host3"
|
||||
//
|
||||
func (r *ReplaySettings) ForwardedHosts() (hosts []*ForwardHost) {
|
||||
hosts = make([]*ForwardHost, 0, 10)
|
||||
|
||||
for _, address := range strings.Split(r.ForwardAddress, ",") {
|
||||
host_info := strings.Split(address, "|")
|
||||
|
||||
if strings.Index(host_info[0], "http") == -1 {
|
||||
host_info[0] = "http://" + host_info[0]
|
||||
}
|
||||
|
||||
host := &ForwardHost{Url: host_info[0]}
|
||||
host.Stat = NewRequestStats(host)
|
||||
|
||||
if len(host_info) > 1 {
|
||||
host.Limit, _ = strconv.Atoi(host_info[1])
|
||||
}
|
||||
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Helper to return address with port, e.g.: 127.0.0.1:28020
|
||||
func (r *ReplaySettings) Address() string {
|
||||
return r.Host + ":" + strconv.Itoa(r.Port)
|
||||
}
|
||||
|
||||
var Settings ReplaySettings = ReplaySettings{}
|
||||
|
||||
func init() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "replay" {
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
defaultPort = 28020
|
||||
defaultHost = "0.0.0.0"
|
||||
|
||||
defaultAddress = "http://localhost:8080"
|
||||
)
|
||||
|
||||
flag.IntVar(&Settings.Port, "p", defaultPort, "specify port number")
|
||||
|
||||
flag.StringVar(&Settings.Host, "ip", defaultHost, "ip addresses to listen on")
|
||||
|
||||
flag.StringVar(&Settings.ForwardAddress, "f", defaultAddress, "http address to forward traffic.\n\tYou can limit requests per second by adding `|num` after address.\n\tIf you have multiple addresses with different limits. For example: http://staging.example.com|100,http://dev.example.com|10")
|
||||
|
||||
flag.BoolVar(&Settings.Verbose, "verbose", false, "Log requests")
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var VERSION string
|
||||
|
||||
// MultiOption allows to specify multiple flags with same name and collects all values into array
|
||||
type MultiOption []string
|
||||
|
||||
func (h *MultiOption) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
}
|
||||
|
||||
// Set gets called multiple times for each flag with same name
|
||||
func (h *MultiOption) Set(value string) error {
|
||||
*h = append(*h, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AppSettings is the struct of main configuration
|
||||
type AppSettings struct {
|
||||
verbose bool
|
||||
debug bool
|
||||
stats bool
|
||||
|
||||
splitOutput bool
|
||||
|
||||
inputDummy MultiOption
|
||||
outputDummy MultiOption
|
||||
|
||||
inputTCP MultiOption
|
||||
outputTCP MultiOption
|
||||
outputTCPStats bool
|
||||
|
||||
inputFile MultiOption
|
||||
outputFile MultiOption
|
||||
|
||||
inputRAW MultiOption
|
||||
|
||||
middleware string
|
||||
|
||||
inputHTTP MultiOption
|
||||
outputHTTP MultiOption
|
||||
|
||||
outputHTTPConfig HTTPOutputConfig
|
||||
modifierConfig HTTPModifierConfig
|
||||
}
|
||||
|
||||
// Settings holds Gor configuration
|
||||
var Settings AppSettings
|
||||
|
||||
func usage() {
|
||||
fmt.Printf("Gor is a simple http traffic replication tool written in Go. Its main goal is to replay traffic from production servers to staging and dev environments.\nProject page: https://github.com/buger/gor\nAuthor: <Leonid Bugaev> leonsbox@gmail.com\nCurrent Version: %s\n\n", VERSION)
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.Usage = usage
|
||||
|
||||
flag.BoolVar(&Settings.verbose, "verbose", false, "Turn on more verbose output")
|
||||
flag.BoolVar(&Settings.debug, "debug", false, "Turn on debug output, shows all intercepted traffic. Works only when with `verbose` flag")
|
||||
flag.BoolVar(&Settings.stats, "stats", false, "Turn on queue stats output")
|
||||
|
||||
flag.BoolVar(&Settings.splitOutput, "split-output", false, "By default each output gets same traffic. If set to `true` it splits traffic equally among all outputs.")
|
||||
|
||||
flag.Var(&Settings.inputDummy, "input-dummy", "Used for testing outputs. Emits 'Get /' request every 1s")
|
||||
flag.Var(&Settings.outputDummy, "output-dummy", "Used for testing inputs. Just prints data coming from inputs.")
|
||||
|
||||
flag.Var(&Settings.inputTCP, "input-tcp", "Used for internal communication between Gor instances. Example: \n\t# Receive requests from other Gor instances on 28020 port, and redirect output to staging\n\tgor --input-tcp :28020 --output-http staging.com")
|
||||
flag.Var(&Settings.outputTCP, "output-tcp", "Used for internal communication between Gor instances. Example: \n\t# Listen for requests on 80 port and forward them to other Gor instance on 28020 port\n\tgor --input-raw :80 --output-tcp replay.local:28020")
|
||||
flag.BoolVar(&Settings.outputTCPStats, "output-tcp-stats", false, "Report TCP output queue stats to console every 5 seconds.")
|
||||
|
||||
flag.Var(&Settings.inputFile, "input-file", "Read requests from file: \n\tgor --input-file ./requests.gor --output-http staging.com")
|
||||
flag.Var(&Settings.outputFile, "output-file", "Write incoming requests to file: \n\tgor --input-raw :80 --output-file ./requests.gor")
|
||||
|
||||
flag.Var(&Settings.inputRAW, "input-raw", "Capture traffic from given port (use RAW sockets and require *sudo* access):\n\t# Capture traffic from 8080 port\n\tgor --input-raw :8080 --output-http staging.com")
|
||||
|
||||
flag.StringVar(&Settings.middleware, "middleware", "", "Used for modifying traffic using external command")
|
||||
|
||||
flag.Var(&Settings.inputHTTP, "input-http", "Read requests from HTTP, should be explicitly sent from your application:\n\t# Listen for http on 9000\n\tgor --input-http :9000 --output-http staging.com")
|
||||
|
||||
flag.Var(&Settings.outputHTTP, "output-http", "Forwards incoming requests to given http address.\n\t# Redirect all incoming requests to staging.com address \n\tgor --input-raw :80 --output-http http://staging.com")
|
||||
flag.IntVar(&Settings.outputHTTPConfig.workers, "output-http-workers", 0, "Gor uses dynamic worker scaling by default. Enter a number to run a set number of workers.")
|
||||
flag.IntVar(&Settings.outputHTTPConfig.redirectLimit, "output-http-redirects", 0, "Enable how often redirects should be followed.")
|
||||
flag.DurationVar(&Settings.outputHTTPConfig.Timeout, "output-http-timeout", 0, "Specify HTTP request/response timeout. By default 5s. Example: --output-http-timeout 30s")
|
||||
|
||||
flag.BoolVar(&Settings.outputHTTPConfig.stats, "output-http-stats", false, "Report http output queue stats to console every 5 seconds.")
|
||||
flag.BoolVar(&Settings.outputHTTPConfig.OriginalHost, "http-original-host", false, "Normally gor replaces the Host http header with the host supplied with --output-http. This option disables that behavior, preserving the original Host header.")
|
||||
|
||||
flag.StringVar(&Settings.outputHTTPConfig.elasticSearch, "output-http-elasticsearch", "", "Send request and response stats to ElasticSearch:\n\tgor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.headers, "http-set-header", "Inject additional headers to http reqest:\n\tgor --input-raw :8080 --output-http staging.com --http-set-header 'User-Agent: Gor'")
|
||||
flag.Var(&Settings.modifierConfig.headers, "output-http-header", "WARNING: `--output-http-header` DEPRECATED, use `--http-set-header` instead")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.params, "http-set-param", "Set request url param, if param already exists it will be overwritten:\n\tgor --input-raw :8080 --output-http staging.com --http-set-param api_key=1")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.methods, "http-allow-method", "Whitelist of HTTP methods to replay. Anything else will be dropped:\n\tgor --input-raw :8080 --output-http staging.com --http-allow-method GET --http-allow-method OPTIONS")
|
||||
flag.Var(&Settings.modifierConfig.methods, "output-http-method", "WARNING: `--output-http-method` DEPRECATED, use `--http-allow-method` instead")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.urlRegexp, "http-allow-url", "A regexp to match requests against. Filter get matched against full url with domain. Anything else will be dropped:\n\t gor --input-raw :8080 --output-http staging.com --http-allow-url ^www.")
|
||||
flag.Var(&Settings.modifierConfig.urlRegexp, "output-http-url-regexp", "WARNING: `--output-http-url-regexp` DEPRECATED, use `--http-allow-url` instead")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.urlNegativeRegexp, "http-disallow-url", "A regexp to match requests against. Filter get matched against full url with domain. Anything else will be forwarded:\n\t gor --input-raw :8080 --output-http staging.com --http-disallow-url ^www.")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.urlRewrite, "http-rewrite-url", "Rewrite the request url based on a mapping:\n\tgor --input-raw :8080 --output-http staging.com --http-rewrite-url /v1/user/([^\\/]+)/ping:/v2/user/$1/ping")
|
||||
flag.Var(&Settings.modifierConfig.urlRewrite, "output-http-rewrite-url", "WARNING: `--output-http-rewrite-url` DEPRECATED, use `--http-rewrite-url` instead")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.headerFilters, "http-allow-header", "A regexp to match a specific header against. Requests with non-matching headers will be dropped:\n\t gor --input-raw :8080 --output-http staging.com --http-allow-header api-version:^v1")
|
||||
flag.Var(&Settings.modifierConfig.headerFilters, "output-http-header-filter", "WARNING: `--output-http-header-filter` DEPRECATED, use `--http-allow-header` instead")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.headerFilters, "http-disallow-header", "A regexp to match a specific header against. Requests with matching headers will be dropped:\n\t gor --input-raw :8080 --output-http staging.com --http-disallow-header \"User-Agent: Replayed by Gor\"")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.headerHashFilters, "http-header-limiter", "Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific header:\n\t gor --input-raw :8080 --output-http staging.com --http-header-imiter user-id:25%")
|
||||
flag.Var(&Settings.modifierConfig.headerHashFilters, "output-http-header-hash-filter", "WARNING: `output-http-header-hash-filter` DEPRECATED, use `--http-header-hash-limiter` instead")
|
||||
|
||||
flag.Var(&Settings.modifierConfig.paramHashFilters, "http-param-limiter", "Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific GET param:\n\t gor --input-raw :8080 --output-http staging.com --http-param-limiter user_id:25%")
|
||||
}
|
||||
|
||||
var previousDebugTime int64
|
||||
var debugMutex sync.Mutex
|
||||
|
||||
// Debug gets called only if --verbose flag specified
|
||||
func Debug(args ...interface{}) {
|
||||
if Settings.verbose {
|
||||
debugMutex.Lock()
|
||||
now := time.Now()
|
||||
diff := float64(now.UnixNano()-previousDebugTime) / 1000000
|
||||
previousDebugTime = now.UnixNano()
|
||||
debugMutex.Unlock()
|
||||
|
||||
fmt.Printf("[DEBUG][PID %d][%d][%fms] ", os.Getpid(), now.UnixNano(), diff)
|
||||
fmt.Println(args...)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestInput used for testing purpose, it allows emitting requests on demand
|
||||
type TestInput struct {
|
||||
data chan []byte
|
||||
}
|
||||
|
||||
// NewTestInput constructor for TestInput
|
||||
func NewTestInput() (i *TestInput) {
|
||||
i = new(TestInput)
|
||||
i.data = make(chan []byte, 100)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *TestInput) Read(data []byte) (int, error) {
|
||||
buf := <-i.data
|
||||
|
||||
header := payloadHeader(RequestPayload, uuid(), time.Now().UnixNano())
|
||||
copy(data[0:len(header)], header)
|
||||
copy(data[len(header):], buf)
|
||||
|
||||
return len(buf) + len(header), nil
|
||||
}
|
||||
|
||||
// EmitGET emits GET request without headers
|
||||
func (i *TestInput) EmitGET() {
|
||||
i.data <- []byte("GET / HTTP/1.1\r\n\r\n")
|
||||
}
|
||||
|
||||
// EmitPOST emits POST request with Content-Length
|
||||
func (i *TestInput) EmitPOST() {
|
||||
i.data <- []byte("POST /pub/WWW/ HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
|
||||
}
|
||||
|
||||
// EmitChunkedPOST emits POST request with `Transfer-Encoding: chunked` and chunked body
|
||||
func (i *TestInput) EmitChunkedPOST() {
|
||||
i.data <- []byte("POST /pub/WWW/ HTTP/1.1\r\nHost: www.w3.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
|
||||
}
|
||||
|
||||
// EmitLargePOST emits POST request with large payload (5mb)
|
||||
func (i *TestInput) EmitLargePOST() {
|
||||
size := 5 * 1024 * 1024 // 5 MB
|
||||
rb := make([]byte, size)
|
||||
rand.Read(rb)
|
||||
|
||||
rs := base64.URLEncoding.EncodeToString(rb)
|
||||
|
||||
i.data <- []byte("POST / HTTP/1.1\nHost: www.w3.org\nContent-Length:5242880\r\n\r\n" + rs)
|
||||
}
|
||||
|
||||
// EmitOPTIONS emits OPTIONS request, similar to GET
|
||||
func (i *TestInput) EmitOPTIONS() {
|
||||
i.data <- []byte("OPTIONS / HTTP/1.1\nHost: www.w3.org\r\n\r\n")
|
||||
}
|
||||
|
||||
func (i *TestInput) String() string {
|
||||
return "Test Input"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
type writeCallback func(data []byte)
|
||||
|
||||
// TestOutput used in testing to intercept any output into callback
|
||||
type TestOutput struct {
|
||||
cb writeCallback
|
||||
}
|
||||
|
||||
// NewTestOutput constructor for TestOutput, accepts callback which get called on each incoming Write
|
||||
func NewTestOutput(cb writeCallback) (i *TestOutput) {
|
||||
i = new(TestOutput)
|
||||
i.cb = cb
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *TestOutput) Write(data []byte) (int, error) {
|
||||
i.cb(data)
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (i *TestOutput) String() string {
|
||||
return "Test Input"
|
||||
}
|
||||
Reference in New Issue
Block a user