Compare commits

...
115 Commits
Author SHA1 Message Date
Leonid Bugaev de6c41c731 Update docs for Host header 2015-08-06 09:34:51 +03:00
Leonid Bugaev 08933e148a Merge pull request #188 from SaraRands/master
Added --http-original-host command line flag
2015-08-06 09:24:50 +03:00
SaraRands 259b70a7df Added --http-original-host flag 2015-08-05 14:05:59 -06:00
Leonid Bugaev 9185453453 Merge pull request #176 from dt/dt.hexdecode
Avoid trailing null on hex encoded TCP
2015-08-03 18:38:53 +03:00
Leonid Bugaev 87ad851ad8 Merge pull request #185 from buger/fix-large-response
Fix large responses
2015-08-03 18:36:09 +03:00
Leonid Bugaev ce193d0d4f Fix travis config 2015-08-03 18:33:36 +03:00
Leonid Bugaev f92a0b1242 Make buffer size configurable 2015-08-03 18:31:09 +03:00
Leonid Bugaev f6864d05a0 More debugging 2015-08-03 18:20:59 +03:00
Leonid Bugaev 3316447bf5 Verbose output for travis 2015-08-03 18:13:36 +03:00
Leonid Bugaev 961d5a754c Style fixes 2015-08-03 18:08:35 +03:00
Leonid Bugaev dd68d07c75 Fix large responses 2015-08-03 18:01:46 +03:00
David Taylor 5c46db9220 avoid trailing null in encoded tcp
Fixes #175.
2015-07-31 21:13:46 +00:00
David Taylor ed7c5210be Print warning if hex decoding fails 2015-07-31 21:09:24 +00:00
Leonid Bugaev 00ad6bd92b Merge pull request #183 from buger/configurable-timeout
Option to configure timeout
2015-07-31 22:42:19 +03:00
Leonid Bugaev 587b9ad2d5 Update Readme 2015-07-31 22:41:32 +03:00
Leonid Bugaev 083c97a9c4 Option to configure timeout 2015-07-31 22:39:01 +03:00
Leonid Bugaev 8f2e9bd1dc Merge pull request #182 from buger/fix-set-header
Do not override http Host header  if it explicitly set via options
2015-07-31 21:42:41 +03:00
Leonid Bugaev a79e54c5f1 Do not set http header if it explicitly set 2015-07-31 21:41:06 +03:00
Leonid Bugaev 27f83a6c0c Improve debugging 2015-07-31 21:08:35 +03:00
Leonid Bugaev 5158f75dc8 Merge pull request #178 from jakubholynet/patch-1
Readme: Explain when `--stats` needs to be included
2015-07-30 15:09:32 +03:00
Jakub Holy 126957c599 Readme: Explain when --stats needs to be included
It isn't intuitive that when I specify e.g. `--output-http-stats`, it isn't enough and I need to explicitly enable also `--stats` (and that `--stats` on its own does not do anything, you need other flags to specify what to output)
2015-07-30 10:15:56 +02:00
Leonid Bugaev 0a30582131 Fix tests by adding small delay 2015-07-17 12:57:36 +05:00
Leonid Bugaev 5c21600bf9 Fix timeout 2015-07-17 12:12:04 +05:00
Leonid Bugaev 107982394e Run all tests 2015-07-17 11:47:41 +05:00
Leonid Bugaev ac3e209787 One more fix 2015-07-17 11:38:42 +05:00
Leonid Bugaev e5d9f73937 Fix tests 2015-07-17 11:36:18 +05:00
Leonid Bugaev df3b410e98 Merge pull request #171 from buger/code-styling
Improve code documentation and styling
2015-07-17 11:33:26 +05:00
Leonid Bugaev 125d9db834 Next batch 2015-07-17 11:30:43 +05:00
Leonid Bugaev 6147e14f88 Merge pull request #173 from rgarcia/http-disallow-header
add --http-disallow-header option
2015-07-17 08:30:32 +05:00
Rafael Garcia fa003f5fec add --http-disallow-header option 2015-07-17 03:26:06 +00:00
Leonid Bugaev 28540f43a1 New portion of changes 2015-07-13 16:56:52 +05:00
Leonid Bugaev 6158ee3025 Remove debug 2015-07-13 16:20:57 +05:00
Leonid Bugaev 0859642b38 Update raw listener 2015-07-13 16:19:24 +05:00
Leonid Bugaev 32081b2d17 Improve byteutils 2015-07-13 09:36:54 +05:00
Leonid Bugaev 6647481fe0 Improve style and docs for proto package 2015-07-13 09:31:11 +05:00
Leonid Bugaev 0551b19ddd Add linter task 2015-07-12 19:08:12 +05:00
Leonid Bugaev a5f2d2fd6d Update README.md 2015-07-12 08:53:59 +05:00
Leonid Bugaev f1c341e2ac Update README.md 2015-07-12 08:52:59 +05:00
Leonid Bugaev becb4b958f Delete CHANGELOG.md 2015-07-12 08:47:39 +05:00
Leonid Bugaev db7f5c6a9c Update to use go 1.4.2 2015-07-11 19:34:20 +05:00
Leonid Bugaev 7af0eb2d65 Merge pull request #170 from buger/input-raw-performance
Improve performance of input-raw and improve handing of large payloads
2015-07-11 19:19:13 +05:00
Leonid Bugaev 32d0557a28 Reduce test file size 2015-07-11 19:07:35 +05:00
Leonid Bugaev 6ba0bfc614 Fix tests runner 2015-07-11 18:57:00 +05:00
Leonid Bugaev 34fe137d19 Improve message_id to handle multiple ports 2015-07-11 18:54:41 +05:00
Leonid Bugaev bb74704f9d Rollback to use net package 2015-07-11 18:46:35 +05:00
Leonid Bugaev d0873d1453 Try to add select 2015-07-11 18:36:00 +05:00
Leonid Bugaev 313c382adb By default use all cores 2015-07-11 09:58:10 +05:00
Leonid Bugaev 6b9c9b91e3 Improve performance of input-raw and improve handing of large payloads 2015-07-11 09:47:26 +05:00
Leonid Bugaev 531bd1da67 Merge pull request #169 from buger/http-client-bugfixes
Improve http-client reliability and improve debug
2015-07-10 17:19:06 +05:00
Leonid Bugaev acaf935cae Fix tests 2015-07-10 17:17:16 +05:00
Leonid Bugaev 0fe72cc432 Improve http-client reliability and improve debug 2015-07-10 17:13:11 +05:00
Leonid Bugaev b38cb458ff Update version 2015-07-09 18:45:39 +05:00
Leonid Bugaev d380b71945 Merge pull request #165 from buger/refactor-settings
Settings improvements
2015-07-09 18:41:29 +05:00
Leonid Bugaev fd5388b6c9 Improve README 2015-07-09 18:39:07 +05:00
Leonid Bugaev 48492dc8cb Allow url param rewriting 2015-07-09 16:58:42 +05:00
Leonid Bugaev 29035d4518 Allow multiple url regexp and apply fmt 2015-07-09 16:05:00 +05:00
Leonid Bugaev 1ce10249be Percent based syntax for hash filters 2015-07-09 15:44:15 +05:00
Leonid Bugaev 5a23573cfe Add --http-allow-param-hash 2015-07-09 15:30:06 +05:00
Leonid Bugaev 413f50129f Add support for restricting certain urls 2015-07-08 20:27:35 +05:00
Leonid Bugaev 686d7436ae Merge setting files 2015-07-08 20:09:43 +05:00
Leonid Bugaev 18c0473239 Merge pull request #164 from buger/change-tcp-output-format
Change internal message format
2015-07-08 19:12:25 +05:00
Leonid Bugaev 4d00ff8757 Change internal message format 2015-07-08 19:07:50 +05:00
Leonid Bugaev 8f73233717 Merge pull request #163 from buger/fix-proxy-set-host
Handle proxy request format and HTTP 1.0 requests
2015-07-08 18:40:12 +05:00
Leonid Bugaev 07c65117ad Handle proxy request format and HTTP 1.0 requests 2015-07-08 18:26:14 +05:00
Leonid Bugaev 31fa612fef Add go tool coverage make task 2015-07-07 08:29:49 +05:00
Leonid Bugaev 9ddb718525 Merge pull request #160 from buger/raw-http-client
Send unmodified http request
2015-07-06 21:43:47 +05:00
Leonid Bugaev 4f114f067b Apply fmt 2015-07-06 21:09:06 +05:00
Leonid Bugaev 9a0d9166c3 Yay! http.Client replaced by own simple http client 2015-07-06 21:08:34 +05:00
Leonid Bugaev bc48e1ad49 Fix tests 2015-07-06 18:56:50 +05:00
Leonid Bugaev 53c27cf374 Add Header Modifier tests 2015-07-06 18:53:54 +05:00
Leonid Bugaev a859ae02ad More tests and simplify hashFilter 2015-07-06 18:36:12 +05:00
Leonid Bugaev 2b17989fd9 Move HTTP modifier from HTTP output plugin 2015-07-05 23:17:57 +05:00
Leonid Bugaev 96fd713453 Use separate config for modifier 2015-07-05 10:25:44 +05:00
Leonid Bugaev 43fd14b561 Merge pull request #159 from whitekid/master
add .dockerignore file
2015-07-05 10:02:52 +05:00
Leonid Bugaev 72b15db93f Add utilities for modifying HTTP payload 2015-07-05 09:42:59 +05:00
Leonid Bugaev 557cdbebb6 Update README.md 2015-07-04 22:05:23 +05:00
Leonid Bugaev 925ff151dc Update README.md 2015-07-04 19:59:14 +05:00
Leonid Bugaev 481e4f2e74 Refactor http output settings 2015-07-02 23:08:31 +05:00
Leonid Bugaev 416586a365 Add support for redirects 2015-07-02 20:41:25 +05:00
Leonid Bugaev a08ecc79fc Add support for timeouts and servers closing connections 2015-07-02 18:33:38 +05:00
Leonid Bugaev decb9483a5 Fix docker file path 2015-07-02 10:25:47 +05:00
Leonid Bugaev 21c544ef67 Fix tests and apply go fmt 2015-07-01 19:29:19 +05:00
Leonid Bugaev 122f06f6c2 Add raw http client 2015-07-01 19:06:49 +05:00
Choe, Cheng-Dae 3d3c6a34b1 add .dockerignore 2015-06-30 21:34:42 +09:00
Leonid Bugaev 04d727915d Merge pull request #158 from buger/fix-100-expect
Fix Expect: 100-continue requests and refactor chunked encoding
2015-06-29 17:29:59 +05:00
Leonid Bugaev 1e7b603ae0 Fix import path 2015-06-29 17:14:11 +05:00
Leonid Bugaev 41294f9470 Fix 100-Expect requests and refactor chunked encoding 2015-06-29 17:01:26 +05:00
Leonid Bugaev 451a1cd11a Update version number 2015-06-26 10:02:31 +05:00
Leonid Bugaev 6fe7ba8e86 Merge pull request #157 from buger/encoding-chunked
Handle Transfer-Encoding: chunked
2015-06-26 09:19:02 +05:00
Leonid Bugaev 534e9b29fa Merge branch 'master' into encoding-chunked
Conflicts:
	output_http.go
2015-06-26 09:12:29 +05:00
Leonid Bugaev 390f75fd3c Handle Transfer-Encoding: chunked 2015-06-26 09:08:16 +05:00
Leonid Bugaev a059648008 Merge pull request #150 from makern/64bit-align
Fix alignment crash on i386
2015-03-23 13:33:39 +05:00
Markus Kern b2e1eea6bf Fix alignment crash on i386 2015-03-22 21:22:44 +00:00
Leonid Bugaev 92ba321569 Merge pull request #148 from MartinNowak/pow2
simplify power of 2 test
2015-03-19 17:37:53 +05:00
Martin Nowak 2b497e6177 simplify power of 2 test 2015-03-19 11:08:49 +01:00
Leonid Bugaev 09f9cac6fb Merge pull request #145 from tarfu/master
Introduce possibility to enable redirects.
2015-03-15 08:40:29 +05:00
Tobias Breitwieser d05bc0d5e9 Introduce possibility to enable redirects. 2015-03-14 16:47:04 +01:00
Leonid Bugaev 1c0182188c Merge pull request #143 from falzm/fix-socket-perm-crash
Prevent crash if no permissions on port
2015-03-06 16:13:12 +05:00
Marc Falzon cef8b75496 Prevent crash if no permissions on port
This fix prevents Gor from crashing at startup if run without proper
permissions for listening on specified port.
2015-03-05 21:58:52 +01:00
Leonid Bugaev e9fd5eb273 Merge pull request #141 from wmoss/master
Properly handle errors on input sockets
2015-03-03 12:00:55 +05:00
Will Moss 85fbfff308 Properly handle errors on input sockets
`buf` is of length 0, so you never make it into
the loop to handle the errors. This means you end
up in a tight loop calling `ReadBytes` and always
getting back `io.EOF` and 0 bytes.
2015-03-02 21:52:40 -08:00
Leonid Bugaev a7aaf7df65 Merge pull request #140 from wmoss/master
Properly return from errors from ReadRequest
2015-02-28 09:57:33 +05:00
Will Moss c1c87ed4e2 Properly return from errors from ReadRequest 2015-02-26 16:07:52 -08:00
Leonid Bugaev 072f332956 Merge pull request #138 from wmoss/master
Make TCPOutput more robust to failures
2015-02-23 09:05:18 +05:00
Will Moss 7c68920a3b Make TCPOutput more robust to failures
This should allow for running a gor listener on a
production box and bringing up and down the replay
node without having to go restart the listener.
2015-02-22 15:49:35 -08:00
Leonid Bugaev b6351a8525 Fix travis test runner 2015-02-22 14:19:48 +05:00
Leonid Bugaev f557d412c4 Merge pull request #133 from buger/fix-post-requests
Fix POST requests
2015-01-11 20:04:23 +05:00
Leonid Bugaev ed5c2215f5 Fix POST requests 2015-01-06 17:50:36 +05:00
Leonid Bugaev ffe1646d8e Merge pull request #131 from chmac/patch-1
Minor typo correction
2014-12-07 20:37:26 +05:00
Callum Macdonald ae293320ad Minor typo correction 2014-12-07 16:20:49 +01:00
Leonid Bugaev eb7885cd56 Patch from @jcw9930
See https://github.com/buger/gor/pull/127#issuecomment-64538157
2014-11-26 16:30:17 +05:00
Leonid Bugaev b6977df90d Merge pull request #127 from jcw9930/url_regexp_rewrite
add regular expression matching to --output-http-rewrite-url
2014-10-29 15:24:42 +05:00
Jan Willies 15417894a9 add test that explicitly checks if rewrites.Rewrite(url) == /v2/user/joe/ping 2014-10-28 18:42:38 +01:00
Jan Willies 7f49314ec7 allow matched parts in -output-http-rewrite-url 2014-10-28 18:12:21 +01:00
Jan Willies 80572f5f18 add regular expression matching to --output-http-rewrite-url 2014-10-28 14:52:55 +01:00
53 changed files with 2676 additions and 1132 deletions
+1
View File
@@ -0,0 +1 @@
*.tar.gz
+4
View File
@@ -1,2 +1,6 @@
*.swp
*.gor
*.out
*.bin
+3 -3
View File
@@ -1,3 +1,3 @@
language: go
go: 1.3.3
script: sudo -E bash -c "source /etc/profile && gvm use go1.3.3 && export GOPATH=$HOME/gopath:$GOPATH && go get && GORACE='halt_on_error=1' go test -race -v"
language: go
go: 1.4.2
script: sudo -E bash -c "source /etc/profile && eval '$(gimme 1.4.2)' && export GOPATH=$HOME/gopath:$GOPATH && go get && GORACE='halt_on_error=1' go test ./... -v -timeout 15s"
-25
View File
@@ -1,25 +0,0 @@
v0.7.0 - 31 Oct 2013
* New modular architecture. Listener and Replay functionality merged.
* Added option to equally split traffic between multiple outputs: --split-output true
* Saving requests to file and replaying from it
* Injecting custom headers to http requests
* Advanced stats using ElasticSearch
v0.3.5 - 15 Sep 2013
* Significantly 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
* Significantly improved performance
* Fixed bugs causing locking and message dropping (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
+4 -3
View File
@@ -1,9 +1,10 @@
FROM google/golang
FROM google/golang:1.4
RUN cd /goroot/src/ && GOOS=linux GOARCH=386 ./make.bash --no-clean
WORKDIR /gopath/src/gor
WORKDIR /gopath/src/github.com/buger/gor/
ADD . /gopath/src/gor
ADD . /gopath/src/github.com/buger/gor/
RUN go get -u github.com/golang/lint/golint
RUN go get
+31 -10
View File
@@ -1,28 +1,49 @@
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 settings_header_filters.go settings_header_hash_filters.go settings_headers.go settings_methods.go settings_option.go settings_url_regexp.go test_input.go elasticsearch.go settings_url_map.go
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
SOURCE_PATH = /gopath/src/github.com/buger/gor/
release: release-x86 release-x64
release-x64:
docker run -v `pwd`:/gopath/src/gor -t --env GOOS=linux --env GOARCH=amd64 --env CGO_ENABLED=0 -i gor go build && tar -czf gor_x64.tar.gz gor && rm gor
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 --env CGO_ENABLED=0 -i gor go build && tar -czf gor_x64.tar.gz gor && rm gor
release-x86:
docker run -v `pwd`:/gopath/src/gor -t --env GOOS=linux --env GOARCH=386 --env CGO_ENABLED=0 -i gor go build && tar -czf gor_x86.tar.gz gor && rm gor
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=386 --env CGO_ENABLED=0 -i gor go build && tar -czf gor_x86.tar.gz gor && rm gor
dbuild:
docker build -t gor .
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`:/gopath/src/gor -t -i --env GORACE="halt_on_error=1" gor go test $(ARGS) -race -v
docker run -v `pwd`:$(SOURCE_PATH) -t -i gor go test ./... $(ARGS) -v -timeout 10s
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`:/gopath/src/gor -t -i gor go fmt
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`:/gopath/src/gor -t -i gor go test -v -run NOT_EXISTING -bench HTTP
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`:/gopath/src/gor -t -i gor go run $(SOURCE) --input-dummy=0 --input-http=:9000 --output-http="http://localhost:9000" --verbose
docker run -v `pwd`:$(SOURCE_PATH) -t -i gor go run $(SOURCE) --input-dummy=0 --output-http="http://localhost:9000" --verbose
dbash:
docker run -v `pwd`:/gopath/src/gor -t -i gor /bin/bash
drecord:
docker run -v `pwd`:$(SOURCE_PATH) -t -i gor go run $(SOURCE) --input-dummy=0 --output-file=requests.bin --verbose
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
+204 -170
View File
@@ -1,4 +1,3 @@
[![Stories in Ready](https://badge.waffle.io/buger/gor.png?label=ready)](https://waffle.io/buger/gor)
[![Build Status](https://travis-ci.org/buger/gor.png?branch=master)](https://travis-ci.org/buger/gor)
## About
@@ -10,7 +9,7 @@ Its main goal is to replay traffic from production servers to staging and dev en
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.
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.
![Diagram](http://i.imgur.com/9mqj2SK.png)
@@ -27,6 +26,8 @@ sudo gor --input-raw :80 --output-tcp replay.local:28020
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.
@@ -34,7 +35,7 @@ It's recommended to use separate server for replaying traffic, but if you have e
sudo gor --input-raw :80 --output-http "http://staging.com"
```
### Guarante of replay and HTTP input
### 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.
```
@@ -43,19 +44,49 @@ 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
## Advanced use
### 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 dynamic pull of workers: it starts with 10 and create 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 when your replayed environment introduce 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
Every input and output support rate limiting. It can be useful if you want
forward only part of production traffic and not overload your staging
environment.
Rate limiting can be useful if you want forward only part of production traffic and not overload your staging environment. There is 2 strategies: dropping random requests or dropping fraction of requests based on Header or URL param value.
#### Dropping random requests
Every input and output support 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 weight you specified.
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:
@@ -73,51 +104,80 @@ gor --input-tcp :28020 --output-http "http://staging.com|10"
gor --input-raw :80 --output-tcp "replay.local:28020|10%"
```
### 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.
```
# Replay from file on 2x speed
gor --input-file "requests.gor|200%" --output-http "staging.com"
#### 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
#### Match on regexp of url
#### Allow url regexp
```
# only forward requests being sent to the api... domains
gor --input-raw :8080 --output-http staging.com --output-http-url-regexp ^www.
# 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 --output-http-header-filter api-version:^1\.0\d
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 hash of header
```
# send 1/32 of all users consistently to staging
gor --input-raw :8080 --output-http staging.com --output-http-header-hash-filter user-id:1/32
```
### 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:
#### Filter based on http method
Requests not matching a specified whitelist can be filtered out. For example to strip non-nullipotent requests:
```
gor --input-tcp :28020 --output-http "http://staging.com" --output-http "http://dev.com" --split-output true
gor --input-raw :80 --output-http "http://staging.server" \
--http-allow-method GET \
--http-allow-method OPTIONS
```
### Saving requests to file
### Rewriting original request
Gor supports built-in basic rewriting support, for complex logic see https://github.com/buger/gor/pull/162
#### 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.
### Saving requests to file and replaying them
You can save requests to file, and replay them later:
```
# write to file
@@ -129,24 +189,13 @@ gor --input-file requests.gor --output-http "http://staging.com"
**Note:** Replay will preserve the original time differences between requests.
### Injecting headers
### Load testing
Additional headers can be injected/overwritten into requests during replay. This may be useful if you need to identify requests generated by Gor or enable feature flagged functionality in an application:
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:
```
gor --input-raw :80 --output-http "http://staging.server" \
--output-http-header "User-Agent: Replayed by Gor" \
--output-http-header "Enable-Feature-X: true"
```
## Filtering HTTP methods
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" \
--output-http-method GET \
--output-http-method OPTIONS
# Replay from file on 2x speed
gor --input-file "requests.gor|200%" --output-http "staging.com"
```
### Basic Auth
@@ -159,14 +208,52 @@ gor --input-raw :80 --output-http "http://user:pass@staging .com"
Note: This will overwrite any Authorization headers in the original request.
#### Rewrite the target urls based on a mapping
```
# rewrite url to match the following
gor --input-raw :8080 --output-http staging.com --output-http-rewrite-url /xml_test/interface.php:/api/service.do
```
## 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.
@@ -185,60 +272,57 @@ https://github.com/buger/gor/releases
## Command line reference
`gor -h` output:
```
-cpuprofile="": write cpu profile to file
-memprofile="": write memory profile to this file
-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-diallow-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-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-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
# 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-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
# 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=[]: Inject additional headers to http reqest:
gor --input-raw :8080 --output-http staging.com --output-http-header 'User-Agent: Gor'
-output-http-header-filter=[]: A regexp to match a specific header against. Requests with non-matching headers will be dropped:
gor --input-raw :8080 --output-http staging.com --output-http-header-filter api-version:^v1
-output-http-header-hash-filter=[]: Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific header. The fraction must have a denominator that is a power of two:
gor --input-raw :8080 --output-http staging.com --output-http-header-hash-filter user-id:1/4
-output-http-url-regexp=: A regexp to match requests against. Anything else will be dropped:
gor --input-raw :8080 --output-http staging.com --output-http-url-regexp ^www.
-output-http-workers=-1: Number of http output workers desired. Use default -1 for dynamic worker scaling. Gor will add http workers if its work queue starts getting too full and kill them .
-output-http-stats=false: If set to `true` it gives out queuing stats for the HTTP output every 5 seconds in the form latest,mean,max,count,count/second.
-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: If set to `true` it gives out queuing stats for the TCP output every 5 seconds in the form latest,mean,max,count,count/second.
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.
-output-http-rewrite-url=[]: Rewrites the url in the request based on a mapping
gor --input-raw :8080 --output-http staging.com --output-http-rewrite-url /xml_test/interface.php:/api/service.do
-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
@@ -271,6 +355,20 @@ For now only Linux based. *BSD (including MacOS is not supported yet, check http
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:
@@ -278,73 +376,8 @@ Typical linux shell has a small open files soft limit at 1024. You can easily ra
More about ulimit: http://blog.thecodingmachine.com/content/solving-too-many-open-files-exception-red5-or-any-other-application
### What do the stats commands do?
Gor can report stats on the output-tcp and output-http request queues. Stats are reported to the console every 5 seconds in the form `latest,mean,max,count,count/second` by using the `-output-http-stats` and `-output-tcp-stats` options.
Examples:
```
2014/04/23 21:17:50 output_tcp:latest,mean,max,count,count/second
2014/04/23 21:17:50 output_tcp:0,0,0,0,0
2014/04/23 21:17:55 output_tcp:1,1,2,68,13
2014/04/23 21:18:00 output_tcp:1,1,2,92,18
2014/04/23 21:18:05 output_tcp:1,1,2,119,23
2014/04/23 21:18:10 output_tcp:1,0,1,95,19
2014/04/23 21:18:15 output_tcp:1,1,2,92,18
2014/04/23 21:18:20 output_tcp:1,1,2,108,21
2014/04/23 21:18:25 output_tcp:1,1,2,117,23
2014/04/23 21:18:30 output_tcp:1,1,2,113,22
2014/04/23 21:18:35 output_tcp:21,20,21,132,26
2014/04/23 21:18:40 output_tcp:100,99,100,99,19
```
```
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
2014/04/23 21:18:28 output_http:100,99,100,55,11
2014/04/23 21:18:34 output_http:100,99,100,57,11
2014/04/23 21:18:41 output_http:100,99,100,61,12
2014/04/23 21:18:48 output_http:100,99,100,56,11
2014/04/23 21:18:56 output_http:100,99,100,58,11
2014/04/23 21:19:01 output_http:100,99,100,31,6
2014/04/23 21:19:08 output_http:100,99,100,61,12
2014/04/23 21:19:15 output_http:100,99,100,64,12
2014/04/23 21:19:21 output_http:100,99,100,70,14
2014/04/23 21:19:28 output_http:100,99,100,61,12
2014/04/23 21:19:35 output_http:100,99,100,56,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.
### 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.
### How does dynamic http worker scaling work?
By using the Gor setting `--output-http-workers=-1` Gor will create 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.
## Tuning
@@ -383,4 +416,5 @@ To achieve the top most performance you should tune the source server system lim
* [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
+40
View File
@@ -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
}
}
+32
View File
@@ -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")
}
}
+44 -44
View File
@@ -4,8 +4,8 @@ import (
"encoding/json"
"github.com/buger/elastigo/api"
"github.com/buger/elastigo/core"
"github.com/buger/gor/proto"
"log"
"net/http"
"regexp"
"time"
)
@@ -26,27 +26,27 @@ type ESPlugin struct {
}
type ESRequestResponse struct {
ReqUrl string `json:"Req_URL"`
ReqMethod string `json:"Req_Method"`
ReqUserAgent string `json:"Req_User-Agent"`
ReqAcceptLanguage string `json:"Req_Accept-Language,omitempty"`
ReqAccept string `json:"Req_Accept,omitempty"`
ReqAcceptEncoding string `json:"Req_Accept-Encoding,omitempty"`
ReqIfModifiedSince string `json:"Req_If-Modified-Since,omitempty"`
ReqConnection string `json:"Req_Connection,omitempty"`
ReqCookies []*http.Cookie `json:"Req_Cookies,omitempty"`
RespStatus string `json:"Resp_Status"`
RespStatusCode int `json:"Resp_Status-Code"`
RespProto string `json:"Resp_Proto,omitempty"`
RespContentLength int64 `json:"Resp_Content-Length,omitempty"`
RespContentType string `json:"Resp_Content-Type,omitempty"`
RespTransferEncoding []string `json:"Resp_Transfer-Encoding,omitempty"`
RespContentEncoding string `json:"Resp_Content-Encoding,omitempty"`
RespExpires string `json:"Resp_Expires,omitempty"`
RespCacheControl string `json:"Resp_Cache-Control,omitempty"`
RespVary string `json:"Resp_Vary,omitempty"`
RespSetCookie string `json:"Resp_Set-Cookie,omitempty"`
Rtt int64 `json:"RTT"`
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
}
@@ -111,8 +111,8 @@ func (p *ESPlugin) RttDurationToMs(d time.Duration) int64 {
return int64(fl)
}
func (p *ESPlugin) ResponseAnalyze(req *http.Request, resp *http.Response, start, stop time.Time) {
if resp == nil {
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
}
@@ -120,26 +120,26 @@ func (p *ESPlugin) ResponseAnalyze(req *http.Request, resp *http.Response, start
rtt := p.RttDurationToMs(stop.Sub(start))
esResp := ESRequestResponse{
ReqUrl: req.URL.String(),
ReqMethod: req.Method,
ReqUserAgent: req.UserAgent(),
ReqAcceptLanguage: req.Header.Get("Accept-Language"),
ReqAccept: req.Header.Get("Accept"),
ReqAcceptEncoding: req.Header.Get("Accept-Encoding"),
ReqIfModifiedSince: req.Header.Get("If-Modified-Since"),
ReqConnection: req.Header.Get("Connection"),
ReqCookies: req.Cookies(),
RespStatus: resp.Status,
RespStatusCode: resp.StatusCode,
RespProto: resp.Proto,
RespContentLength: resp.ContentLength,
RespContentType: resp.Header.Get("Content-Type"),
RespTransferEncoding: resp.TransferEncoding,
RespContentEncoding: resp.Header.Get("Content-Encoding"),
RespExpires: resp.Header.Get("Expires"),
RespCacheControl: resp.Header.Get("Cache-Control"),
RespVary: resp.Header.Get("Vary"),
RespSetCookie: resp.Header.Get("Set-Cookie"),
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,
}
+19 -4
View File
@@ -5,6 +5,7 @@ import (
"time"
)
// Start initialize loop for sending data from inputs to outputs
func Start(stop chan int) {
for _, in := range Plugins.Inputs {
go CopyMulty(in, Plugins.Outputs...)
@@ -19,19 +20,33 @@ func Start(stop chan int) {
}
}
// Copy from 1 reader to multiple writers
// 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 {
Debug("Sending", src, ": ", string(buf[0:nr]))
payload := buf[0:nr]
if modifier != nil {
payload = modifier.Rewrite(payload)
// If modifier tells to skip request
if len(payload) == 0 {
continue
}
}
if Settings.debug {
Debug("[EMITTER] Sending payload, size:", len(payload), "First 500 bytes:", string(payload[0:500]))
}
if Settings.splitOutput {
// Simple round robin
writers[wIndex].Write(buf[0:nr])
writers[wIndex].Write(payload)
wIndex++
@@ -40,7 +55,7 @@ func CopyMulty(src io.Reader, writers ...io.Writer) (err error) {
}
} else {
for _, dst := range writers {
dst.Write(buf[0:nr])
dst.Write(payload)
}
}
+6
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"log"
"os"
"runtime"
"runtime/debug"
"runtime/pprof"
"time"
@@ -19,6 +20,11 @@ var (
)
func main() {
// If not set via env cariable
if len(os.Getenv("GOMAXPROCS")) == 0 {
runtime.GOMAXPROCS(runtime.NumCPU() * 2)
}
// Don't exit on panic
defer func() {
if r := recover(); r != nil {
+195
View File
@@ -0,0 +1,195 @@
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
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, ":") {
u.Host += ":" + defaultPorts[u.Scheme]
}
if config.Timeout.Nanoseconds() == 0 {
config.Timeout = 5 * time.Second
}
if config.ResponseBufferSize == 0 {
config.ResponseBufferSize = 512*1024 // 500kb
}
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()
c.conn, err = net.Dial("tcp", c.host)
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("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)
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)
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)
return
}
payload := c.respBuf[:n]
Debug("[HTTPClient] Received:", 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))
}
+301
View File
@@ -0,0 +1,301 @@
package main
import (
"bytes"
"crypto/rand"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"sync"
"testing"
_ "time"
)
func TestHTTPClientURLPort(t *testing.T) {
c1 := NewHTTPClient("http://example.com", &HTTPClientConfig{})
if c1.baseURL != "http://example.com:80" {
t.Error("Sould 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:80" {
t.Error("Sould 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()
}))
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()
}))
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()
}))
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")
go func() {
for {
conn, _ := ln.Accept()
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")
go func() {
for {
conn, err := ln.Accept()
if err != nil {
// handle error
}
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()
}))
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()
}))
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()
}))
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true})
wg.Add(1)
client.Send(GETPayload)
wg.Wait()
}
+153
View File
@@ -0,0 +1,153 @@
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 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
}
+215
View File
@@ -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
}
+65
View File
@@ -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 :")
}
}
+247
View File
@@ -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))
}
}
+2
View File
@@ -4,10 +4,12 @@ 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)
+4 -2
View File
@@ -7,6 +7,7 @@ import (
"time"
)
// FileInput can read requests generated by FileOutput
type FileInput struct {
data chan []byte
path string
@@ -14,19 +15,20 @@ type FileInput struct {
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)
i.init(path)
go i.emit()
return
}
func (i *FileInput) Init(path string) {
func (i *FileInput) init(path string) {
file, err := os.Open(path)
if err != nil {
+2
View File
@@ -7,12 +7,14 @@ import (
"net/http/httputil"
)
// 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)
+2
View File
@@ -7,11 +7,13 @@ import (
"strings"
)
// RAWInput used for intercepting traffic for given address
type RAWInput struct {
data chan []byte
address string
}
// NewRAWInput constructor for RAWInput. Accepts address with port as argument.
func NewRAWInput(address string) (i *RAWInput) {
i = new(RAWInput)
i.data = make(chan []byte)
+178 -2
View File
@@ -1,11 +1,18 @@
package main
import (
"bytes"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"net/http/httputil"
"os/exec"
"strings"
"sync"
"testing"
"time"
)
func TestRAWInput(t *testing.T) {
@@ -25,15 +32,184 @@ func TestRAWInput(t *testing.T) {
address := strings.Replace(listener.Addr().String(), "[::]", "127.0.0.1", -1)
client := NewHTTPClient(address, &HTTPClientConfig{})
time.Sleep(time.Millisecond)
go Start(quit)
for i := 0; i < 100; i++ {
wg.Add(1)
res, _ := http.Get("http://" + address)
res.Body.Close()
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 := startHTTP(func(req *http.Request) {
defer req.Body.Close()
ioutil.ReadAll(req.Body)
wg.Done()
})
originAddr := strings.Replace(origin.Addr().String(), "[::]", "127.0.0.1", -1)
input := NewRAWInput(originAddr)
// We will use it to get content of raw HTTP request
testOutput := NewTestOutput(func(data []byte) {
if strings.Contains(string(data), "Expect: 100-continue") {
t.Error("Should not contain 100-continue header")
}
wg.Done()
})
listener := startHTTP(func(req *http.Request) {
defer req.Body.Close()
body, _ := ioutil.ReadAll(req.Body)
if !bytes.Equal(body, fileContent) {
buf, _ := httputil.DumpRequest(req, true)
t.Error("Wrong POST body:", string(buf))
}
wg.Done()
})
replayAddr := listener.Addr().String()
httpOutput := NewHTTPOutput(replayAddr, &HTTPOutputConfig{})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{testOutput, httpOutput}
go Start(quit)
wg.Add(3)
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 := startHTTP(func(req *http.Request) {
defer req.Body.Close()
ioutil.ReadAll(req.Body)
wg.Done()
})
originAddr := strings.Replace(origin.Addr().String(), "[::]", "127.0.0.1", -1)
input := NewRAWInput(originAddr)
listener := startHTTP(func(req *http.Request) {
defer req.Body.Close()
body, _ := ioutil.ReadAll(req.Body)
if !bytes.Equal(body, fileContent) {
buf, _ := httputil.DumpRequest(req, true)
t.Error("Wrong POST body:", string(buf))
}
wg.Done()
})
replayAddr := listener.Addr().String()
httpOutput := NewHTTPOutput(replayAddr, &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)
}
// Origing and Replay server initialization
origin := startHTTP(func(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.Addr().String(), "[::]", "127.0.0.1", -1)
input := NewRAWInput(originAddr)
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)
}
+19 -19
View File
@@ -2,20 +2,22 @@ package main
import (
"bufio"
"io"
"encoding/hex"
"fmt"
"log"
"net"
"os"
)
// Can be tested using nc tool:
// echo "asdad" | nc 127.0.0.1 27017
//
// TCPInput used for internal communication
// It expected hex encoded data
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)
@@ -59,23 +61,21 @@ func (i *TCPInput) handleConnection(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
scanner := bufio.NewScanner(reader)
for {
buf, err := reader.ReadBytes('¶')
buf_len := len(buf)
if buf_len > 0 {
new_buf_len := len(buf) - 2
if new_buf_len > 0 {
new_buf := make([]byte, new_buf_len)
copy(new_buf, buf[:new_buf_len])
i.data <- new_buf
if err != nil {
if err != io.EOF {
log.Printf("error: %s\n", err)
}
}
}
for scanner.Scan() {
encodedPayload := scanner.Bytes()
// Hex encoding always 2x number of bytes
decoded := make([]byte, len(encodedPayload)/2)
_, err := hex.Decode(decoded, encodedPayload)
if err != nil {
log.Println("[TCPInput] failed to hex decode TCP payload:", err)
}
i.data <- decoded
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "Unexpected error in input tcp connection:", err)
}
}
+8 -7
View File
@@ -1,6 +1,7 @@
package main
import (
"encoding/hex"
"io"
"log"
"net"
@@ -38,10 +39,10 @@ func TestTCPInput(t *testing.T) {
for i := 0; i < 100; i++ {
wg.Add(1)
new_buf := make([]byte, len(msg)+2)
msg = append(msg, []byte("¶")...)
copy(new_buf, msg)
conn.Write(new_buf)
encoded := make([]byte, len(msg)*2+1)
hex.Encode(encoded, msg)
conn.Write(append(encoded, '\n'))
}
wg.Wait()
@@ -82,10 +83,10 @@ func BenchmarkTCPInput(b *testing.B) {
for {
data := <-dataChan
new_buf := make([]byte, len(data)+2)
buf := make([]byte, len(data)+2)
data = append(data, []byte("¶")...)
copy(new_buf, data)
conn.Write(new_buf)
copy(buf, data)
conn.Write(buf)
}
}(conn)
}
+3
View File
@@ -9,6 +9,7 @@ import (
"time"
)
// Limiter is a wrapper for input or output plugin which adds rate limiting
type Limiter struct {
plugin interface{}
limit int
@@ -30,6 +31,8 @@ func parseLimitOptions(options string) (limit int, isPercent bool) {
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)
+2
View File
@@ -4,9 +4,11 @@ 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)
+5 -2
View File
@@ -8,26 +8,29 @@ import (
"time"
)
// RawRequest stores original start time and request payload
type RawRequest struct {
Timestamp int64
Request []byte
}
// FileOutput output plugin
type FileOutput struct {
path string
encoder *gob.Encoder
file *os.File
}
// NewFileOutput constructor for FileOutput, accepts path
func NewFileOutput(path string) io.Writer {
o := new(FileOutput)
o.path = path
o.Init(path)
o.init(path)
return o
}
func (o *FileOutput) Init(path string) {
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)
+61 -129
View File
@@ -1,125 +1,107 @@
package main
import (
"bufio"
"bytes"
"io"
"log"
"net/http"
"net/url"
"strings"
"sync/atomic"
"time"
)
type RedirectNotAllowed struct{}
const initialDynamicWorkers = 10
func (e *RedirectNotAllowed) Error() string {
return "Redirects not allowed"
// 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
}
// customCheckRedirect disables redirects https://github.com/buger/gor/pull/15
func customCheckRedirect(req *http.Request, via []*http.Request) error {
if len(via) >= 0 {
return new(RedirectNotAllowed)
}
return nil
}
// ParseRequest in []byte returns a http request or an error
func ParseRequest(data []byte) (request *http.Request, err error) {
buf := bytes.NewBuffer(data)
reader := bufio.NewReader(buf)
request, err = http.ReadRequest(reader)
return
}
const InitialDynamicWorkers = 10
// 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
activeWorkers int64
needWorker chan int
needWorker chan int
urlRegexp HTTPUrlRegexp
headerFilters HTTPHeaderFilters
headerHashFilters HTTPHeaderHashFilters
outputHTTPUrlRewrite UrlRewriteMap
headers HTTPHeaders
methods HTTPMethods
elasticSearch *ESPlugin
config *HTTPOutputConfig
queueStats *GorStat
elasticSearch *ESPlugin
}
func NewHTTPOutput(address string, headers HTTPHeaders, methods HTTPMethods, urlRegexp HTTPUrlRegexp, headerFilters HTTPHeaderFilters, headerHashFilters HTTPHeaderHashFilters, elasticSearchAddr string, outputHTTPUrlRewrite UrlRewriteMap) io.Writer {
// NewHTTPOutput constructor for HTTPOutput
// Initialize workers
func NewHTTPOutput(address string, config *HTTPOutputConfig) io.Writer {
o := new(HTTPOutput)
if !strings.HasPrefix(address, "http") {
address = "http://" + address
}
o.address = address
o.headers = headers
o.methods = methods
o.config = config
o.urlRegexp = urlRegexp
o.headerFilters = headerFilters
o.headerHashFilters = headerHashFilters
o.outputHTTPUrlRewrite = outputHTTPUrlRewrite
o.queue = make(chan []byte, 100)
if Settings.outputHTTPStats {
if o.config.stats {
o.queueStats = NewGorStat("output_http")
}
o.queue = make(chan []byte, 100)
o.needWorker = make(chan int, 1)
// Initial workers count
if Settings.outputHTTPWorkers == -1 {
o.needWorker <- InitialDynamicWorkers
if o.config.workers == 0 {
o.needWorker <- initialDynamicWorkers
} else {
o.needWorker <- Settings.outputHTTPWorkers
o.needWorker <- o.config.workers
}
if elasticSearchAddr != "" {
if o.config.elasticSearch != "" {
o.elasticSearch = new(ESPlugin)
o.elasticSearch.Init(elasticSearchAddr)
o.elasticSearch.Init(o.config.elasticSearch)
}
go o.WorkerMaster()
go o.workerMaster()
return o
}
func (o *HTTPOutput) WorkerMaster() {
func (o *HTTPOutput) workerMaster() {
for {
new_workers := <-o.needWorker
for i := 0; i < new_workers; i++ {
go o.Worker()
newWorkers := <-o.needWorker
for i := 0; i < newWorkers; i++ {
go o.startWorker()
}
// Disable dynamic scaling if workers poll fixed size
if Settings.outputHTTPWorkers != -1 {
if o.config.workers != 0 {
return
}
}
}
func (o *HTTPOutput) Worker() {
client := &http.Client{
CheckRedirect: customCheckRedirect,
}
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,
})
death_count := 0
deathCount := 0
atomic.AddInt64(&o.activeWorkers, 1)
@@ -127,19 +109,19 @@ func (o *HTTPOutput) Worker() {
select {
case data := <-o.queue:
o.sendRequest(client, data)
death_count = 0
deathCount = 0
case <-time.After(time.Millisecond * 100):
// When dynamic scaling enabled workers die after 2s of inactivity
if Settings.outputHTTPWorkers == -1 {
death_count += 1
if o.config.workers == 0 {
deathCount++
} else {
continue
}
if death_count > 20 {
if deathCount > 20 {
workersCount := atomic.LoadInt64(&o.activeWorkers)
// At least 1 worker should be alive
// At least 1 startWorker should be alive
if workersCount != 1 {
atomic.AddInt64(&o.activeWorkers, -1)
return
@@ -155,11 +137,11 @@ func (o *HTTPOutput) Write(data []byte) (n int, err error) {
o.queue <- buf
if Settings.outputHTTPStats {
if o.config.stats {
o.queueStats.Write(len(o.queue))
}
if Settings.outputHTTPWorkers == -1 {
if o.config.workers == 0 {
workersCount := atomic.LoadInt64(&o.activeWorkers)
if len(o.queue) > int(workersCount) {
@@ -170,49 +152,12 @@ func (o *HTTPOutput) Write(data []byte) (n int, err error) {
return len(data), nil
}
func (o *HTTPOutput) sendRequest(client *http.Client, data []byte) {
request, err := ParseRequest(data)
if err != nil {
log.Println("Cannot parse request", string(data), err)
return
}
if len(o.methods) > 0 && !o.methods.Contains(request.Method) {
return
}
if !(o.urlRegexp.Good(request) && o.headerFilters.Good(request) && o.headerHashFilters.Good(request)) {
return
}
// Rewrite the path as necessary
request.URL.Path = o.outputHTTPUrlRewrite.Rewrite(request.URL.Path)
// Change HOST of original request
URL := o.address + request.URL.Path + "?" + request.URL.RawQuery
request.RequestURI = ""
request.URL, _ = url.ParseRequestURI(URL)
for _, header := range o.headers {
SetHeader(request, header.Name, header.Value)
}
func (o *HTTPOutput) sendRequest(client *HTTPClient, request []byte) {
start := time.Now()
resp, err := client.Do(request)
resp, err := client.Send(request)
stop := time.Now()
// We should not count Redirect as errors
if urlErr, ok := err.(*url.Error); ok {
if _, ok := urlErr.Err.(*RedirectNotAllowed); ok {
err = nil
}
}
if err == nil {
defer resp.Body.Close()
} else {
if err != nil {
log.Println("Request error:", err)
}
@@ -221,19 +166,6 @@ func (o *HTTPOutput) sendRequest(client *http.Client, data []byte) {
}
}
func SetHeader(request *http.Request, name string, value string) {
// Need to check here for the Host header as it needs to be set on the request and not as a separate header
// http.ReadRequest sets it by default to the URL Host of the request being read
if name == "Host" {
request.Host = value
} else {
request.Header.Set(name, value)
}
return
}
func (o *HTTPOutput) String() string {
return "HTTP output: " + o.address
}
+83 -33
View File
@@ -2,8 +2,11 @@ package main
import (
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
_ "net/http/httputil"
"sync"
"testing"
"time"
@@ -11,7 +14,7 @@ import (
func startHTTP(cb func(*http.Request)) net.Listener {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
go cb(r)
cb(r)
})
listener, _ := net.Listen("tcp", ":0")
@@ -21,36 +24,12 @@ func startHTTP(cb func(*http.Request)) net.Listener {
return listener
}
func TestSetHeader(t *testing.T) {
req := &http.Request{
Header: make(map[string][]string),
}
req.Host = "test.com"
SetHeader(req, "Host", "test2.com")
if req.Host != "test2.com" {
t.Error("Expected test2.com - got ", req.Host)
}
SetHeader(req, "test_header", "test_value")
if req.Header.Get("test_header") != "test_value" {
t.Error("Wrong header value found")
}
}
func TestHTTPOutput(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
headers := HTTPHeaders{HTTPHeader{"User-Agent", "Gor"}}
methods := HTTPMethods{"GET", "PUT", "POST"}
listener := startHTTP(func(req *http.Request) {
if req.Header.Get("User-Agent") != "Gor" {
t.Error("Wrong header")
@@ -60,10 +39,23 @@ func TestHTTPOutput(t *testing.T) {
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()
})
output := NewHTTPOutput(listener.Addr().String(), headers, methods, HTTPUrlRegexp{}, HTTPHeaderFilters{}, HTTPHeaderHashFilters{}, "", UrlRewriteMap{})
headers := HTTPHeaders{HTTPHeader{"User-Agent", "Gor"}}
methods := HTTPMethods{[]byte("GET"), []byte("PUT"), []byte("POST")}
Settings.modifierConfig = HTTPModifierConfig{headers: headers, methods: methods}
output := NewHTTPOutput(listener.Addr().String(), &HTTPOutputConfig{})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
@@ -71,7 +63,7 @@ func TestHTTPOutput(t *testing.T) {
go Start(quit)
for i := 0; i < 100; i++ {
wg.Add(2)
wg.Add(2) // OPTIONS should be ignored
input.EmitPOST()
input.EmitOPTIONS()
input.EmitGET()
@@ -80,23 +72,81 @@ func TestHTTPOutput(t *testing.T) {
wg.Wait()
close(quit)
Settings.modifierConfig = HTTPModifierConfig{}
}
func TestHTTPOutputKeepOriginalHost(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
listener := startHTTP(func(req *http.Request) {
if req.Host != "custom-host.com" {
t.Error("Wrong header", req.Host)
}
wg.Done()
})
headers := HTTPHeaders{HTTPHeader{"Host", "custom-host.com"}}
Settings.modifierConfig = HTTPModifierConfig{headers: headers}
output := NewHTTPOutput(listener.Addr().String(), &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)
input := NewTestInput()
headers := HTTPHeaders{HTTPHeader{"User-Agent", "Gor"}}
methods := HTTPMethods{"GET", "PUT", "POST"}
listener := startHTTP(func(req *http.Request) {
time.Sleep(50 * time.Millisecond)
wg.Done()
})
output := NewHTTPOutput(listener.Addr().String(), headers, methods, HTTPUrlRegexp{}, HTTPHeaderFilters{}, HTTPHeaderHashFilters{}, "", UrlRewriteMap{})
input := NewTestInput()
output := NewHTTPOutput(listener.Addr().String(), &HTTPOutputConfig{})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
+24 -6
View File
@@ -1,12 +1,17 @@
package main
import (
"encoding/hex"
"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
@@ -14,6 +19,8 @@ type TCPOutput struct {
bufStats *GorStat
}
// NewTCPOutput constructor for TCPOutput
// Initialize 10 workers which hold keep-alive connection
func NewTCPOutput(address string) io.Writer {
o := new(TCPOutput)
@@ -32,19 +39,30 @@ func NewTCPOutput(address string) io.Writer {
}
func (o *TCPOutput) worker() {
conn, _ := o.connect(o.address)
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(<-o.buf)
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) {
new_buf := make([]byte, len(data)+2)
data = append(data, []byte("¶")...)
copy(new_buf, data)
o.buf <- new_buf
// Hex encoding always 2x number of bytes
encoded := make([]byte, len(data)*2+1)
hex.Encode(encoded, data)
encoded[len(encoded)-1] = '\n'
o.buf <- encoded
if Settings.outputTCPStats {
o.bufStats.Write(len(o.buf))
}
+10 -12
View File
@@ -2,6 +2,7 @@ package main
import (
"bufio"
"encoding/hex"
"io"
"log"
"net"
@@ -44,22 +45,19 @@ func startTCP(cb func([]byte)) net.Listener {
go func() {
for {
conn, _ := listener.Accept()
defer conn.Close()
go func() {
reader := bufio.NewReader(conn)
for {
buf, err := reader.ReadBytes('¶')
new_buf_len := len(buf) - 2
new_buf := make([]byte, new_buf_len)
copy(new_buf, buf[:new_buf_len])
if err != nil {
if err != io.EOF {
log.Printf("error: %s\n", err)
}
}
cb(new_buf)
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
encodedPayload := scanner.Bytes()
// Hex encoding always 2x number of bytes
decoded := make([]byte, len(encodedPayload)/2)
hex.Decode(decoded, encodedPayload)
cb(decoded)
}
conn.Close()
}()
}
}()
+22 -10
View File
@@ -6,23 +6,25 @@ import (
"strings"
)
// InOutPlugins struct for holding references to plugins
type InOutPlugins struct {
Inputs []io.Reader
Outputs []io.Writer
}
type ReaderOrWriter interface{}
// 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]
} else {
return split[0], ""
}
return split[0], ""
}
// Automatically detects type of plugin and initialize it
@@ -45,23 +47,24 @@ func registerPlugin(constructor interface{}, options ...interface{}) {
// Calling our constructor with list of given options
plugin := vc.Call(vo)[0].Interface()
plugin_wrapper := plugin
pluginWrapper := plugin
if limit != "" {
plugin_wrapper = NewLimiter(plugin, limit)
pluginWrapper = NewLimiter(plugin, limit)
} else {
plugin_wrapper = plugin
pluginWrapper = plugin
}
if _, ok := plugin.(io.Reader); ok {
Plugins.Inputs = append(Plugins.Inputs, plugin_wrapper.(io.Reader))
Plugins.Inputs = append(Plugins.Inputs, pluginWrapper.(io.Reader))
}
if _, ok := plugin.(io.Writer); ok {
Plugins.Outputs = append(Plugins.Outputs, plugin_wrapper.(io.Writer))
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)
@@ -95,7 +98,16 @@ func InitPlugins() {
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.outputHTTPHeaders, Settings.outputHTTPMethods, Settings.outputHTTPUrlRegexp, Settings.outputHTTPHeaderFilters, Settings.outputHTTPHeaderHashFilters, Settings.outputHTTPElasticSearch, Settings.outputHTTPUrlRewrite)
registerPlugin(NewHTTPOutput, options, &Settings.outputHTTPConfig)
}
}
+204
View File
@@ -0,0 +1,204 @@
/*
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:], '\r')
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)
}
// 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)
}
+146
View File
@@ -0,0 +1,146 @@
package proto
import (
"bytes"
"testing"
)
func TestHeader(t *testing.T) {
var payload, val []byte
var headerStart int
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")
}
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 :")
}
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))
}
}
+130 -91
View File
@@ -1,141 +1,180 @@
package raw_socket
/*
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 (
"encoding/binary"
"log"
"net"
"strconv"
"bytes"
"encoding/binary"
"log"
"net"
"strconv"
)
// 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
// Listener handle traffic capture
type Listener struct {
messages map[string]*TCPMessage // buffer of TCPMessages waiting to be send
// buffer of TCPMessages waiting to be send
messages map[string]*TCPMessage
c_packets chan *TCPPacket
c_messages chan *TCPMessage // Messages ready to be send to client
// 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
c_del_message chan *TCPMessage // Used for notifications about completed or expired messages
// Messages ready to be send to client
packetsChan chan *TCPPacket
addr string // IP to listen
port int // Port to listen
// Messages ready to be send to client
messagesChan chan *TCPMessage
// Used for notifications about completed or expired messages
messageDelChan chan *TCPMessage
addr string // IP to listen
port int // Port to listen
}
// RAWTCPListen creates a listener to capture traffic from RAW_SOCKET
// NewListener creates and initializes new Listener object
func NewListener(addr string, port string) (rawListener *Listener) {
rawListener = &Listener{}
rawListener = &Listener{}
rawListener.c_packets = make(chan *TCPPacket, 100)
rawListener.c_messages = make(chan *TCPMessage, 100)
rawListener.c_del_message = make(chan *TCPMessage, 100)
rawListener.messages = make(map[string]*TCPMessage)
rawListener.packetsChan = make(chan *TCPPacket, 10000)
rawListener.messagesChan = make(chan *TCPMessage, 10000)
rawListener.messageDelChan = make(chan *TCPMessage, 10000)
rawListener.addr = addr
rawListener.port, _ = strconv.Atoi(port)
rawListener.messages = make(map[string]*TCPMessage)
rawListener.ackAliases = make(map[uint32]uint32)
rawListener.seqWithData = make(map[uint32]uint32)
go rawListener.listen()
go rawListener.readRAWSocket()
rawListener.addr = addr
rawListener.port, _ = strconv.Atoi(port)
return
go rawListener.listen()
go rawListener.readRAWSocket()
return
}
func (t *Listener) 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
delete(t.messages, message.ID)
for {
select {
// If message ready for deletion it means that its also complete or expired by timeout
case message := <-t.messageDelChan:
t.messagesChan <- message
delete(t.ackAliases, message.Ack)
delete(t.messages, message.ID)
// We need to use channels to process each packet to avoid data races
case packet := <-t.c_packets:
t.processTCPPacket(packet)
}
}
// We need to use channels to process each packet to avoid data races
case packet := <-t.packetsChan:
t.processTCPPacket(packet)
}
}
}
func (t *Listener) readRAWSocket() {
conn, e := net.ListenPacket("ip4:tcp", t.addr)
defer conn.Close()
conn, e := net.ListenPacket("ip4:tcp", t.addr)
if e != nil {
log.Fatal(e)
}
if e != nil {
log.Fatal(e)
}
buf := make([]byte, 4096*2)
defer conn.Close()
for {
// Note: ReadFrom receive messages without IP header
n, addr, err := conn.ReadFrom(buf)
for {
buf := make([]byte, 64*1024) // 64kb
// Note: ReadFrom receive messages without IP header
n, addr, err := conn.ReadFrom(buf)
if err != nil {
log.Println("Error:", err)
continue
}
if err != nil {
log.Println("Error:", err)
continue
}
if n > 0 {
t.parsePacket(addr, buf[:n])
}
}
if n > 0 {
go t.parsePacket(addr, buf[:n])
}
}
}
func (t *Listener) parsePacket(addr net.Addr, buf []byte) {
if t.isIncomingDataPacket(buf) {
new_buf := make([]byte, len(buf))
copy(new_buf, buf)
t.c_packets <- ParseTCPPacket(addr, new_buf)
}
if t.isIncomingDataPacket(buf) {
t.packetsChan <- ParseTCPPacket(addr, buf)
}
}
func (t *Listener) isIncomingDataPacket(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
dest_port := binary.BigEndian.Uint16(buf[2:4])
// 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])
// Because RAW_SOCKET can't be bound to port, we have to control it by ourself
if int(dest_port) == t.port {
// Get the 'data offset' (size of the TCP header in 32-bit words)
dataOffset := (buf[12] & 0xF0) >> 4
// Because RAW_SOCKET can't be bound to port, we have to control it by ourself
if int(destPort) == 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
}
}
// 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
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) {
defer func() { recover() }()
defer func() { recover() }()
var message *TCPMessage
m_id := packet.Addr.String() + strconv.Itoa(int(packet.Ack))
var message *TCPMessage
message, ok := t.messages[m_id]
if parentAck, ok := t.seqWithData[packet.Seq]; ok {
t.ackAliases[packet.Ack] = parentAck
delete(t.seqWithData, packet.Seq)
}
if !ok {
// We sending c_del_message channel, so message object can communicate with Listener and notify it if message completed
message = NewTCPMessage(m_id, t.c_del_message)
t.messages[m_id] = message
}
if alias, ok := t.ackAliases[packet.Ack]; ok {
packet.Ack = alias
}
// Adding packet to message
message.c_packets <- packet
mID := packet.Addr.String() + strconv.Itoa(int(packet.SrcPort)) + strconv.Itoa(int(packet.Ack))
message, ok := t.messages[mID]
if !ok {
// We sending messageDelChan channel, so message object can communicate with Listener and notify it if message completed
message = NewTCPMessage(mID, t.messageDelChan, packet.Ack)
t.messages[mID] = message
}
if bytes.Equal(packet.Data[0:4], bPOST) {
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:]...)
}
}
// Adding packet to message
message.packetsChan <- packet
}
// Receive TCP messages from the listener channel
func (t *Listener) Receive() *TCPMessage {
return <-t.c_messages
return <-t.messagesChan
}
+63 -61
View File
@@ -1,12 +1,13 @@
package raw_socket
package rawSocket
import (
"log"
"sort"
"time"
"log"
"sort"
"time"
)
const MSG_EXPIRE = 2000 * time.Millisecond
// MsgExpire specify period that message should wait before it considered as finished
const MsgExpire = 2000 * 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
@@ -15,91 +16,92 @@ const MSG_EXPIRE = 2000 * time.Millisecond
// 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
packets []*TCPPacket
ID string // Message ID
Ack uint32
packets []*TCPPacket
timer *time.Timer // Used for expire check
timer *time.Timer // Used for expire check
c_packets chan *TCPPacket
packetsChan chan *TCPPacket
c_del_message chan *TCPMessage
delChan chan *TCPMessage
}
// NewTCPMessage pointer created from a Acknowledgment number and a channel of messages readuy to be deleted
func NewTCPMessage(ID string, c_del chan *TCPMessage) (msg *TCPMessage) {
msg = &TCPMessage{ID: ID}
func NewTCPMessage(ID string, delChan chan *TCPMessage, Ack uint32) (msg *TCPMessage) {
msg = &TCPMessage{ID: ID, Ack: Ack}
msg.c_packets = make(chan *TCPPacket)
msg.c_del_message = c_del // used for notifying that message completed or expired
msg.packetsChan = make(chan *TCPPacket)
msg.delChan = delChan // 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)
// Every time we receive packet we reset this timer
msg.timer = time.AfterFunc(MsgExpire, msg.Timeout)
go msg.listen()
go msg.listen()
return
return
}
func (t *TCPMessage) listen() {
for {
select {
case packet, more := <-t.c_packets:
if more {
t.AddPacket(packet)
} else {
// Stop loop if channel closed
return
}
}
}
for {
select {
case packet, more := <-t.packetsChan:
if more {
t.AddPacket(packet)
} else {
// Stop loop if channel closed
return
}
}
}
}
// Timeout notifies message to stop listening, close channel and message ready to be sent
func (t *TCPMessage) Timeout() {
select {
// In some cases Timeout can be called multiple times (do not know how yet)
// Ensure that we did not close channel 2 times
case packet, ok := <- t.c_packets:
if ok {
t.AddPacket(packet)
} else {
return
}
default:
close(t.c_packets)
t.c_del_message <- t // Notify RAWListener that message is ready to be send to replay server
}
select {
// In some cases Timeout can be called multiple times (do not know how yet)
// Ensure that we did not close channel 2 times
case packet, ok := <-t.packetsChan:
if ok {
t.AddPacket(packet)
} else {
return
}
default:
close(t.packetsChan)
t.delChan <- t // Notify RAWListener that message is ready to be send to replay server
}
}
// Bytes sorts packets in right orders and return message content
func (t *TCPMessage) Bytes() (output []byte) {
sort.Sort(BySeq(t.packets))
sort.Sort(sortBySeq(t.packets))
for _, v := range t.packets {
output = append(output, v.Data...)
}
for _, v := range t.packets {
output = append(output, v.Data...)
}
return
return output
}
// 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
packetFound := false
for _, pkt := range t.packets {
if packet.Seq == pkt.Seq {
packetFound = true
break
}
}
for _, pkt := range t.packets {
if packet.Seq == pkt.Seq {
packetFound = true
break
}
}
if packetFound {
log.Println("Received packet with same sequence")
} else {
t.packets = append(t.packets, packet)
}
if packetFound {
log.Println("Received packet with same sequence")
} else {
t.packets = append(t.packets, packet)
}
// Reset message timeout timer
t.timer.Reset(MSG_EXPIRE)
// Reset message timeout timer
t.timer.Reset(MsgExpire)
}
+66 -65
View File
@@ -1,100 +1,101 @@
package raw_socket
package rawSocket
import (
"encoding/binary"
"net"
"strconv"
"strings"
"encoding/binary"
"net"
"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
fFIN = 1 << iota
fSYN
fRST
fPSH
fACK
fURG
fECE
fCWR
fNS
)
// Simple TCP packet parser
//
// 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
SrcPort uint16
DestPort uint16
Seq uint32
Ack uint32
DataOffset uint8
Flags uint16
Window uint16
Checksum uint16
Urgent uint16
Data []byte
Data []byte
Addr net.Addr
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
p = &TCPPacket{Data: b}
p.ParseBasic()
p.Addr = addr
return p
return p
}
// Parse TCP Packet, inspired by: https://github.com/miekg/pcap/blob/master/packet.go
func (t *TCPPacket) Parse() {
t.ParseBasic()
t.SrcPort = binary.BigEndian.Uint16(t.Data[0:2])
t.DestPort = binary.BigEndian.Uint16(t.Data[2: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.ParseBasic()
t.DestPort = binary.BigEndian.Uint16(t.Data[2: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])
}
// ParseBasic set of fields
func (t *TCPPacket) ParseBasic() {
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.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:]
t.Data = t.Data[t.DataOffset*4:]
}
// String output for a TCP Packet
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)),
"Acknowledgment:" + strconv.Itoa(int(t.Ack)),
"Header len:" + strconv.Itoa(int(t.DataOffset)),
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&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),
"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)),
"Window size:" + strconv.Itoa(int(t.Window)),
"Checksum:" + strconv.Itoa(int(t.Checksum)),
"Data:" + string(t.Data),
}, "\n")
"Data size:" + strconv.Itoa(len(t.Data)),
"Data:" + string(t.Data),
}, "\n")
}
type BySeq []*TCPPacket
type sortBySeq []*TCPPacket
func (a BySeq) Len() int { return len(a) }
func (a BySeq) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a BySeq) Less(i, j int) bool { return a[i].Seq < a[j].Seq }
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 }
+61 -23
View File
@@ -8,11 +8,27 @@ import (
)
const (
VERSION = "0.9.2"
// VERSION specifies Gor current version
VERSION = "0.9.8"
)
// 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
@@ -29,20 +45,15 @@ type AppSettings struct {
inputRAW MultiOption
inputHTTP MultiOption
outputHTTP MultiOption
outputHTTPHeaders HTTPHeaders
outputHTTPMethods HTTPMethods
outputHTTPUrlRegexp HTTPUrlRegexp
outputHTTPUrlRewrite UrlRewriteMap
outputHTTPHeaderFilters HTTPHeaderFilters
outputHTTPHeaderHashFilters HTTPHeaderHashFilters
outputHTTPElasticSearch string
outputHTTPWorkers int
outputHTTPStats bool
inputHTTP MultiOption
outputHTTP MultiOption
outputHTTPConfig HTTPOutputConfig
modifierConfig HTTPModifierConfig
}
var Settings AppSettings = AppSettings{}
// 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)
@@ -53,7 +64,8 @@ func usage() {
func init() {
flag.Usage = usage
flag.BoolVar(&Settings.verbose, "verbose", false, "Turn on verbose/debug output")
flag.BoolVar(&Settings.verbose, "verbose", false, "Turn on more verbose output")
flag.BoolVar(&Settings.debug, "debug", false, "Turn on debug output, shows all itercepted 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.")
@@ -73,20 +85,46 @@ func init() {
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.Var(&Settings.outputHTTPHeaders, "output-http-header", "Inject additional headers to http reqest:\n\tgor --input-raw :8080 --output-http staging.com --output-http-header 'User-Agent: Gor'")
flag.Var(&Settings.outputHTTPMethods, "output-http-method", "Whitelist of HTTP methods to replay. Anything else will be dropped:\n\tgor --input-raw :8080 --output-http staging.com --output-http-method GET --output-http-method OPTIONS")
flag.Var(&Settings.outputHTTPUrlRegexp, "output-http-url-regexp", "A regexp to match requests against. Anything else will be dropped:\n\t gor --input-raw :8080 --output-http staging.com --output-http-url-regexp ^www.")
flag.Var(&Settings.outputHTTPHeaderFilters, "output-http-header-filter", "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 --output-http-header-filter api-version:^v1")
flag.Var(&Settings.outputHTTPHeaderHashFilters, "output-http-header-hash-filter", "Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific header. The fraction must have a denominator that is a power of two:\n\t gor --input-raw :8080 --output-http staging.com --output-http-header-hash-filter user-id:1/4")
flag.IntVar(&Settings.outputHTTPWorkers, "output-http-workers", -1, "Gor uses dynamic worker scaling by default. Enter a number to run a set number of workers.")
flag.BoolVar(&Settings.outputHTTPStats, "output-http-stats", false, "Report http output queue stats to console every 5 seconds.")
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.StringVar(&Settings.outputHTTPElasticSearch, "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.outputHTTPUrlRewrite, "output-http-rewrite-url", "Rewrite the requst url based on a mapping:\n\tgor --input-raw :8080 --output-http staging.com --output-http-rewrite-url /xml_test/interface.php:/api/service.do")
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 agains 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-diallow-url", "A regexp to match requests against. Filter get matched agains full url with domain. Anything else will be dropped:\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%")
}
// Debug gets called only if --verbose flag specified
func Debug(args ...interface{}) {
if Settings.verbose {
fmt.Printf("[DEBUG][PID %d] ", os.Getpid())
log.Println(args...)
}
}
-44
View File
@@ -1,44 +0,0 @@
package main
import (
"errors"
"fmt"
"net/http"
"regexp"
"strings"
)
type headerFilter struct {
name string
regexp *regexp.Regexp
}
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: valArr[0], regexp: r})
return nil
}
func (h *HTTPHeaderFilters) Good(req *http.Request) bool {
for _, f := range *h {
if !f.regexp.MatchString(req.Header.Get(f.name)) {
return false
}
}
return true
}
-35
View File
@@ -1,35 +0,0 @@
package main
import (
"net/http"
"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:^:$")
}
err = filters.Set("Header3-^$")
if err == nil {
t.Error("Should error on Header2:^:$")
}
req := http.Request{}
req.Header = make(map[string][]string)
req.Header.Add("Header1", "")
req.Header.Add("Header2", ":")
req.Header.Add("Header3", "Irrelevant")
if !filters.Good(&req) {
t.Error("Request should pass filters")
}
}
-68
View File
@@ -1,68 +0,0 @@
package main
import (
"errors"
"fmt"
"hash/fnv"
"net/http"
"strconv"
"strings"
)
type headerHashFilter struct {
name string
maxHash uint32
}
type HTTPHeaderHashFilters []headerHashFilter
func (h *HTTPHeaderHashFilters) String() string {
return fmt.Sprint(*h)
}
func (h *HTTPHeaderHashFilters) 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:1/2).")
}
fracArr := strings.Split(valArr[1], "/")
if len(fracArr) < 2 {
return errors.New("need both a numerator and denominator specified, slash-delimited (ex. user_id:1/4).")
}
var num, den uint64
num, _ = strconv.ParseUint(fracArr[0], 10, 64)
den, _ = strconv.ParseUint(fracArr[1], 10, 64)
if num < 1 || den < 1 || num > den {
panic("need positive numerators and denominators, with the former less than the latter.")
}
for test := den; test != 1; test /= 2 {
if test%2 == 1 {
return errors.New("must have a denominator which is a power of two.")
}
}
var f headerHashFilter
f.name = valArr[0]
f.maxHash = (uint32)(num * (((uint64)(2 << 31)) / den))
*h = append(*h, f)
return nil
}
func (h *HTTPHeaderHashFilters) Good(req *http.Request) bool {
for _, f := range *h {
if req.Header.Get(f.name) == "" {
return false
}
hasher := fnv.New32a()
hasher.Write([]byte(req.Header.Get(f.name)))
if hasher.Sum32() > f.maxHash {
return false
}
}
return true
}
-43
View File
@@ -1,43 +0,0 @@
package main
import (
"net/http"
"testing"
)
func TestHTTPHeaderHashFilters(t *testing.T) {
filters := HTTPHeaderHashFilters{}
err := filters.Set("Header1:1/2")
if err != nil {
t.Error("Should not error on Header1:^$")
}
err = filters.Set("Header2:1/2")
if err != nil {
t.Error("Should not error on Header2:^:$")
}
err = filters.Set("HeaderIrrelevant:1/3")
if err == nil {
t.Error("Should error on HeaderIrrelevant:1/3")
}
req := http.Request{}
req.Header = make(map[string][]string)
req.Header.Add("Header1", "test3414")
if filters.Good(&req) {
t.Error("Request should not pass filters, Header2 does not exist")
}
req.Header.Add("Header2", "test2")
if filters.Good(&req) {
t.Error("Request should not pass filters, Header2 hash too high")
}
req.Header.Set("Header2", "test3414")
if !filters.Good(&req) {
t.Error("Request should pass filters")
}
}
-32
View File
@@ -1,32 +0,0 @@
package main
import (
"errors"
"fmt"
"strings"
)
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
}
-26
View File
@@ -1,26 +0,0 @@
package main
import (
"fmt"
"strings"
)
type HTTPMethods []string
func (h *HTTPMethods) String() string {
return fmt.Sprint(*h)
}
func (h *HTTPMethods) Set(value string) error {
*h = append(*h, strings.ToUpper(value))
return nil
}
func (h *HTTPMethods) Contains(value string) bool {
for _, method := range *h {
if value == method {
return true
}
}
return false
}
-24
View File
@@ -1,24 +0,0 @@
package main
import (
"testing"
)
func TestHTTPMethods(t *testing.T) {
methods := HTTPMethods{}
methods.Set("lower")
methods.Set("UPPER")
if !methods.Contains("LOWER") {
t.Error("Does not contain LOWER")
}
if !methods.Contains("UPPER") {
t.Error("Does not contain UPPER")
}
if methods.Contains("ABSENT") {
t.Error("Does contain ABSENT")
}
}
-16
View File
@@ -1,16 +0,0 @@
package main
import (
"fmt"
)
type MultiOption []string
func (h *MultiOption) String() string {
return fmt.Sprint(*h)
}
func (h *MultiOption) Set(value string) error {
*h = append(*h, value)
return nil
}
-36
View File
@@ -1,36 +0,0 @@
package main
import (
"errors"
"fmt"
"strings"
)
type urlRewrite struct {
src string
target string
}
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).")
}
*r = append(*r, urlRewrite{src: valArr[0], target: valArr[1]})
return nil
}
func (r *UrlRewriteMap) Rewrite(path string) string {
for _, f := range *r {
if f.src == path {
return f.target
}
}
return path
}
-27
View File
@@ -1,27 +0,0 @@
package main
import (
"testing"
)
func TestUrlRewriteMap(t *testing.T) {
var url string
rewrites := UrlRewriteMap{}
err := rewrites.Set("/abc:/123")
if err != nil {
t.Error("Should not error on /abc:/123")
}
url = "/abc"
if rewrites.Rewrite(url) == url {
t.Error("Request url should have been rewritten, wasn't")
}
url = "/wibble"
if rewrites.Rewrite(url) != url {
t.Error("Request url should not have been rewritten, was")
}
}
-30
View File
@@ -1,30 +0,0 @@
package main
import (
"net/http"
"regexp"
)
type HTTPUrlRegexp struct {
regexp *regexp.Regexp
}
func (r *HTTPUrlRegexp) String() string {
if r.regexp == nil {
return ""
}
return r.regexp.String()
}
func (r *HTTPUrlRegexp) Set(value string) error {
regexp, err := regexp.Compile(value)
r.regexp = regexp
return err
}
func (r *HTTPUrlRegexp) Good(req *http.Request) bool {
if r.regexp == nil {
return true
}
return r.regexp.MatchString(req.Host + req.URL.String())
}
-26
View File
@@ -1,26 +0,0 @@
package main
import (
"net/http"
"net/url"
"testing"
)
func TestHTTPUrlRegexp(t *testing.T) {
filter := HTTPUrlRegexp{}
filter.Set("^www.google.com/admin/")
req := http.Request{}
req.Host = "www.google.com"
var err error
req.URL, err = url.Parse("/admin/testpage1")
if !filter.Good(&req) || err != nil {
t.Error("Request should pass filters")
}
req.URL, err = url.Parse("/user/testpage2")
if filter.Good(&req) || err != nil {
t.Error("Request should not pass filters")
}
}
+15 -4
View File
@@ -5,10 +5,12 @@ import (
"encoding/base64"
)
// 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)
@@ -23,24 +25,33 @@ func (i *TestInput) Read(data []byte) (int, error) {
return len(buf), 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\nHost: www.w3.org\r\n\r\na=1&b=2\r\n\r\n")
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")
}
func (i *TestInput) EmitFile() {
size := 1024 * 1024 // 1 MB
// 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 /pub/WWW/ HTTP/1.1\nHost: www.w3.org\r\n\r\n------WebKitFormBoundaryePkpFF7tjBAqx29L\nContent-Disposition: form-data; name=\"uploadedfile\"; filename=\"hello.o\"\nContent-Type: application/x-object\n" + rs + "\r\n\r\n")
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")
}
+2
View File
@@ -2,10 +2,12 @@ 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