Compare commits

...
821 Commits
Author SHA1 Message Date
Leonid Bugaev f6287727d2 Move memory limiting to capture level 2021-05-18 16:11:03 +03:00
Leonid Bugaev a940bad19e Updates 2021-05-17 18:07:39 +00:00
Leonid Bugaev 32c005a63d Add handling for out of order packets 2021-05-03 21:41:11 +03:00
Leonid Bugaev c7d04738c1 Do not use empty bpf filter when tracking response
Should have way better performance, and can fix a few bugs
2021-05-03 14:34:29 +03:00
Leonid Bugaev 01f5a1c9b5 Remove variable ticker functionality
Too smart, and can cause to unexpected errors.
2021-05-02 21:29:12 +03:00
Leonid Bugaev c641e62786 Fix comment 2021-05-02 21:17:26 +03:00
Leonid Bugaev fcad7155f2 Refactoring/renaming 2021-05-02 21:14:46 +03:00
Leonid Bugaev 257d4d36bd Refactoring 2021-04-29 18:49:56 +03:00
Urban Ishimwe 6cb06f97ca global message timer 2021-04-25 21:05:09 +02:00
Urban Ishimwe 007dcb40c9 fix hasChunked 2021-04-11 23:22:30 +02:00
Ankit Dobhal 8f14d5b178 Fixed bug risk and antipattern using deepsource (#904)
## Description

Hi 👋 I ran the [DeepSource](https://deepsource.io/) static analyzer on the forked copy of this repo and found some [interesting code quality issues](https://deepsource.io/gh/ankitdobhal/goreplay/issues/). This PR fixes a few of them.

## Summary of Fixes

- Added .deepsource.toml to fix bug risks
- Empty string test can be improved
- Nested if can be replaced with else-if

## Type of change

- [✔] Antipattern
- [ ] New feature (non-breaking change which adds functionality)

## Checklist:

- [✔] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
2021-02-23 20:15:24 +03:00
Urban Ishimwe 5e27c92f80 fix middlewares bugs: (#887)
bugs include:
- middleware doesn't send message meta
- send empty request when prettifier is off
- handle shutdown gracefully
- disable debug in example middlewares during test
2021-01-25 16:08:57 +03:00
Romain Masclef dfbaf2c808 Add githubAction to build & push docker image on github release published event. (#881)
fixes #880 👍 

Update Dockerfile
- fix alpine image version
- add RELEASE_VERSION build arg

Add githubAction to build & push docker image on github release published event
- add github workflow file
- add dependabot file to automatically propose PRs on githubActions version update*

> ⚠️ *Note1: you will have to add `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` as github secrets in order for the 
> ⚠️ *Note2: github dependabot support must be enabled in order for this action to work
githubAction to be able to login to the dockerHub and push images on it 👍
> *Note3:  we can add other dependabot entries in order to update Go modules and Dockerfile base images 👍 (i.e `FROM` parts)
2021-01-15 21:38:27 +03:00
Urban Ishimwe 51db961942 fix-#875 (#882)
fix #875
2021-01-15 21:37:22 +03:00
Urban Ishimwe 790545042b migrate test CI to github actions (#883)
added test integration with GitHub actions and removed Travis CI
2021-01-15 21:36:26 +03:00
jl2005 cd28444b0d assign initial value to buf (#884)
Fix prettify functionality
2021-01-15 21:35:50 +03:00
Urban Ishimwe a7aa5ac5b3 fix #887 (#878) 2021-01-05 22:47:22 +03:00
Urban Ishimwe a526f3a5f1 lazy packet parser (#870)
benchmarks of packet parser with `-cpu=1` packet (IPv6 with 2 extension header)
master:
```
337463	      3300 ns/op	    1624 B/op	      24 allocs/op
```
current:
```
2014885       576 ns/op	            384 B/op	       3 allocs/op
```
2020-12-21 19:41:21 +03:00
Urban Ishimwe 8dbec6706d Merge pull request #864 from two/master
Update output_http.go (fix #862)
2020-12-18 11:20:11 +02:00
Sean Chen 92aabc5328 fix #862 lost replay uri 2020-12-08 13:37:49 +08:00
Sean Chen 8e110cee7b fix #862 lost replay uri 2020-12-08 13:37:34 +08:00
Urban Ishimwe 87bdb4da00 fix kafka assignment to nil map (#867)
fixes #866
2020-12-07 21:07:06 +03:00
Sean Chen d677218053 Update output_http.go
judge the `req.URL` by `req.RequestURI`
2020-12-04 20:27:38 +08:00
Urban Ishimwe f6a4170615 refine HTTP1 end hint (#863)
fixes #859
2020-12-04 11:27:02 +03:00
Urban Ishimwe a84898ce8b remove unsafe converison (#865) 2020-12-04 11:26:37 +03:00
Sean Chen bc87707d16 Update output_http.go
make the copy traffic url equal to original request url
2020-12-04 10:12:12 +08:00
Leonid Bugaev 40a3c51c9c Fix Makefile 2020-11-22 20:10:00 +03:00
Urban Ishimwe 5e2bbf8228 tcp.message uuid fix (#855)
the current UUID generator runs at:
```
BenchmarkMessageUUID 	13599342	        87.7 ns/op	      16 B/op	       0 allocs/op
```
 with the former version that was running on
```
BenchmarkMessageUUID 	 2818203	       427 ns/op	      96 B/op	       2 allocs/op
```

it fixes #842 #851
2020-11-16 15:01:06 +03:00
Urban Ishimwe a3b6be8b82 test with race detector (#852)
* middleware tests

* test with race detector
2020-11-16 15:00:50 +03:00
Urban Ishimwe 3aec926ad9 Merge pull request #845 from buger/change-plugins-interface
changes plugins reader and writer method
2020-11-06 19:21:52 +02:00
Urban Ishimwe ec4dbf2061 appropriate emitter close 2020-11-06 17:35:33 +02:00
Urban Ishimwe 6d812ceb7f changes plugins reader and writer method
// PluginReader is an interface for input plugins
type PluginReader interface {
	PluginRead() (msg *Message, err error)
}

// PluginWriter is an interface for output plugins
type PluginWriter interface {
	PluginWrite(msg *Message) (n int, err error)
}
2020-11-02 06:15:10 +02:00
Urban Ishimwe 20435af11b Merge pull request #839 from davidFR/master
Fix #838 : Kafka in TLS (not only mutual TLS)
2020-11-02 06:14:19 +02:00
David Fradin f719071358 Update kafka.go
Correct invalid character
2020-10-20 16:16:06 +02:00
David FradinandUrban Ishimwe 716fb72e27 Update kafka.go
Co-authored-by: Urban Ishimwe <urbainishimwe@gmail.com>
2020-10-20 16:11:26 +02:00
David FradinandUrban Ishimwe 3fcdb6f1bc Update kafka.go
Co-authored-by: Urban Ishimwe <urbainishimwe@gmail.com>
2020-10-20 16:11:19 +02:00
David FradinandUrban Ishimwe 715ce88a63 Update kafka.go
Co-authored-by: Urban Ishimwe <urbainishimwe@gmail.com>
2020-10-20 16:11:07 +02:00
David FRADIN bcf81c6410 Kafka TLS 2020-10-19 13:57:34 +02:00
David Fradin 358cb78a53 Fix #835: kafka bug (#836)
Correction of error introduce in PR #800
2020-10-19 10:56:07 +03:00
Urban Ishimwe 48aa326ba6 engine benchmarks (#837) 2020-10-19 10:55:47 +03:00
Urban Ishimwe 5d8ca525a4 deprecate output-http non-compatible clients (#833)
for easy readability check changes by their underlying commits!

these benchmarks address the whole operation of the request cycle in goreplay.

_**goos: linux
goarch: amd64**_

**Using Compatible client:**

```
BenchmarkHTTPOutput-4      	   10417	    118969 ns/op	   12172 B/op	      93 allocs/op
BenchmarkHTTPOutputTLS-4   	    9136	    132929 ns/op	   12448 B/op	      97 allocs/op
```

**Using non-compatible client**
```
BenchmarkHTTPOutput-4      	     859	   1175040 ns/op	   15598 B/op	      46 allocs/op
BenchmarkHTTPOutputTLS-4   	     880	   1189643 ns/op	   15544 B/op	      52 allocs/op

```
Binary size reduced: **7%**

from these benchmarks, we may trade allocations with performance and memory!
2020-10-13 08:36:16 +03:00
Urban Ishimwe 3635d66a76 Merge pull request #818 from DimaGolomozy/increase-latency-delta
increase-latency-delta-tests
2020-10-11 21:33:37 +02:00
Urban Ishimwe a4e78cf22c Merge pull request #830 from buger/tcp-output-distribute-workers
Tcp output distribute workers fairly in non-sticky env
2020-10-07 09:38:54 +02:00
Urban Ishimwe 8aedc15027 Merge pull request #827 from tomerf/syscall_error_check
Fixed check of syscall errors
2020-10-05 21:59:52 +02:00
Urban Ishimwe 30d66d31de distribute tasks in workers 2020-09-30 14:38:21 +02:00
Urban Ishimwe b1d7c4134b debug on stderr 2020-09-30 14:01:05 +02:00
Tomer Froumin 66e680a56f Fixed check of syscall errors 2020-09-29 10:50:22 +03:00
Urban Ishimwe a4ed67acd6 Merge pull request #826 from buger/fix-buf-has-http-title
fix #825
2020-09-25 09:21:34 +02:00
Urban Ishimwe ee9ff8163b fix #825 2020-09-24 15:39:20 +02:00
Urban Ishimwe 9519b9a9f5 Reduce allocation and unnecessary layers (#822)
The focus here was to **reduce allocation in TCP parser** but speed may have hopeful improved too!
pool no longer use map's key of **string** it uses **uint64**
**Benchmarks** was revamped to be more clear
if you want to compare these results copy the benchmark in tcp/bench_test.go@reduce-allocation to tcp/bench_test.go@master:

**before(master)**:
```
BenchmarkPacketParseAndSort-4         	 1000000	      1006 ns/op	      64 B/op	       2 allocs/op
BenchmarkMessageParserWithoutHint-4   	     625	   1772309 ns/op	      1000 packets/op	  419096 B/op	   10045 allocs/op
BenchmarkMessageParserWithHint-4      	      74	  14969926 ns/op	      1000 chunks/op	      1002 packets/op	  450992 B/op	   10126 allocs/op
```

**After(this branch)**:
```
BenchmarkPacketParseAndSort-4         	 1267662	       941 ns/op	      64 B/op	       2 allocs/op
BenchmarkMessageParserWithoutHint-4   	    2256	    523474 ns/op	      1000 packets/op	  243530 B/op	    1037 allocs/op
BenchmarkMessageParserWithHint-4      	      80	  13990955 ns/op	      1000 chunks/op	      1002 packets/op	  268609 B/op	    1099 allocs/op

```
2020-09-22 21:14:31 +03:00
Tomer Froumin 3401dfbf2d Make the number of TCP connections configurable (#819) 2020-09-22 21:13:52 +03:00
Urban Ishimwe cfa14b0a32 Merge pull request #824 from swills/size_int64
Use int64 for Size
2020-09-21 19:53:29 +02:00
Steve Wills 85065cb41a Use int64 for Size 2020-09-21 13:43:02 -04:00
Urban Ishimwe 7100ac3593 fix add header out of bounds (#820)
this fixes the issue that arises in adding an HTTP header in an invalid HTTP request(request without CRLF)
2020-09-17 15:16:48 +03:00
Dima Golomozy d5184c6cbd faster latency is not a problem 2020-09-17 10:22:25 +03:00
karanssj4andLeonid Bugaev 4d7d4671c4 Update Dockerfile to work with v1.2.0 (#803)
* Update Dockerfile to work with v1.1.0

* Update Dockerfile

Co-authored-by: Leonid Bugaev <leonsbox@gmail.com>
2020-09-17 09:56:05 +03:00
Tomer Froumin 8c668d252c Added flag to skip hostname verification (#814)
Allow to configure the output to ignore TLS certificates that doesn't match the hostname.
This is necessary when connecting using an IP address instead of a domain or using a self signed certificate.
2020-09-17 09:54:22 +03:00
Dima Golomozy 887293230b increase latency delta 2020-09-16 19:31:58 +03:00
Tomer Froumin 2b73ea1f0c Make makefile release commands reentrant and the move the binary name… (#815)
Improved the Makefile:
* Fixed release-* commands fail when running the second time
* Added command to create only the binary executable on mac
* Moved binary name to a variable
* Fixed hardcoded container name
2020-09-16 18:34:58 +03:00
Urban Ishimwe 66c333656c added current version and updated travis (#816)
made version to be `1.2.0`  and modified travis-ci to make test results more predictable
2020-09-16 18:34:27 +03:00
Urban Ishimwe f11c65889e create interface for socket (#812)
This is to make socket implementations independent of the host OS.
this is done by creating an interface that define all required behaviors of the socket.
2020-09-11 09:16:21 +03:00
Urban Ishimwe 5de9830f9d Merge pull request #810 from buger/afpacket_linux_only
fix #809 and minor bug in pool
2020-08-30 13:02:52 +02:00
Urban Ishimwe 48150e53ba make syn work with hint 2020-08-30 12:16:24 +02:00
Urban Ishimwe f9b0ae6d55 fix #809 and minor bug in pool 2020-08-30 12:07:00 +02:00
Urban Ishimwe df6c4e8d02 Merge pull request #807 from buger/raw_socket_impls
implements af_packet socket
2020-08-28 08:35:47 +02:00
Urban Ishimwe 571487b135 dispatch on rst flag 2020-08-26 17:48:17 +02:00
Urban Ishimwe 83174c0fd4 fix duplicate packets raw_sockets 2020-08-24 18:36:22 +02:00
Urban Ishimwe dcef7a6eb7 Merge branch 'master' into raw_socket_impls 2020-08-23 22:46:31 +02:00
Urban Ishimwe a03eb92c1e implements af_packet socket 2020-08-23 22:42:18 +02:00
Urban Ishimwe 9a2b763e31 Merge pull request #800 from davidFR/feature/kafka_tls
Add Mutual TLS config for Kafka
2020-08-18 14:21:11 +02:00
Urban Ishimwe 4e1da0dc98 Merge pull request #802 from luweimy/fix-libpcap-spelling
Fix `EngineType` libpcap spelling
2020-08-18 14:17:34 +02:00
welu c0ee094317 Fix libpcap spelling 2020-08-18 19:58:28 +08:00
David Fradin a2bbbb18ef Merge branch 'master' into feature/kafka_tls 2020-08-12 14:44:51 +02:00
Urban Ishimwe fdc8b094f0 Benchmarking, packaging, and fix issues, tests and perfomance (#797)
### performance
- handling of the very big packet(any size that can be buffered)
- speeding up TCP sessions by using message hints: Added **proto.HasFullPayload** that helps to validate the entire HTTP request, it supports `Chunked` encoding too! Added **proto.HasRequestTitle** and **proto.HasResponseTitle** for validating the beginning of HTTP request. Those methods are used `input_raw.go` with `TCP`.
- supports Keep-Alive: the above functions helps to support keep-alive

### Packaging
- **capture:** engines(capture/doc.go)
- **tcp:** tcp message parser (tcp/doc.go)

### benchmarking
- **capture.BenchmarkPcapDump:** the benchmarks regarding dumping packets in a pcap file
- **capture.BenchmarkPcapFile:** the benchmarks of reading packets from a pcap file
- **capture.BenchmarkPcap:** the benchmarks of parsing packets from the loopback interface with pcap handles
- **proto.BenchmarkHasFullPayload:**: benchmarking this function which validates the HTTP payload
- **tcp.BenchmarkPacketParseAndSort:** benchmarks of parsing and sorting packets
- **tcp.BenchmarkMessageParserWithoutHint:** benchmarks of message reasembling by using `SYN` and `FIN` flag
- **tcp.BenchmarkMessageParserWithHint:** benchmarks of message reasembling by using `proto.HasRequestTitle` and `proto.HasFullPayload` flag

### issues
see linked issues

###  tests
- fixed input raw and engine tests

**Most of the changed of the files, was about using functionalities of** `tcp` **and** `capture` **in existing functionalities**
2020-08-11 12:44:53 +03:00
David FRADIN f97f2ba5f3 Add Mutual TLS config for Kafka input and ouput 2020-08-06 13:26:36 +02:00
Arijit Das 2f81fba370 Merge pull request #792 from buger/flag-mapping
Map Appsetting to flag names.
2020-07-25 00:52:04 +05:30
arijitad 8e7cf658b2 Merge remote-tracking branch 'origin/master' into flag-mapping 2020-07-23 10:06:23 +05:30
arijitad 01b6f1129c Restructre AppSetting and address comments. 2020-07-23 10:00:16 +05:30
Urban Ishimwe 3da60ed4b6 Merge pull request #796 from sonmezonur/prettify_http_usage_fix
Fix typo in usage text of --prettify-http flag
2020-07-17 17:31:05 +02:00
Onur Sonmez 8593af3e8a Fix typo in usage text of --prettify-http flag 2020-07-17 17:45:06 +03:00
Urban Ishimwe 99f44a6718 Merge pull request #791 from suiwenfeng/master
fix wrong param value updated while --http-set-param
2020-07-15 15:26:34 +02:00
arijitad 80b19f918b Map Appsetting to flag names. 2020-07-15 00:14:38 +05:30
suiwenfeng aaf8c81c91 fix wrong param value updated while --http-set-param 2020-07-14 22:37:04 +08:00
Leonid Bugaev 839b41f91c Fix scripts to build packages, and MacOS pkg, and remove java dependency
Plus updated version.go
2020-07-06 14:51:54 +03:00
Urban Ishimwe 5cbb5ea85f updating modules (#778)
the major purpose of this PR is to update modules to their recent **minor version**.
this is helpful in terms of having update functionalities and fixing some issues caused by non-up-to-date modules.
2020-06-24 17:19:19 +03:00
Romanenko Alexey ea822d83da Correct size for gz file (#777)
Fix #775 

When we are using .gz for output we are counting incorrect file size. Now it should be ok
2020-06-23 18:11:45 +03:00
gavinshark 3dff07107a bugfix:fix the bug that middleware will return illegal http body when http body is empty (#737)
If the http body is empty. The httpBody function will return HTTP header content from index 3. This is unexpected.
Function caller also cannot tell whether the httpbody is empty or not.
2020-06-22 14:53:49 +03:00
Urban Ishimwe c20b2734dc Merge pull request #771 from buger/pro-merge
Pro merge
2020-06-16 22:53:43 +02:00
Urban Ishimwe 39b1f80457 Merge branch 'master' into pro-merge 2020-06-16 22:43:08 +02:00
Arijit Das 8462d5d09f Merge pull request #769 from buger/arijitAD/refactor_emitter
Refactor emitter.go and fix test accordingly.
2020-06-15 07:59:57 +05:30
Urban Ishimwe a7c5788052 Merge branch 'master' into pro-merge 2020-06-13 11:39:51 +02:00
Urban Ishimwe a63ce611df Merge pull request #768 from buger/optimize-raw-socket
make raw socket usable and minor optimization.
2020-06-12 10:58:21 +02:00
Arijit Das f89c52445b Fix test. 2020-06-12 00:29:12 +05:30
Arijit Das 60d949a243 Fix test. 2020-06-12 00:02:36 +05:30
Arijit Das 9d5caade25 Minor revert. 2020-06-11 21:15:37 +05:30
Arijit Das 28688e29e8 Fix test. 2020-06-11 21:13:37 +05:30
Arijit Das e9bbc252e0 Fix input raw test and go fmt. 2020-06-11 20:34:09 +05:30
Arijit Das 73e26d48d9 Fix CI comments. 2020-06-11 19:24:38 +05:30
Arijit Das 1c87a339a5 Address comments. 2020-06-11 19:21:31 +05:30
Urban Ishimwe 312d9e8b7d optimizing raw socket 2020-06-08 16:22:04 +02:00
Leonid Bugaev 540e42b9d4 Add more PRO protection 2020-06-08 14:35:38 +03:00
Leonid Bugaev 15bd2608f7 Fix Makefile 2020-06-08 14:34:19 +03:00
Leonid Bugaev d67bc13db3 Merging PRO functionality back to OSS repo 2020-06-08 14:29:32 +03:00
Leonid Bugaev ab2ba8400d Improve S3 debugging 2020-06-06 17:07:51 +03:00
Arijit Das db28858804 Refactor emitter.go and fix test accordingly. 2020-06-06 16:00:43 +05:30
Urban Ishimwe 9d71900ad9 listener tests fix 2020-06-06 10:39:06 +02:00
Urban Ishimwe 0b437716be make raw socket usable, minor optimazation and fixed tests.
- ListenPacket can now capture request(non-multicast)
2020-06-06 09:54:25 +02:00
Romanenko Alexey 932c894b6b Merge pull request #767 from slimus/540-clean-code
clean code with #540
2020-06-06 14:15:53 +07:00
Romanenko Alexey 73b1621656 Merge pull request #765 from slimus/fix-flag-settings
Settings parse and some typo fixes
2020-06-06 14:15:31 +07:00
Alexey Romanenko b1ab61f2b2 clean code with #540 2020-06-05 23:05:58 +07:00
Alex Romanenko 080240ca9f add default values for settings 2020-06-05 15:05:26 +07:00
Alex Romanenko b0f3048876 add default setting to emitter_test 2020-06-05 14:16:53 +07:00
Alexey Romanenko 261c592b83 fix parse settings; fix typo 2020-06-04 23:20:23 +07:00
Arijit Das 766cb77410 Merge pull request #764 from buger/fix-race-fileinput
Fix race input_file.go while closing the file and remove dead code.
2020-06-04 17:02:15 +05:30
Arijit Das e1bf25682f Minor changes. 2020-06-03 22:41:58 +05:30
Arijit Das 039d7c3f26 Fix race input_file.go while closing the file and remove dead code. 2020-06-03 22:35:47 +05:30
Romanenko Alexey 71387ffc90 Merge pull request #745 from relunctance/master
Fixed Wrong parameters entered by user (#703)
2020-06-03 11:00:59 +07:00
Romanenko Alexey dbead6b428 Update input_raw.go 2020-06-03 10:52:44 +07:00
Romanenko Alexey 2fb9b2054e fix #730 (#763)
libpcap has a bug if timeout more than 1 second. We should use 1 second by default for mac os x before it will fixed
2020-06-02 20:27:48 +03:00
Arijit Das 17f7c3c74b Merge pull request #759 from buger/graceful-goroutine-close
Wait for goroutines to close in emitter.go.
2020-06-02 22:57:14 +05:30
Arijit Das 8c1a194a51 Addressed comment. 2020-06-02 22:54:02 +05:30
Romanenko Alexey 857bef93f0 Merge pull request #761 from slimus/update-travis-go-version
Update go version in travis environment
2020-06-02 22:12:50 +07:00
Arijit Das aa1eb9f8b7 Address comment. 2020-06-02 12:20:19 +05:30
Alexey Romanenko 7925dbb0b1 update go version in travis environment 2020-06-01 21:54:50 +07:00
Arijit Das c1f14e8cfb Add comment. 2020-06-01 15:19:32 +05:30
Arijit Das 21e0a2f8c2 Close channel once. 2020-06-01 15:18:13 +05:30
Arijit Das 803257c710 Timeout blocking read call. 2020-06-01 15:07:11 +05:30
Arijit Das 28899ea1e1 Wait for go routine to close in emitter.go. 2020-06-01 01:14:47 +05:30
Romanenko Alexey 263108976a Merge pull request #758 from buger/mod-tidy
mod tidy and higher version of Go
2020-05-29 17:11:28 +07:00
Urban Ishimwe 92a8bfb27c mod tidy and higher version of Go
as Go, is moving towards v1.15 and higher we may
consider starting to compile our application with go1.14 and higher
as go1.14 promises a lot more than g01.12 with a lot of fixes!
2020-05-29 09:22:11 +02:00
Dima Golomozy 5c5ef3ac15 TCP output with sticky connection (#746)
An option to send the same request & response to the same output-tcp connection
2020-05-27 21:09:39 +03:00
Romanenko Alexey 2d78f195d2 Make plugin not global. Revert last-chunk bytes to correct value (#755)
Make Plugins variable not global to fix concurrency in test
Revert last-chunk in chunked transfer coding to correct value (https://tools.ietf.org/html/rfc2616#section-3.6.1) fyi @bruce34
2020-05-27 21:03:40 +03:00
Arijit Das df14b29e74 Fix race when writing to the file. (#756)
Reproduce: Run TestInputFileWithGET with -race flag
#755 needs to go before this is merged.
2020-05-27 16:52:48 +03:00
Romanenko Alexey cad7f37c98 Merge pull request #682 from hosamaly/update-dockerfile
Update Dockerfile to work with v1.0
2020-05-27 10:58:57 +07:00
Urban Ishimwe 7e90a73b81 parse buffer from different bases and data unit: (#754)
All flags that expect buffer as input i.e. `--output-file-size-limit`, `--output-file-max-size-limit`, `--copy-buffer-size` and `input-raw-buffer-size` can now parse  inputs from differents bases and data units like: `10mb`, `10kb`, `100gb`,   `18tb`, `11839023`.... 
data units and bases are case insensitive, the parser accepts only the format of [Go integer literals](https://golang.org/ref/spec#Integer_literals)
2020-05-26 18:05:59 +03:00
Romanenko Alexey 1a5ac70c24 check parseURI error for elasticsearch init (#752)
return error if it occurred when we parse elasticsearch url
2020-05-26 18:04:38 +03:00
Romanenko Alexey 11bd20f251 fix tests; fix build for dev (#751)
* fix golang version in Dockerfile
* remove webupd8team launchpad
* add install default java (jre and jdk)
* fix unit tests
2020-05-21 13:59:46 +03:00
Leonid Bugaev 40d57cac36 Create .clabot 2020-05-15 07:03:40 +03:00
Gao.QiLin 9cd5ad719b Fixed Wrong parameters entered by user (#703)
Friendly tips can help users avoid this problem
2020-04-16 21:29:30 +08:00
Leonid Bugaev fa56e7644c More verbose S3 logging 2020-03-06 18:04:16 +03:00
frank-ding ffc1a09ca1 fix: specified multi input, but only enable the last one (#700)
capture traffic on more than one port(e.g. --input-raw :80 --input-raw :81 --input-raw :82), but only listens on the last specified port"
2019-11-01 17:00:47 +03:00
Leonid Bugaev ae4a6259c2 Improve benchmark 2019-09-16 18:20:49 +03:00
Hosam Aly 71eeef5ced Update Dockerfile to work with v1.0 2019-07-01 14:14:35 +01:00
kleug 7ce4fc50ed Update README (#677)
very little detail but 
at line 7 the argument
>--output-http-track-respose
didn't exist and should be 
>--output-http-track-response
2019-06-11 19:10:28 +03:00
Leonid Bugaev df143cdeec Update to latest upstream master 2019-05-18 10:36:46 +03:00
Leonid Bugaev 64cf9e5509 Merge remote-tracking branch 'upstream/master' 2019-05-13 20:47:06 +03:00
Leonid Bugaev daddbb9c42 Merge branch 'swills-gomod' 2019-05-13 20:40:08 +03:00
Leonid Bugaev 34954eb8e8 Use vendoring 2019-05-13 20:38:06 +03:00
Leonid Bugaev 81e8f98ab6 Merge remote-tracking branch 'upstream/master' 2019-05-13 20:20:06 +03:00
Steve Wills dfae555381 try to fix ci 2019-05-03 10:07:13 -04:00
Steve Wills f7e01997a2 convert to gomod 2019-05-03 09:49:18 -04:00
Leonid Bugaev 36ca809e4a Add commands to generate rpm/deb 2019-04-02 10:21:59 +07:00
Leonid Bugaev 107740b57d Update README.md 2019-03-30 21:30:06 +07:00
Leonid Bugaev 7300b83894 Merge remote-tracking branch 'upstream/master' 2019-03-29 22:50:04 +07:00
Leonid Bugaev c48d07d2c6 Merge remote-tracking branch 'upstream/master' 2019-03-29 22:45:27 +07:00
Leonid Bugaev a8cfaa7581 Add --output-http-compatibility-mode
Add support for using default Go HTTP client
2019-03-10 10:29:28 +01:00
Leonid Bugaev 707b311456 Add deleteHttpCookie method (#658)
Version bumped to 1.0, together with upcoming GoReplay release
2019-03-07 22:45:17 +01:00
EhsanKhaki b37fe727b4 Typos and fixes (#657) 2019-03-07 22:23:50 +01:00
Matt Robenolt f6b0d5d794 Support for HTTP(S) Proxy (#655)
Works like with standard Go client
2019-02-23 10:46:53 +01:00
Leonid Bugaev e3f3348105 Update gopacket 2019-02-16 10:13:03 +01:00
Cole Kettler 60e150de67 Fix golint URL per https://github.com/golang/lint/issues/415 (#618) 2019-02-16 09:55:34 +01:00
igorljubuncic c092718ee6 Add support for building snaps (#625) 2019-02-16 09:51:54 +01:00
bruce34 84e99a8bb0 Fix array out of bounds during using http-set-header (#627)
Happens for example when payload stops at a '-' and we are searching for
Accept-Encoding.The method has been simplified and now does full case
insensitivity. Performance checking, on x64, it is ever so slightly
faster than the original.
2019-02-16 09:49:29 +01:00
bruce34 279aa3fc3f More robust chunked encoding finished check. (#628) 2019-02-16 09:41:10 +01:00
Leonid Bugaev 408f1d0f31 Merge branch 'bruce34-b34-pcap-immediate-mode' 2019-02-16 09:40:18 +01:00
Leonid Bugaev f57703ee5c Merge branch 'b34-pcap-immediate-mode' of https://github.com/bruce34/goreplay into bruce34-b34-pcap-immediate-mode 2019-02-16 09:40:02 +01:00
Leonid Bugaev 264d8cf00e Add more error handling 2019-02-16 09:37:00 +01:00
bruce34 6f70a939f4 Add setting for copyBufferSize, defaults to 5 Mbytes (#630) 2019-02-16 09:34:53 +01:00
bruce34 c693938ef9 Allow a range of output-http-workers with min and max. (#631)
This provides the ability to have a maximum number of workers, yet still allow scaling down. The scaling down is important to stop timeouts happening with under-utilised workers.
Note, although the code has workers_min, workers_max, the settings option --output-http-workers has remained and becomes the "max" value. I did this to hopefully not break too much for others. The min will default to 1, so users will see dynamic scaling unless they also set min to be the same. This will probably benefit them as they will avoid potential timeouts when the traffic reduces. But something to be aware of.
2019-02-16 09:32:56 +01:00
bruce34 267d6d0e2b Add adjustable output-http-stats-ms (#633)
This adds an option to log output-http-stats at a period desired.
2019-02-16 09:29:51 +01:00
bruce34 e9a5cc19b5 Configurable http_output queue length (#634)
Makes sense!
2019-02-16 09:27:44 +01:00
bruce34 d8cace421e Configurable override snaplen to 64k for virtualized environments (#637) 2019-02-16 09:26:39 +01:00
Miquel B bf98a636c4 Using scratch image to execute goreplay (#619)
We could minimize the docker image final size using scratch image
2019-02-16 09:17:11 +01:00
bruce34 476ed808f9 Bugfix index out of bounds (\r\n\r\n split over two packets) (#632) 2019-02-16 09:03:17 +01:00
Leonid Bugaev ec3f104c8c Add way to dynamically profile
Added new `--http-pprof` option, example: "--http-pprof :8181"
It starts web server on given address, and expose special /debug/pprof
endpoint

See https://golang.org/pkg/net/http/pprof/
2019-02-16 08:59:43 +01:00
Leonid Bugaev 85c6070775 File output now case limit by size
Added new `output-file-max-size-limit` option
2019-02-16 08:49:31 +01:00
Leonid Bugaev b4a2c018c1 Fix request filtering issue
https://github.com/buger/goreplay/issues/648
2019-01-25 20:00:22 +01:00
Leonid Bugaev 3fa4a8feb7 Merge remote-tracking branch 'upstream/master' 2019-01-24 23:54:26 +01:00
Leonid Bugaev fb3870d68c Fix --http-allow-header to filter non existent headers
Fix https://github.com/buger/goreplay/issues/604
2019-01-24 23:47:14 +01:00
Leonid Bugaev 6eea02ee86 Add Demo mode 2019-01-24 23:37:53 +01:00
Leonid Bugaev 962087065a S3 fixes 2019-01-22 23:08:23 +01:00
elad 7b0f81b82f Keep headers request during redirects (#645)
During redirect, send original request headers payload, just with modified location.
2019-01-20 20:14:37 +01:00
Vladimir Dubovik 633cfd5fb4 support value with "=" in httpCookie() 2019-01-07 19:59:50 +01:00
bruce34 a5a3012347 Allow commons tgzs to be cached 2019-01-03 23:06:30 +03:00
bruce34 393eb74f9c Add setting for pcap immediate mode to fix timeouts waiting for packets 2018-12-17 16:24:51 +00:00
Leonid Bugaev 166c8e8d87 Merge remote-tracking branch 'upstream/master' 2018-08-24 16:35:39 +05:00
Leonid Bugaev 52ed8b4d1e Add protection for malformed records 2018-08-24 16:35:04 +05:00
Leonid Bugaev c8cf370b62 Merge remote-tracking branch 'upstream/master' 2018-08-10 21:35:19 +05:00
Leonid Bugaev 29139e9eda Fix prefixer panic when body is too small 2018-08-10 21:34:00 +05:00
Leonid Bugaev d0d1527d9b Merge remote-tracking branch 'upstream/master' 2018-08-08 22:00:20 +05:00
Leonid Bugaev c63f2a8113 Remove java from dev docker 2018-08-08 20:47:47 +05:00
myzhan 9fd7a46e7e FIX: miscalculation of the mean value 2018-08-08 17:23:33 +05:00
chainhelen f85bb29327 remove unused "copy" 2018-08-06 11:44:33 +05:00
Leonid Bugaev 0ffe3edcbf Update S3 lib and add more debugging 2018-08-02 21:54:37 +05:00
Leonid Bugaev a97c04b9e2 Use must instead of new 2018-08-02 21:51:13 +05:00
Leonid Bugaev 61301e78b1 Fix merge 2018-08-02 21:44:53 +05:00
Leonid Bugaev eeb09a1494 Merge 2018-08-02 21:42:45 +05:00
Leonid Bugaev 68082bfb11 Fix typo 2018-08-02 21:41:36 +05:00
Leonid Bugaev 28d646d118 Merge branch 'master' of https://github.com/buger/gor 2018-08-02 21:39:42 +05:00
Leonid Bugaev c93ff3598c Fix prettifier buffer overflow 2018-08-02 21:36:47 +05:00
Scott Albertson 0bc3fba5d0 Add a "Reviewed by Hound" badge 2018-08-01 13:28:52 +05:00
Alex Zvorygin 65fd1983b1 Fix broken link to goreplay.com -> goreplay.org 2018-07-13 09:09:33 +05:00
Alex Zvorygin 88972ff301 fix typo in ruby middleware example
continiously -> continuously
2018-07-04 13:55:58 +05:00
Samuel Gulliksson 85a7bc1e9a Fix typo in Python middleware example. 2018-06-28 09:18:26 +05:00
Alex Zvorygin 36a8845ffe Fix typo 2018-06-28 09:18:02 +05:00
Leonid Bugaev 9eb6ab262b Add "deleteHttpHeader" 2018-06-22 22:11:29 +05:00
Chintan Patel 896dbe312a Exporting the httpHeaders method
This method export was missing from last commit (when `httpHeaders` was created)
2018-06-08 08:09:10 +05:00
Leonid Bugaev edae33717e Improve start/end time calculation 2018-05-28 10:05:07 +03:00
Leonid Bugaev c5d1112e7a Add way to control packet capture buffer size and optimize snaplen
Added `—input-raw-buffer-size` - Controls size of the OS buffer (in
bytes) which holds packets until they dispatched. Default value depends
by system: in Linux around 2MB. If you see big package drop, increase
this value.

Additionally snaplen (max number of bytes being read for each packet)
now dynamically set based on interface MTU + max header size. In most
situations it should reduce package drop, because each packet will
consume less space in buffer.
2018-05-27 12:31:47 +03:00
Jordan Crawford 18610d3555 Solve issue 535 missing responses to HEAD requests, and add a test to verify 2018-05-23 20:53:01 +03:00
bruce34 0fea94a76c Fix 100 continue (#562)
* On write error, disconnect to force a new connection

* Don't loose bytes checking connection isAlive

* Soak up any 1xx return codes, catching up to the real return code
2018-05-23 20:49:02 +03:00
defa sun 7b1544157f fix out of range index in headerIndex, github.com/buger/goreplay/issues/578 2018-05-23 20:33:32 +03:00
Daniel Roop 4e46bb46af Updated README to include httpHeaders 2018-05-23 20:22:58 +03:00
Daniel Roop b1092d1567 Created helper to return all http headers for node middleware 2018-05-23 20:22:58 +03:00
Leonid Bugaev 0c0ef97b67 Add additional tests 2018-05-12 15:17:26 +03:00
Leonid Bugaev fe6cfd90fe Fix responses when header end line separated by multiple packets 2018-05-10 22:19:56 +03:00
Leonid Bugaev c9fe4a11ad Fix proto.Body when body < 4 bytes 2018-05-10 22:19:08 +03:00
Leonid Bugaev e9294aeb5f Fix emitter typo 2018-05-10 22:18:47 +03:00
Leonid Bugaev cb1ae0e7c0 Revert "Update middleware.js"
This reverts commit 2db143aaef.
2018-04-24 19:42:37 +03:00
Oleg Blokhin 2db143aaef Update middleware.js
fix request filtering in middleware.js
2018-04-24 19:42:05 +03:00
Ubuntu a65256d707 Fixed kafka client run of brokers and index out of range issues 2018-02-27 20:17:45 +03:00
Rajat Verma 1f62555ca8 Update README.md
Fix minor typos
2018-02-27 20:17:14 +03:00
Leonid Bugaev 52f4ad03f3 Ensure that tcp-output do not loose messages when reconnecting 2018-02-07 20:34:29 +02:00
Leonid Bugaev 35a5957801 Increase buffer size 2018-02-07 20:33:24 +02:00
Leonid Bugaev d265594b00 Ensure that tcp-output do not loose messages when reconnecting 2018-02-07 20:29:19 +02:00
Leonid Bugaev 0097c599df Fix response latency calculation
Previously latency calcualted as Response.End - Request.Start
Where both End and Start is a last and first packets

This calcualtion is wrong, because it is total roundtrip

Correct server latency will be Response.End - Request.End

In addition added new `--input-raw-timestamp-type` option
which allows choose more precise packet timestamp source (if available).
2018-02-05 21:47:48 +02:00
Ohad Basan f8a1b9dde6 fix error in bash example 2018-02-03 13:17:20 +02:00
Ohad Basan 22a67df6c0 fix wrong param in docs 2018-02-03 13:17:20 +02:00
Leonid Bugaev 5bd47ee1df Merge branch 'master' of https://github.com/buger/gor 2018-01-10 21:24:38 +02:00
Leonid Bugaev 45d7527128 Increase middleware buffer to avoid overloads when prettifier returns buffer larger then original 2018-01-10 21:23:55 +02:00
Daniel Albuquerque bf8198d639 Hardcode alpine version and bump version of goreplay from 16.0.2 to 16.1 2017-11-15 21:20:44 +02:00
Mel Shafer 3acec0fd42 fix typo in the README 2017-10-31 09:15:00 +02:00
noibar cc005ddcbb Fix comment on tcp message methods (#521)
* Fix comment on tcp message methods

* Update tcp message constructor documentation
2017-10-23 18:26:14 +03:00
Leonid Bugaev debb265d6d Fix memory leak 2017-10-04 20:18:05 +03:00
Leonid Bugaev c5f12b567b Add log helper and fix memory leak 2017-09-24 22:32:03 +05:00
Leonid Bugaev 4255d4155f Improve handling for 100-continue requests for clients ignoring response (#513)
Some clients, even if have `Expect: 100-continue` in request, start
sending data without waiting approval from the server. More over, there
is cases when response “100 Continue” is received, client changes TCP
seq, and following requests start having valid sequences, based on
response one.

This change should address this bugs.

As small addition extended list of http methods, to fully support
webdav.
2017-09-15 19:25:46 +05:00
Leonid Bugaev 927215bd9b Merge branch 'master' of https://github.com/buger/gor 2017-09-14 22:38:51 +05:00
Leonid Bugaev 08efdef37a Merge 2017-09-14 22:37:34 +05:00
Leonid Bugaev 4bb64be27e Fix panic when reading too big response in debug mode 2017-09-14 22:35:45 +05:00
Leonid Bugaev e310fc8369 Fix zero-length 100-continue packets
Should fix https://github.com/buger/goreplay/issues/496
2017-09-03 22:11:42 +05:00
Leonid Bugaev c8f4493eca Update COMM-LICENSE
Misc improvements
2017-08-28 19:26:24 +05:00
Leonid Bugaev 0f8e2f2d52 Merge branch 'master' of https://github.com/buger/gor 2017-08-22 20:43:22 +05:00
Leonid Bugaev 74a5c8570d Update s3 version 2017-08-22 20:33:42 +05:00
Aggelos Avgerinos d309650589 Allow all HTTP methods to have body (#479)
HTTP specification in section 4.3 (Message Body) states:

"A message-body MUST NOT be included in a request if the specification of the
request method (section 5.1.1)"

Section 5.1.1 has links to the description of each method (sections 9.2
to 9.9). None of those sections prohibit the transfer of a message body,
it just points out how each method should be treated server-side.
2017-08-22 19:28:21 +05:00
田欧 17f6c59061 remove some else statements for reducing indent (#488)
* remove else

* remove else
2017-08-22 19:22:59 +05:00
Ashwin Jayaprakash 9cf1ad65df Filter requests using regex on base64 decoded userId:passwd of basic auth (#475)
Example snippet: --http-basic-auth-filter "^customer[0-9].*"
2017-08-22 19:16:55 +05:00
田欧 16b6a3cc31 Adjust package order (#487)
* format package order

* format package order

* format package order

* format package order and format code by fmt
2017-08-22 19:15:01 +05:00
Ivan Klishch dfb67b4456 Fixed Dockerfile that fails during docker build (#472)
* Fixed Dockerfile that fails during docker build

* make build should use Dockerfile.dev since other make targets use it
2017-08-22 19:14:32 +05:00
François-Xavier Ponscarme efce146244 fix: add basic auth support in elastic URI (#433)
* fix: add basic auth support in elastic URI

* more simple URI check and index extract

* chore: remove dead code
2017-08-22 19:13:46 +05:00
SoulZxz eb8eb1db52 fix file rotate issues 2017-08-22 19:12:51 +05:00
Amir Majedi d7ffc3c359 Adding a python middleware example file 2017-08-22 17:35:59 +05:00
Leonid Bugaev 1c3cd633d1 Fix bpfFilter 2017-08-22 17:20:45 +05:00
Leonid Bugaev b8befff286 Sync makefile 2017-08-22 17:07:37 +05:00
Leonid Bugaev 2b79791fe6 Reading from pcap file should properly set src and dest ports 2017-08-22 17:07:37 +05:00
Leonid Bugaev 39ebff56b4 Update to use latest docker.dev 2017-08-22 17:07:37 +05:00
thinkerou c138fa9739 format file 2017-08-11 14:40:55 +03:00
thinkerou 89c34c6bf5 format file 2017-08-11 14:40:55 +03:00
tianou a1054b10e8 fix typo 2017-08-11 06:33:26 +03:00
Leonid Bugaev cf402a1d0e Add option to specify custom BPF filter
Can be useful in case of non standard network interfaces when like
tunnels or SPAN ports, when IP of interface does not match with IP of
packet.

Can be used to read traffic from multiple ports and interfaces at once
as well.
2017-07-27 17:56:33 +03:00
Leonid Bugaev 211b16888e Merge remote-tracking branch 'upstream/master' 2017-07-20 21:10:05 +03:00
Leonid Bugaev 06ec570286 Merge remote-tracking branch 'upstream/master' 2017-07-20 21:09:04 +03:00
Leonid Bugaev 09f47b25bf Fix panic for non compilant http requests 2017-07-20 21:02:07 +03:00
Leonid Bugaev 652e589e2b fix proto.Path 2017-06-30 15:58:45 +02:00
Leonid Bugaev 163bf1f1c0 0.16 updates 2017-06-29 21:55:49 +02:00
Leonid Bugaev 5aa1696680 Add TLS security support between --input-tcp and --output-tcp 2017-06-27 17:11:34 +02:00
Leonid Bugaev 21531cea9a Add support for rewriting headers 2017-06-27 15:30:34 +02:00
Leonid Bugaev 03767d1a77 Add support for HTTP 1.0 and less 2017-06-27 14:24:43 +02:00
Leonid Bugaev 2680ce9dd4 Fix #467 2017-06-27 14:01:17 +02:00
Leonid Bugaev b3dffae66f When using --input-file it should exit once file is fully read 2017-06-27 13:56:14 +02:00
Leonid Bugaev aa4a878436 Handle gzip encoding error
Fix #470
2017-06-24 20:35:35 +02:00
Michael Marino 4fe95be416 Add example of middleware for javascript 2017-06-03 11:32:49 +03:00
KingLebron 23771b8994 [Spelling Error] Change 'dirrectory' to 'directory'
:)
2017-05-23 14:32:43 +03:00
Leonid Bugaev a1a4460c90 Update Dockerfile 2017-05-18 16:53:11 +03:00
Leonid Bugaev 2b20c1107a Update Dockerfile 2017-05-18 16:52:46 +03:00
Leonid Bugaev d755c579e4 Create Dockerfile 2017-05-18 16:47:41 +03:00
Leonid Bugaev 064519ce89 Rename Dockerfile to Dockerfile.dev 2017-05-18 16:40:42 +03:00
Leonid Bugaev cdd818fe77 Update README.md 2017-05-17 22:48:45 +03:00
Leonid Bugaev c23f95d4cf Add httpMethod function 2017-05-17 22:47:05 +03:00
Leonid Bugaev 4cf59cc913 Merge branch 'master' of https://github.com/buger/gor 2017-05-17 22:43:18 +03:00
Leonid Bugaev 8f450c77a6 Update README.md 2017-05-17 21:34:26 +03:00
Leonid Bugaev a49b250ab4 Fix typo 2017-05-17 21:32:08 +03:00
Leonid Bugaev de47f6d6f6 Fix httpHeader when header value contain ":" 2017-05-17 21:21:44 +03:00
Leonid Bugaev d9ce263c6a Update README.md 2017-05-02 17:42:13 +02:00
Leonid Bugaev 74225ebb22 Rename all package imports from gor to goreplay 2017-05-01 17:56:50 +02:00
Leonid Bugaev 80777061a7 Merge remote-tracking branch 'upstream/master' 2017-05-01 17:22:45 +02:00
Leonid Bugaev d1ddda4181 Add govendor and vendor all depended packages 2017-05-01 17:10:32 +02:00
Leonid Bugaev a15055ef7f Update README.md 2017-05-01 15:25:44 +02:00
Leonid Bugaev 2b6203c110 Fix AWS config 2017-04-29 11:39:01 +02:00
Leonid Bugaev 2104bec5be Fix S3 2017-04-29 11:31:30 +02:00
ikenchina 1aa24a9910 fixed bug 2017-04-20 07:23:22 +02:00
Leonid Bugaev dcc7af4c09 Fix output-file sizeLimit 2017-04-07 15:09:16 +02:00
Leonid Bugaev 0fb98a8f48 Improve http handling 2017-04-07 14:48:12 +02:00
Leonid Bugaev 6ea23de38b Add new option --input-raw-expire to configure TCP message expiration 2017-03-17 15:23:57 +01:00
Leonid Bugaev 375538263b Add additional check for malformed packets 2017-03-17 15:01:32 +01:00
Leonid Bugaev da8b75222b Merge remote-tracking branch 'upstream/master' 2017-03-10 09:34:10 +01:00
Leonid Bugaev a2a1bd5c67 Fix handling of 304 responses if EOF error received 2017-03-10 09:32:24 +01:00
Leonid Bugaev ce46188dc2 Update NodeJS middleware 2017-03-06 20:15:35 +03:00
Leonid Bugaev 7c67b8c566 Add NodeJS middleware framework 2017-02-28 17:17:24 +03:00
Leonid Bugaev e584479911 Merge branch 'master' of https://github.com/buger/gor 2017-02-26 20:57:39 +03:00
Leonid Bugaev 5a8d56e10b Improve http client error handling
If replaying too fast, we can result in no TCP error, but actual
response is empty or garbage.
2017-02-26 20:55:40 +03:00
Leonid Bugaev c8dfe67ccb Fix middleware when used with prettyfier and gzip encoding
If while decompressing gzip payload decompressed value was less then
compressed, it did not changed buffer length, and middleware received
bytes left from gzip response.
2017-02-26 20:54:26 +03:00
Leonid Bugaev ecb119e82a Merge branch 'master' of https://github.com/buger/gor 2017-02-07 13:41:47 +03:00
Leonid Bugaev e1f3d71f4e Make http prettifier work with middleware 2017-02-07 13:34:51 +03:00
Leonid Bugaev 858dfaac78 Make HTTP output less verbose
It will act same with —debug flag
2017-02-07 13:33:45 +03:00
Leonid Bugaev 9173379cc8 Merge branch 'master' of https://github.com/buger/gor 2017-02-05 17:26:44 +03:00
Leonid Bugaev 6a48882125 Fix infinite lock at file output 2017-02-05 17:26:22 +03:00
Leonid Bugaev bf1c67c5a1 Add install task 2017-02-05 17:26:04 +03:00
Nicholas Wiersma e726e45dc0 Fix kafka issue (#419) 2017-01-25 15:25:06 +04:00
Leonid Bugaev b1a0198354 fix 2017-01-23 20:18:28 +03:00
Leonid Bugaev 1a32de3dc6 Merge branch 'master' of https://github.com/buger/gor 2017-01-23 20:04:33 +03:00
Leonid Bugaev d66ba0353a Prettify http (#418)
Prettify HTTP
2017-01-23 21:02:26 +04:00
Leonid Bugaev ecb16664db Allow to use payload type for file names (#417) 2017-01-23 16:40:14 +04:00
Leonid Bugaev 8c16220532 Track http responses (#416) 2017-01-23 16:20:27 +04:00
Leonid Bugaev ed37589748 Cleanup modifier settings after test 2017-01-23 14:43:58 +03:00
Leonid Bugaev 590304d687 Ensure that flush do not get called on closed file 2017-01-23 14:43:43 +03:00
Leonid Bugaev e69570367b More fmt fixes 2017-01-23 13:34:57 +03:00
Leonid Bugaev 043c0b7436 few fmt fixes 2017-01-23 13:33:36 +03:00
Leonid Bugaev 973b028517 Merge branch 'master' into s3 2017-01-23 13:27:56 +03:00
Leonid Bugaev b48aadc9ea Merge branch 'master' of https://github.com/buger/gor 2017-01-20 22:37:11 +03:00
Leonid Bugaev 768a29e2af Merge branch 'msales-apache-kafka-input' 2017-01-20 21:03:03 +03:00
Leonid Bugaev 5e90377a7f Fmt changes 2017-01-20 21:01:19 +03:00
Leonid Bugaev f520b338f3 Add tests and modify to work same as Kafka output 2017-01-20 21:00:23 +03:00
Leonid Bugaev 53cbd8863d Merge branch 'apache-kafka-input' of https://github.com/msales/goreplay into msales-apache-kafka-input 2017-01-20 18:15:33 +03:00
Leonid Bugaev def3c91898 Add Kafka tests and raw format mode (#415)
* Add tests and send messages in raw format by default
2017-01-20 19:13:30 +04:00
Leonid Bugaev 4ea306b5a9 Merge branch 'master' of https://github.com/buger/gor 2017-01-20 16:10:12 +03:00
Leonid Bugaev 3b51158163 continue work on new docs 2017-01-20 16:09:00 +03:00
Leonid Bugaev fe321cd055 Merge pull request #413 from SophisticaSean/master
flush the output buffer when it should be flushed
2017-01-08 11:54:53 +04:00
Leonid Bugaev 8421ddb6ff Rename Home.md to index.md 2017-01-05 20:35:21 +03:00
Leonid Bugaev e5935a001d Set theme jekyll-theme-cayman 2017-01-05 20:31:56 +03:00
Leonid Bugaev a519c09770 Set theme jekyll-theme-slate 2017-01-05 20:28:45 +03:00
Leonid Bugaev 17c1abfc5e Create CNAME 2017-01-05 20:25:09 +03:00
Leonid Bugaev ea8d3f7299 Set theme jekyll-theme-cayman 2017-01-05 20:21:20 +03:00
Leonid Bugaev 962fd70d1a Add docs to master 2017-01-05 20:19:44 +03:00
Sean Lewis b0b697cba6 flush the output buffer when it should be flushed
fixes #409
2017-01-04 14:59:39 -07:00
Leonid Bugaev 060f521a0a Merge pull request #412 from buger/handle_spaces_in_filters
Allow spaces inside filters
2016-12-27 19:52:11 +04:00
Leonid Bugaev 8e5a256226 Allow spaces inside filters 2016-12-27 18:48:59 +03:00
Leonid Bugaev 8ce5318fbf Merge pull request #410 from smostovoy/381-dumb-to-files
Support %r - request id - output file pattern #381
2016-12-27 19:07:54 +04:00
Leonid Bugaev db1e3d71e7 Merge pull request #411 from buger/fix-proto-crash
Fix crashes in ParseHeaders
2016-12-27 18:39:22 +04:00
Leonid Bugaev f5e8e26e96 Fix #408 2016-12-27 17:16:29 +03:00
Sergey Mostovoy 826cc45d44 clean files in test [#381] 2016-12-27 12:46:47 +02:00
Sergey Mostovoy 5909dd71c5 Support %r - request id - output file pattern #381 2016-12-27 12:18:49 +02:00
Leonid Bugaev 3043189d55 Merge branch 'pr/406' 2016-12-23 20:29:13 +03:00
Leonid Bugaev 9b40730338 Merge branch 'master' into pr/406 2016-12-23 20:26:09 +03:00
Leonid Bugaev be49c7e7ac Run GC on each 1000th request 2016-12-23 20:25:09 +03:00
Leonid Bugaev 92b02e4af4 Fmt 2016-12-23 19:56:33 +03:00
Leonid Bugaev 5a7bdd3d3a Add support for reading from s3 2016-12-23 19:56:25 +03:00
Nicholas Wiersma dfd5fa0c4f Update documentation 2016-12-22 10:28:35 +01:00
Sergey Mostovoy 5b5c3f33f4 Filter response if request is filtered #388 2016-12-22 11:25:29 +02:00
Nicholas Wiersma 1f19e550bb Create kafka input 2016-12-22 09:59:48 +01:00
Leonid Bugaev cd51e09fcd Merge branch 'master' into s3 2016-12-19 20:25:06 +03:00
Leonid Bugaev ddf2dc2876 fixes 2016-12-19 20:16:11 +03:00
Leonid Bugaev c4869d9338 fix 2016-12-19 14:49:28 +03:00
Leonid Bugaev fd5118b76d fix test: should not add port to host for https 2016-12-19 14:45:18 +03:00
Leonid Bugaev bc1a6c9524 Merge branch 'master' of https://github.com/buger/gor 2016-12-19 14:22:41 +03:00
Leonid Bugaev 708b2cebd3 change 2016-12-19 14:17:13 +03:00
Leonid Bugaev 7c7882969f Merge pull request #377 from kudos/patch-1
Treat PATCH as a HTTP verb
2016-12-18 20:29:48 +03:00
Leonid Bugaev 5e6b58e7a6 Merge branch 'master' of https://github.com/buger/gor 2016-12-18 20:15:35 +03:00
Leonid Bugaev a9ab7ca2f1 Merge pull request #395 from ylegat/feature/use-pcap-timestamp
Resolve buger/gor#392 : use pcap timestamp
2016-12-18 20:15:09 +03:00
Leonid Bugaev 0a74db6aae Disallow zero timeout (also used fix tests) 2016-12-18 20:09:24 +03:00
Yohan Legat b462c06a7c remove dead code 2016-12-08 17:35:45 +01:00
Yohan Legat c6244435ca [Boyscout] unexport TCPPacket.Dump() and Packet struct 2016-12-08 15:18:10 +01:00
Yohan Legat fa0e96b7ef Resolve buger/gor#392 : use pcap timestamp
Timestamp requests are now fetched from pcap payload
2016-12-08 15:18:10 +01:00
Leonid Bugaev ca61e39790 Merge pull request #398 from oivoodoo/master
Register kafka output plugin in case of having passed settings
2016-12-02 18:02:49 +03:00
Alexandr Korsak bea255ab11 Register kafka output plugin in case of having passed settings 2016-12-01 16:46:18 +03:00
Leonid Bugaev bde2977268 Merge pull request #396 from ylegat/bugfix/handle-content-length-missing-for-1xx-204-304
Resolve buger/gor#394 : Timeout for NO_CONTENT responses
2016-12-01 14:15:47 +03:00
Yohan Legat 12043e5a01 Resolve buger/gor#394 : Timeout for NO_CONTENT responses
1xx, 204 and 304 HTTP responses MUST NOT include a message body (see [RFC-2616](https://tools.ietf.org/html/rfc2616#section-4.4)).
Also, a server MAY send a Content-Length header field in a 304 (Not Modified) and MUST NOT send a Content-Length header field in any response with a status code of 1xx (Informational) or 204 (No Content) (see [RFC-7230](https://tools.ietf.org/html/rfc7230#section-3.3.2))
2016-12-01 12:08:32 +01:00
Leonid Bugaev 719e9b8e56 Merge pull request #379 from blastworksinc/feature/kafka-output
Add Apache Kafka output as JSON message
2016-11-30 20:38:47 +03:00
Leonid Bugaev cedc5fae32 Merge pull request #390 from outpace/sni-support
Add basic SNI support
2016-11-23 20:36:19 +03:00
Daniel Solano Gómez 963f7a73d7 Add basic SNI support
By adding the server name as part of the TLS client configuration, gor
will now support connecting to hosts that require SNI (such as Amazon
API Gateway).
2016-11-23 09:55:54 -05:00
Leonid Bugaev 1f24c709c2 Do not add port to Host header for https (#385) 2016-11-14 17:49:58 +03:00
Leonid Bugaev a1174a159c Do not add port to Host header for https 2016-11-14 17:47:30 +03:00
Alexandr Korsak f90dcc763b Remove server.go because of having 'gor file-server :8080' 2016-11-08 22:21:35 +03:00
Alexandr Korsak 638ddfb144 Add one more test case to cover ':' inside of header value 2016-11-02 17:50:45 +03:00
Alexandr Korsak 40f7facc33 User-Agent could contains ':' inside of value. 2016-11-02 15:34:42 +03:00
Alexandr Korsak 0a038575f3 Add all headers and properly passed body 2016-11-02 00:03:12 +03:00
Alexandr Korsak d424167f04 Change example of port for kafka host 2016-11-01 17:55:04 +03:00
Alexandr Korsak 41d3ce48a3 Add json message output for kafka 2016-11-01 17:51:15 +03:00
Alexandr Korsak 9b96dc20df Change Dockerfile because of issue on build echo.java 2016-10-31 19:29:59 +03:00
Alexandr Korsak e3a9a9b2c2 Add kafka output 2016-10-31 19:04:05 +03:00
Jonathan Cremin 76fb91966b Treat PATCH as a HTTP verb 2016-10-27 18:44:54 +01:00
manjeshnilange 22466d4028 Adding ability to output http client to close connection on specific … (#375)
* Adding ability to output http client to close connection on specific response status

* Addressed review comments
2016-10-25 22:06:43 +03:00
Leonid Bugaev 47db326b0f Fix truncated tcp check 2016-10-11 18:56:30 +03:00
Huu Khiem (Mark) 0831476beb Set default value for output-http-timeout at declaration (#371)
The actual default is 5s, set in the call of NewHTTPClient,
even if we use `--output-http--timeout=0`.

It's clearer to user if this value id declared upfront.
2016-09-27 10:40:26 +03:00
Leonid Bugaev 7f7cb7c439 Fix tests 2016-08-31 18:39:56 +03:00
Leonid Bugaev 0b986a9c3c Merge branch 'master' of https://github.com/buger/gor 2016-08-31 18:23:27 +03:00
Leonid Bugaev 967c380dc3 go fmt 2016-08-31 17:27:45 +03:00
Leonid Bugaev 1674b58707 Fix basic http auth 2016-08-31 17:27:18 +03:00
Leonid Bugaev a2f5e43375 Fix replayed response header 2016-08-31 16:27:07 +03:00
Leonid Bugaev fc0361dcdc Fix handling of connection: close for POST requests 2016-08-31 16:11:13 +03:00
Guillaume Gelin 647f7022cf Add google/gopacket in vendor/ (#360) 2016-08-23 19:29:27 +03:00
Mourjo Sen 082106dfd8 Fix Java middleware example (#358)
* Add encoding/decoding HTTP msgs to Java middleware example.

* Add clojure middleware example.
2016-08-20 19:55:45 +03:00
Leonid Bugaev 75b31fa701 Fix make file 2016-08-10 15:01:06 +03:00
Leonid Bugaev a12357b497 Fix Makefile 2016-08-10 14:08:59 +03:00
Leonid Bugaev 8e37136b8e Fix test (one more time) 2016-08-10 14:06:42 +03:00
Leonid Bugaev dac3b171a3 Fix test 2016-08-10 13:35:19 +03:00
Leonid Bugaev daa9754dd6 Merge branch 'master' of https://github.com/buger/gor 2016-08-10 13:19:52 +03:00
Leonid Bugaev ecd7e3a5e5 Update README.md 2016-08-08 19:35:11 +03:00
Leonid Bugaev 4268dcf7d1 Merge branch 'master' of https://github.com/buger/gor 2016-08-08 18:28:32 +03:00
Leonid Bugaev 544d9402f0 Disable forced flush 2016-08-08 18:09:14 +03:00
Leonid Bugaev 25ded56a59 Update COMM-LICENSE 2016-08-05 14:14:37 +03:00
Leonid Bugaev 392d9a2f0e Update LICENSE.txt 2016-08-05 14:09:20 +03:00
Leonid Bugaev fbebe72f02 Fix multi-packet splited headers 2016-08-03 16:56:50 +03:00
Leonid Bugaev e38f6a5801 Do not quite on flush error 2016-08-02 12:55:02 +03:00
Leonid Bugaev 25b998fa3f Fix run command 2016-08-01 19:23:14 +03:00
Leonid Bugaev f1f8e2f2a4 Fix http-disallow-header option 2016-08-01 19:17:05 +03:00
Leonid Bugaev 07fa6d9a7c Force Go DNS resolver 2016-08-01 18:05:52 +03:00
Leonid Bugaev 2faae82db0 Merge branch 'master' of https://github.com/buger/gor 2016-07-31 20:29:51 +03:00
Leonid Bugaev 7e245dfb18 Merge pull request #2 from buger/tcp-sessions
Add support for tcp sessions
2016-07-31 20:28:42 +03:00
Leonid Bugaev f0acd31d88 Disable --input-http flag until I decide how to do it properly 2016-07-31 20:18:49 +03:00
Leonid Bugaev d34c27c3de Track FIN packets to check for closed connection (#350)
* Track FIN packets to check for closed connection

* Add tests for FIN / fix bugs

* Fmt changes
2016-07-31 20:16:01 +03:00
Leonid Bugaev a4826fcff9 Add support for tcp sessions 2016-07-30 16:42:40 +03:00
Leonid Bugaev 2e768f776d Fix versioning 2016-07-13 20:45:44 +03:00
Leonid Bugaev 1d811f34db Update LICENSE.txt 2016-07-13 20:29:36 +03:00
Leonid Bugaev 7ce13e36d2 Merge branch 'master' of https://github.com/buger/gor 2016-07-13 16:07:42 +03:00
Leonid Bugaev edaf43027c Merge pull request #1 from buger/binary
Add support for binary input/output
2016-07-13 16:02:12 +03:00
Leonid Bugaev a6d263df1c Add support for binary input/output 2016-07-13 16:01:25 +03:00
Leonid Bugaev 35bb655f2d Fix response meta 2016-07-13 15:59:01 +03:00
Leonid Bugaev 24fc5b0078 Update README.md 2016-07-10 19:13:57 +03:00
Leonid Bugaev 8e0a8b31a9 Add missing submodule 2016-07-08 19:38:45 +03:00
Leonid Bugaev dac04cbef3 Merge branch 'ortz-feature/timeout-flag' 2016-07-08 19:27:33 +03:00
Leonid Bugaev dae6a8915d Refactor exit-after 2016-07-08 19:27:20 +03:00
Or Tzabary 6620d25626 Change flag name, use of DurationVar instead of int 2016-07-08 19:04:24 +03:00
Or Tzabary c78933f1df Feature flag 2016-07-08 17:05:46 +03:00
Leonid Bugaev 55fe5b9c11 s3-output 2016-07-08 13:20:33 +03:00
Leonid Bugaev 12287d8bae Fix type check, and add asserts submodule 2016-07-07 18:41:29 +03:00
Leonid Bugaev a5cc943af4 Fix padded packet check 2016-07-07 18:11:33 +03:00
Joseph Lawson e65035dd95 Update elasticsearch output to use vendored elastigo and fix #331. (#333)
* add elastigo submodule

* add gou submodule

* add go-hostpool submodule

* use vendored elasticgo

* update ReqUrl to ReqURL
2016-07-07 16:30:51 +03:00
Joseph Lawson c128d46088 calculate TCP payload slice sizes for Ethernet II packets less than 64 octets (#326)
* remove unused bCLRF var.

* calculate TCP payload slice sizes for Ethernet II packets less than 64 octets

* simplify slicing off padding for Ethernet link packets less than 61 octets in length.

* Make IPv4 header stripping comments more accurate.
2016-07-06 21:10:02 +03:00
Leonid Bugaev 1ed8691b38 Fix loopback for non local IP's 2016-07-05 17:09:56 +03:00
Leonid Bugaev f17f51390a Fix 100-continue with multi-packet headers
Also adds —output-null for testing purpose
2016-07-01 20:01:25 +03:00
Leonid Bugaev 5a0c03ec33 Fix BodySize for multi-packet headers 2016-06-30 14:13:27 +03:00
Leonid Bugaev 803d53a760 Merge branch 'ylegat-feature/respect_timestamps_when_replaying_requests' 2016-06-29 17:30:05 +03:00
Leonid Bugaev 7c33336433 Add missing change 2016-06-29 17:29:36 +03:00
Leonid Bugaev 37650df2c8 Refactor #308 2016-06-29 17:29:09 +03:00
Leonid Bugaev f2ff542cea Merge branch 'feature/respect_timestamps_when_replaying_requests' of https://github.com/ylegat/gor into ylegat-feature/respect_timestamps_when_replaying_requests 2016-06-27 16:19:47 +03:00
Leonid Bugaev c756a11333 Pass only valid HTTP responses (#317) 2016-06-27 16:00:57 +03:00
Leonid Bugaev 48fdc02d2f Fix tests + refactoring 2016-06-21 18:32:07 +03:00
Leonid Bugaev f2c9aee4ac Merge branch 'o19s-fix-307' 2016-06-21 18:06:05 +03:00
Leonid Bugaev aea32d9395 Rename DelHeader to DeleteHeader 2016-06-21 18:05:32 +03:00
Joseph Lawson 0089892b26 update 100-continue logic to support different header placement 2016-06-20 16:58:36 -04:00
Joseph Lawson 5d2cc68fff Add proto.DelHeader
Update proto.go to handle headers with whitespace after
2016-06-20 16:58:36 -04:00
Joseph Lawson b0fff3c402 Alternative 100 continue test 2016-06-20 16:58:36 -04:00
Joseph Lawson da09b14c8b DRY 100-continue tests 2016-06-20 16:58:36 -04:00
Leonid Bugaev e50ff77ca6 Update echo.sh 2016-06-20 19:57:10 +03:00
Leonid Bugaev 51860e130c Add support for known network layers
Fix #310
Close #311
2016-06-20 19:32:13 +03:00
Yohan Legat 9e7351d28c Resolve buger/gor#300 : respect timestamps when replaying requests
When Gor deals with multiple input files, it sorts them and replay their requests file after file.
This behavior is not convenient and should be changed. We should respect timestamps order when replaying requests.
2016-06-17 11:53:10 +02:00
Marc Lallaouret 2c49556b2e Clean current output file name (#304) 2016-06-16 22:33:55 +06:00
Marc Lallaouret d0e1858eb3 Fix Close implementation of plugins (#305)
Indeed they do not declare error as return type and so, were not called at the end of the program
2016-06-16 22:33:07 +06:00
Leonid Bugaev 95eecb6b1c Output file fixes
* Fixed flush/Write race
* Fixed names with underscores
2016-06-14 20:27:52 +05:00
Leonid Bugaev c4271ffaf4 Connection timeout should inherit Timeout 2016-06-12 16:19:51 +05:00
Leonid Bugaev f4e2b12d0f v0.14.1 (#301)
* Fix output-file

* Fix tunnel interface
2016-06-10 21:15:08 +06:00
Leonid Bugaev d193e24910 Raise error if packet layer unknown 2016-06-10 10:38:13 +05:00
Leonid Bugaev d36afeeb39 Update homebrew formula 2016-06-09 20:18:40 +05:00
Leonid Bugaev 06d129d20d Add support for injecting real ip (#296) 2016-06-09 19:06:59 +06:00
Leonid Bugaev a3aeca204c Check dst and src IP's for packets (#295)
* Add IP check

* Check dst and src ips
2016-06-09 18:41:01 +06:00
Leonid Bugaev 35696a49c2 Add logging to the file-server 2016-06-08 14:38:09 +05:00
Leonid Bugaev 0fb8cec69b Embed simple file server for tutorial purpose 2016-06-07 19:43:53 +05:00
Leonid Bugaev cefa2b5cc2 Write files in chunks (#293)
* Chunked output strategy
* Fix sorting with indexes >= 10
2016-06-07 18:46:13 +06:00
Leonid Bugaev ae092d6a02 Add custom HTTP methods
Should fix #291
2016-06-04 21:12:43 +05:00
Leonid Bugaev 9a6186ce9c Merge pull request #290 from buger/output-file-improvements
File replay and recording improvements
2016-06-01 20:42:34 +06:00
Leonid Bugaev ede0fb4c4c Allow looping mode for --input-file 2016-06-01 18:49:20 +05:00
Leonid Bugaev 651bd82d3d input-file support for gzip files 2016-06-01 10:41:29 +05:00
zweite a4e5918534 start should after beging listen signal (#289) 2016-06-01 11:11:43 +06:00
Leonid Bugaev ecfd8549a6 Remove debug 2016-05-31 20:57:19 +05:00
Leonid Bugaev 0f5b509397 Add support for reading multiple files 2016-05-31 20:44:04 +05:00
Leonid Bugaev c8a234b3dc Remove test artifacts 2016-05-31 17:12:34 +05:00
Leonid Bugaev b6f7de00c5 Add compression support 2016-05-31 16:41:01 +05:00
Leonid Bugaev ba98e6e18a Allow date templates for file names 2016-05-31 15:52:35 +05:00
Leonid Bugaev b01748bbdf Update echo.rb 2016-05-27 20:07:21 +05:00
Leonid Bugaev 75d217b846 Few fmt fixes 2016-05-27 16:38:09 +05:00
Leonid Bugaev c1f354a01c Support lower case headers 2016-05-27 16:36:28 +05:00
Leonid Bugaev b900e38035 Add homebrew formula 2016-05-27 16:36:09 +05:00
Matteo 2f45fc8ef3 Fix link in Readme.md (#287) 2016-05-27 16:33:22 +06:00
Leonid Bugaev 62b92576a0 Update README.md 2016-05-25 19:15:01 +05:00
Leonid Bugaev 98fe7f19b2 Fix raw socket ipv6 support 2016-05-25 16:27:18 +05:00
Dmitri Shuralyov 1c3af3ef6d README: Fix link to issue tracker. (#283)
It looks like you've used incorrect Markdown syntax to create a few links in commit eb9e073af8. You've since fixed some of the other links, e.g., in 2f86d005ad, but this one remained unfixed.

Also change the URL to point to issue tracker, and change the schema to https.
2016-05-24 13:22:01 +06:00
Leonid Bugaev 98868e81c5 Do not run multiple BPF filters at once 2016-05-23 18:12:31 +05:00
Leonid Bugaev 0ccace8cad Improve reconnection strategy 2016-05-23 18:05:34 +05:00
Leonid Bugaev a181b144d7 Fix output-stdout 2016-05-23 18:05:09 +05:00
Leonid Bugaev bf13dc59c3 Rename --output-dummy to --output-stdout and make it boolean (#282) 2016-05-21 21:16:47 +06:00
Leonid Bugaev d944ac6789 Properly handle requests with headers consisting of multiple packets (#281) 2016-05-21 19:46:34 +06:00
Leonid Bugaev a52f56df2b Do not track response by default (#279)
* Do not track response by default

* Add track-response integration test

* Apply fmt
2016-05-20 21:09:39 +06:00
Leonid Bugaev 2836b555e3 Fix http client when headers are sent in separate chunks 2016-05-19 11:54:19 +05:00
Leonid Bugaev af6307e6ef Rename http-buffer option 2016-05-19 11:52:35 +05:00
Ricardo Galli 38535a2055 Added http client buffer size (#249) 2016-05-19 11:33:17 +06:00
Leonid Bugaev ae7a3b7fa5 Merge pull request #277 from buger/http-compilance
Improve Http client compilance
2016-05-19 11:31:55 +06:00
Leonid Bugaev 3fa1b66fc5 Fix typo 2016-05-19 10:31:44 +05:00
Leonid Bugaev 7496692d76 Fix hound 2016-05-19 10:31:06 +05:00
Leonid Bugaev cdbf3b910e Apply fmt and fix tests 2016-05-19 10:26:39 +05:00
Leonid Bugaev fe90601bff Improve HTTP client compliance 2016-05-19 10:10:15 +05:00
Leonid Bugaev d40d6c6116 Fix --input-tcp for payloads > 64kb 2016-05-17 19:56:13 +05:00
Leonid Bugaev 37d657fc33 Update README.md 2016-05-17 18:27:04 +05:00
Leonid Bugaev 2f86d005ad Update README.md 2016-05-17 18:24:59 +05:00
Leonid Bugaev 9bed47e4d0 Update README.md 2016-05-17 18:10:27 +05:00
Leonid Bugaev 61605aefb2 Update README.md 2016-05-17 18:09:54 +05:00
Leonid Bugaev eb9e073af8 Update README.md 2016-05-17 18:07:25 +05:00
Leonid Bugaev 163f4602e4 Update README.md 2016-05-17 17:57:28 +05:00
Leonid Bugaev fee36a486d Fix makefile 2016-05-14 11:36:33 +05:00
Leonid Bugaev 854ea15900 Update release script 2016-05-13 17:42:59 +05:00
Leonid Bugaev f8349e4488 Merge pull request #273 from buger/libpcap-fixes
Libpcap fixes
2016-05-13 18:35:06 +06:00
Leonid Bugaev aac1345d16 Add Mac support 2016-05-13 17:31:39 +05:00
Leonid Bugaev 663bd8b95d fmt fixes 2016-05-13 14:46:27 +05:00
Leonid Bugaev 19bfb8211c Use latest libpcap and ensure to close sockets 2016-05-13 14:33:20 +05:00
Leonid Bugaev cbc871b83f Improve listening on all interfaces for complex environments
If one interface is failed it should not fail whole program
2016-05-11 20:08:20 +05:00
Leonid Bugaev 768c5f9726 Apply FMT 2016-05-11 16:53:23 +05:00
Leonid Bugaev 47bb312b73 Allow listening on ALL interfaces 2016-05-11 16:52:52 +05:00
Leonid Bugaev fb9cf68ff9 Merge pull request #270 from buger/ipv6
Add support for IPv6
2016-05-11 17:28:40 +06:00
Leonid Bugaev 759efce521 Add support for IPv6 2016-05-11 16:27:48 +05:00
Leonid Bugaev af831381aa Merge pull request #269 from buger/performance
Fix memory leaks and performance improvements
2016-05-11 15:32:17 +06:00
Leonid Bugaev 85e84ce330 Improve handling of fragmented messages 2016-05-11 14:26:11 +05:00
Leonid Bugaev 2bc441e0be More fixes 2016-05-11 13:16:05 +05:00
Leonid Bugaev 7ee06e84f4 Add dest port to id 2016-05-10 15:40:13 +05:00
Leonid Bugaev 4109336a43 Remove unused logging 2016-05-10 11:10:14 +05:00
Leonid Bugaev 3ada9b63fd Remove unused fields from packet 2016-05-10 11:08:39 +05:00
Leonid Bugaev c41608e48c Fix tests 2016-05-10 11:01:49 +05:00
Leonid Bugaev 48995cc41b Do not span multiple goroutines 2016-05-10 06:40:35 +05:00
Leonid Bugaev 6a3b791f7b Fix memory leak and improve tests 2016-05-09 18:00:12 +05:00
Leonid Bugaev ddaa0e9d74 Require IP address
libpcap can’t listen on all interfaces in single thread
2016-05-03 18:47:11 +05:00
Leonid Bugaev 281219e6a0 Use static linking for libpcap and fix error with truncated packets 2016-05-03 17:55:38 +05:00
Leonid Bugaev ba227ae9ba Fix license file 2016-05-03 10:28:15 +05:00
Leonid Bugaev 5779ea1b45 Update java middleware to not use apache commons 2016-05-02 16:31:38 +05:00
Leonid Bugaev bb0fab4d35 Update README.md 2016-04-30 18:35:51 +05:00
Leonid Bugaev 8cc377f22f Update README.md 2016-04-30 18:35:13 +05:00
Leonid Bugaev 5bea65c723 Fix input raw command line options 2016-04-29 21:44:50 +05:00
Leonid Bugaev 52a0ebfd57 Fmt fixes 2016-04-29 16:01:37 +05:00
Leonid Bugaev a8731f290c Fix broken pipe error 2016-04-29 16:01:30 +05:00
Leonid Bugaev 2f0e865e82 Fix middleware crashing for big payloads.
Fix #245 and #250
2016-04-29 15:34:38 +05:00
Leonid Bugaev afc8f7852e Libpcap support and vairous bugfixes
Merge #260
2016-04-29 14:48:17 +05:00
Leonid Bugaev 00feb08165 Add benchmark for testing packet capture (and its quality) 2016-04-26 17:18:09 +05:00
Leonid Bugaev 0754ed7d83 Merge pull request #239 from dougjohnson/output-http-debug
add flag to enable debugging of http output
2016-04-25 17:55:04 +06:00
Leonid Bugaev 4baf3985dc More debugging 2016-04-25 16:46:34 +05:00
Leonid Bugaev 32264eb427 Update README.md 2016-04-25 10:44:58 +05:00
Salimane Adjao Moustapha f2cedaf89f Merge pull request #256 from buger/revert-238-fix-memory-leak-in-aliases
Revert "Fixes memory leak by adding respAliases cleanup after expiration time"
2016-04-23 21:35:38 +02:00
Salimane Adjao Moustapha e8f7d30551 Revert "Fixes memory leak by adding respAliases cleanup after expiration time" 2016-04-23 21:17:54 +02:00
Salimane Adjao Moustapha 0c7643f477 Merge pull request #238 from AnderEnder/fix-memory-leak-in-aliases
Fixes memory leak by adding respAliases cleanup after expiration time
2016-04-23 20:23:11 +02:00
Salimane Adjao Moustapha 4c8a73a58b Merge pull request #235 from sneakybeaky/input_file_reader
Replace the use of bufio.Scanner with bufio.Reader
2016-04-23 20:20:42 +02:00
Leonid Bugaev 3d7455d6c1 Update README.md 2016-04-23 18:35:17 +05:00
Leonid Bugaev 3c40ac7ab6 Fix input dummy spec 2016-04-23 09:38:46 +05:00
Leonid Bugaev e0e065ba8a Fix license name 2016-04-23 09:36:33 +05:00
Leonid Bugaev fc1771ef70 Update README.md 2016-04-22 15:00:29 +05:00
Leonid Bugaev 5ec67ee88d Create COM-LICENSE 2016-04-22 14:21:41 +05:00
Leonid Bugaev 500d80d261 Update LICENSE.txt 2016-04-22 14:21:01 +05:00
Leonid Bugaev eb395ff815 Update README.md 2016-04-22 13:47:06 +05:00
Leonid Bugaev 538c05ec62 Merge pull request #255 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.md
2016-04-22 14:45:56 +06:00
The Gitter Badger d99117c92b Add Gitter badge 2016-04-22 08:45:25 +00:00
Jon Barber e659f65d76 Replace the use of bufio.Scanner with bufio.Reader so that requests over 64Kb in size can be replayed from a capture file. 2016-04-21 10:15:48 +01:00
Andrii Radyk 7a2f1aef4c Merge remote-tracking branch 'upstream/master' into fix-memory-leak-in-aliases 2016-04-21 09:23:03 +02:00
Leonid Bugaev 6c410ab978 Update README.md 2016-04-21 11:51:33 +05:00
Leonid Bugaev a3403a3295 Update README.md 2016-04-21 11:49:19 +05:00
Leonid Bugaev cb4db10caf Update README.md 2016-04-21 11:48:58 +05:00
Leonid Bugaev 55405b6bfc Update README.md 2016-04-21 11:48:03 +05:00
Leonid Bugaev fecfa7f15f More listener tests 2016-04-21 10:34:08 +05:00
Leonid Bugaev eb6166724b Try to fix tests 2016-04-21 06:27:19 +05:00
Salimane Adjao Moustapha dbd8a48561 Merge pull request #237 from leejones/patch-1
Moved -output-http-redirects option description to a new line in README.md
2016-04-21 01:04:53 +02:00
Salimane Adjao Moustapha d6f5ac1c3d Merge pull request #241 from mortonfox/patch-1
Fix link to the 'too many open files' blog entry
2016-04-21 00:51:34 +02:00
Leonid Bugaev 334db15df9 Apply gofmt simplification 2016-04-20 22:03:19 +05:00
Leonid Bugaev d3e58ba374 Update README.md 2016-04-20 21:56:04 +05:00
Leonid Bugaev 7dee2dbefb Update README.md 2016-04-20 21:55:35 +05:00
Leonid Bugaev 1014abf878 Merge pull request #253 from buger/listener-tests
Listener tests, fix TravisCI builds, switch to 1.6
2016-04-20 22:49:42 +06:00
Leonid Bugaev fa11cc04c8 Explicitly set ip in tests 2016-04-20 21:44:37 +05:00
Leonid Bugaev 4cfa33f89c Fix tests and formatting 2016-04-20 21:40:18 +05:00
Leonid Bugaev a62f269d3e Try to use local ip in tests 2016-04-20 21:34:00 +05:00
Leonid Bugaev 7b2d7800b4 Merge remote-tracking branch 'origin/go-1.5' into go-1.5
# Conflicts:
#	Makefile
#	raw_socket_listener/listener.go
#	raw_socket_listener/listener_test.go
#	raw_socket_listener/tcp_message.go
2016-04-20 21:26:09 +05:00
Leonid Bugaev e5daaf067e Fix tests 2016-04-20 21:19:20 +05:00
Leonid Bugaev 9fc7cdc0d3 Add tests for listener and tcp_message 2016-04-19 20:43:54 +05:00
Leonid Bugaev 94f066c97a Merge pull request #246 from korzonek/master
codebeat badge
2016-03-10 16:29:34 +05:00
Tomasz Korzeniowski 7f27d60f8a codebeat badge
Is it fine to add codebeat badge to README? 

codebeat is automated code review tool for Swift,Ruby,Go & Python that helps get instant feedback on code quality. 

"Quick wins" suggested by codebeat could be a nice candidate for a pull request and help other developers become contributors.

FYI. To be fully open and honest. I'm co-founder of codebeat.
2016-03-10 11:40:55 +01:00
Morton Fox ad2bf0334c Fix link to the 'too many open files' blog entry 2016-02-01 16:24:44 -05:00
Doug Johnson 699eefe317 add flag to enable debugging of http output 2016-01-26 21:56:55 +00:00
Andrii Radyk 2a955dcdbf Fixes memory leak by adding respAliases cleanup after expiration time 2016-01-26 13:26:47 +01:00
Lee Jones 28f82b462e Update README.md
Moved `-output-http-redirects` option description to new line.
2016-01-22 16:12:57 -06:00
Leonid Bugaev 1fdcc54121 Merge pull request #225 from johanneswuerbach/middleware-failures
Handle middleware failures
2015-11-23 18:22:24 +01:00
Johannes Würbach 5378ecca71 Handle middleware failures
Without this the middleware might crash silently and gor keeps running
2015-11-23 17:54:52 +01:00
Leonid Bugaev 9d861af3a2 Merge pull request #219 from pbsurf/pbsurf/fix_crashes
fix crash on missing payload metadata
2015-11-18 11:32:19 +01:00
pbsurf 5c272661e9 fix crash on missing payload metadata 2015-11-04 20:40:44 +00:00
Leonid Bugaev a4c40cd8b4 Update README.md 2015-10-07 22:18:38 +03:00
Leonid Bugaev d23848167d Merge pull request #214 from bkochendorfer/itercepted
Fix small typo
2015-09-30 10:34:15 +03:00
Brett Kochendorfer 44506785cb Fix small typo 2015-09-29 15:11:51 -05:00
Leonid Bugaev bb2adbce65 Update travis 2015-09-24 17:21:16 +03:00
Leonid Bugaev 028bc6c34e Switch to Go 1.5 2015-09-24 17:20:49 +03:00
Leonid Bugaev 16c5b6f15b Merge pull request #210 from buger/support-non-rfc-headers
Handle headers added by bad clients
2015-09-23 16:53:49 +03:00
Leonid Bugaev 5cf8f7dfd0 Handle headers added by bad clients 2015-09-23 16:32:50 +03:00
Leonid Bugaev 2b522285c6 Fix 100-continue check for packets with data < 4 2015-09-05 16:48:39 +03:00
Leonid Bugaev 5dcec259d6 Add more debug 2015-09-04 17:44:14 +03:00
Leonid Bugaev 766bbc6127 Merge pull request #202 from buger/file-input-speed-fix
Fix file input replay speed
2015-09-03 21:55:09 +03:00
Leonid Bugaev 6d2a895db3 Fix file input replay speed 2015-09-03 21:50:32 +03:00
Leonid Bugaev d81e39d25c Improve test speed 2015-09-02 22:40:00 +03:00
Leonid Bugaev ed94985346 Merge pull request #201 from buger/tcp-gc
Improve raw input scheduler
2015-09-02 20:43:41 +03:00
Leonid Bugaev f378def0b3 Improve raw input GC 2015-09-02 20:33:12 +03:00
Leonid Bugaev cd83e6f9ba Formatting fixes 2015-09-01 22:43:36 +03:00
Leonid Bugaev 60ddd535bc Add test for large input-http payload 2015-09-01 22:43:04 +03:00
Leonid Bugaev 9ded255788 Merge pull request #195 from mre/patch-1
Fix some typos
2015-09-01 22:34:47 +03:00
Leonid Bugaev 323f85bcfb Merge pull request #194 from buger/fix-memory-leaks
Fix memory leaks
2015-09-01 22:21:13 +03:00
Leonid Bugaev 07a3c191d7 Speedup input-raw 2015-09-01 22:16:29 +03:00
Leonid Bugaev 4f3354507c Fix race 2015-09-01 22:10:06 +03:00
Leonid Bugaev 661dd2026f Fix grouting spawn loop and memory leaks 2015-09-01 20:27:26 +03:00
Leonid Bugaev 67505d99d8 Merge pull request #198 from justanshulsharma/fix-typo
Fix typo
2015-09-01 19:16:15 +03:00
Anshul Sharma ac0438995c Fix typo 2015-09-01 09:58:35 +05:30
Matthias Endler b5aa7d7c2d Fix some typos 2015-08-28 16:18:04 +02:00
Leonid Bugaev 8daa7fdcab Fix memory leaks 2015-08-26 22:25:58 +03:00
Leonid Bugaev 1fea397d54 Merge pull request #162 from buger/input-modifier
Middleware for custom request rewrite logic
2015-08-24 09:44:00 +03:00
Leonid Bugaev abde6ecfcb less verbose output 2015-08-24 09:40:15 +03:00
Leonid Bugaev 43262f3336 Increase timeouts 2015-08-24 09:32:31 +03:00
Leonid Bugaev fe91a20629 fix race 2015-08-24 09:24:31 +03:00
Leonid Bugaev 3c81723ac3 Increase timeout for large payload 2015-08-24 08:58:57 +03:00
Leonid Bugaev 929e027907 small formatting change 2015-08-23 13:08:51 +03:00
Leonid Bugaev f0b96af420 Add protection for malformed requests 2015-08-23 13:07:29 +03:00
Leonid Bugaev f3ea30c6df Add example of java echo middleware 2015-08-23 12:45:28 +03:00
Leonid Bugaev d5a1eb0b9d Increase input raw timeout 2015-08-22 11:37:01 +03:00
Leonid Bugaev 02bf7a86de Rollback to 1.4.2 in travis 2015-08-22 09:58:52 +03:00
Leonid Bugaev cbc27bd662 More debugging 2015-08-22 09:48:10 +03:00
Leonid Bugaev 16d7f23d98 New fixes 2015-08-22 09:41:22 +03:00
Leonid Bugaev 6533b50ab0 Travis should use 1.5 2015-08-20 09:51:44 +03:00
Leonid Bugaev 9309fb86ef Reduce debugging 2015-08-20 09:45:59 +03:00
Leonid Bugaev 76d5e22ba1 Enable packet debugging 2015-08-20 09:33:51 +03:00
Leonid Bugaev 81d3508653 Enable race testing for travis 2015-08-20 09:29:51 +03:00
Leonid Bugaev 3c45595a96 More race fixes 2015-08-20 09:27:44 +03:00
Leonid Bugaev 6974a6e278 Ensure that raw input gets closed 2015-08-20 08:56:10 +03:00
Leonid Bugaev 33cdcd4d13 Use httptest package and properly close servers 2015-08-20 07:28:13 +03:00
Leonid Bugaev 9550b31fde Fix plugin registration 2015-08-19 22:38:24 +03:00
Leonid Bugaev 6cb4c265dd Some debugging and race fixes 2015-08-19 21:46:45 +03:00
Leonid Bugaev e54f77f229 Try to return all responses 2015-08-19 09:27:16 +03:00
Leonid Bugaev 5d2f6eceea Fixes 2015-08-19 09:19:00 +03:00
Leonid Bugaev a6e29c684c Close connection 2015-08-18 21:25:56 +03:00
Leonid Bugaev 78456d6cd1 More timeout 2015-08-18 17:46:57 +03:00
Leonid Bugaev c4037e0c57 Increase travis timeout time 2015-08-18 17:42:55 +03:00
Leonid Bugaev 7cff78174a Remove verbose flag 2015-08-18 17:35:54 +03:00
Leonid Bugaev b53fba0415 Fix formatting issues 2015-08-18 17:28:09 +03:00
Leonid Bugaev d6c776fa0c Verbose output for travis 2015-08-18 17:27:26 +03:00
Leonid Bugaev ae35f88004 Remove magic numbers 2015-08-18 17:18:02 +03:00
Leonid Bugaev 8da4e54cbb Tcp communication should use same protocol as file based 2015-08-18 16:02:03 +03:00
Leonid Bugaev 7790b33cea Refactor protocol scanner 2015-08-18 15:52:23 +03:00
Leonid Bugaev a971d8a843 Change file format to be text based 2015-08-18 15:48:42 +03:00
Leonid Bugaev 3e763c43cf Unify all input/output plugins 2015-08-18 14:32:52 +03:00
Leonid Bugaev ea94dd0162 fmt changes 2015-08-18 08:20:08 +03:00
Leonid Bugaev d96e3393ec Handle HTTP error codes 2015-08-17 20:36:59 +03:00
Leonid Bugaev 69424bf670 Fix hound 2015-08-17 17:13:55 +03:00
Leonid Bugaev 211be0cddd Add request time and round-trip response time 2015-08-17 17:10:43 +03:00
Leonid Bugaev d1091102ba Ignore all release files 2015-08-15 21:46:09 +03:00
Leonid Bugaev 0d445722b9 Some fixes 2015-08-15 21:27:20 +03:00
Leonid Bugaev 879ed48b80 Doc changes 2015-08-15 21:05:51 +03:00
Leonid Bugaev ddd5190622 Improve docs 2015-08-15 21:05:10 +03:00
Leonid Bugaev a18079fa84 Improving docs 2015-08-15 10:33:50 +03:00
Leonid Bugaev 1a36374c2d Add some comments 2015-08-15 08:44:27 +03:00
Leonid Bugaev 41c1421a1b Simulate some real-life server by adding small response delay 2015-08-15 08:36:13 +03:00
Leonid Bugaev 4f2341b2d3 Remove more debug 2015-08-15 08:06:13 +03:00
Leonid Bugaev 56f0a3fcc0 Remove too verbose output 2015-08-15 07:57:25 +03:00
Leonid Bugaev 70e7d8dd29 Reduce delay of raw input 2015-08-14 23:07:46 +03:00
Leonid Bugaev bb319b5fd2 Add request ID and token modifier middleware 2015-08-14 20:42:23 +03:00
Leonid Bugaev 491541c2df Response object should be separate message 2015-08-13 22:11:23 +03:00
Leonid Bugaev 2990fab635 Track original responses 2015-08-11 17:37:37 +03:00
Leonid Bugaev 8046468ea4 Fix tests 2015-08-11 11:28:22 +03:00
Leonid Bugaev 0009555d96 Apply fmt 2015-08-11 11:08:14 +03:00
Leonid Bugaev 5098e388a2 Fix tests, now properly receive responses 2015-08-11 11:06:25 +03:00
Leonid Bugaev 5667cd35c6 Merge branch 'master' into input-modifier 2015-08-10 22:57:33 +03:00
Leonid Bugaev 9ab8b3c113 Add header to request and response 2015-08-10 22:57:18 +03:00
Leonid Bugaev 5e526af47b Set version dynamically via ldflags 2015-08-10 20:26:37 +03:00
Leonid Bugaev 3a7badc508 Merge pull request #190 from buger/fix-default-port
Fix default port
2015-08-09 11:13:10 +03:00
Leonid Bugaev 9a76e354c5 Fix tcp input test 2015-08-09 11:08:53 +03:00
Leonid Bugaev d6b5c675cf Do not add 80 port to host string 2015-08-09 11:07:27 +03:00
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 35854196f0 Fix tests 2015-08-03 20:07:28 +03:00
Leonid Bugaev a9be1811ea Merge branch 'master' into input-modifier
Conflicts:
	Makefile
2015-08-03 19:59:03 +03: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 d62eccb655 Make raw message expiration configurable to improve tests time 2015-07-21 10:06:10 +05:00
Leonid Bugaev 5cc4228dcd Merge branch 'master' into input-modifier
Conflicts:
	Dockerfile
	emitter.go
	http_client.go
	plugins.go
	proto/proto.go
2015-07-21 09:49:37 +05:00
Leonid Bugaev 5e6f7e03a7 First middleware tests! :dance: 2015-07-17 20:58:31 +05: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 f812120dfc Support only 1 middleware and rewrite tests 2015-07-10 10:24:00 +05:00
Leonid Bugaev e64aac9364 Rename input modifier to middleware 2015-07-09 20:24:27 +05:00
Leonid Bugaev 8db14e334d Merge branch 'master' into input-modifier
Conflicts:
	Dockerfile
	Makefile
	output_http.go
	output_http_test.go
	settings.go
2015-07-09 19:58:24 +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 3e86456e36 Add more debug 2015-06-28 22:00:55 +05:00
Leonid Bugaev 98f941b69a Add support for complex commands 2015-06-28 00:23:21 +05:00
Leonid Bugaev 674ffc6202 Start adding tests for traffic modifier 2015-06-28 00:16:19 +05:00
Leonid Bugaev 4f03f81d04 Working modifier implementation with examples 2015-06-27 21:09:06 +05:00
Leonid Bugaev dcb8a1dee4 Merge branch 'master' into input-modifier 2015-06-26 10:04:46 +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 208faec7d2 Initial support for input modifiers 2014-12-16 16:25:17 +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
Leonid Bugaev b7567bbd15 It should be 0.9.2 version 2014-10-27 16:42:48 +05:00
Leonid Bugaev e4b7c8c763 Merge pull request #126 from buger/limiter-extraction
Limiter improvements
2014-10-27 16:41:33 +05:00
Leonid Bugaev 62ced2e1ba Update version 2014-10-27 16:41:07 +05:00
Leonid Bugaev 79ed6a7548 Fix absolute limiting for file input 2014-10-26 21:21:00 +03:00
Leonid Bugaev 3793a84e59 Add docs on load testing using input-file 2014-10-26 15:08:29 +03:00
Leonid Bugaev f2cf81c5bf FileInput should have own limiter algorithm 2014-10-26 15:00:00 +03:00
Leonid Bugaev aae57733a8 Fix limiter for readers 2014-10-26 13:54:42 +03:00
Leonid Bugaev 00df525b6a Update docs 2014-10-26 13:33:06 +03:00
Leonid Bugaev 7864808e5e Some formatting fixes 2014-10-26 13:25:18 +03:00
Leonid Bugaev 58c7e12d1f Add percentage based limiting 2014-10-26 13:23:17 +03:00
Leonid Bugaev de143e4268 Make limiter work for Inputs 2014-10-26 13:07:10 +03:00
Leonid Bugaev 67cfca0ede Make limiter work for all inputs/outputs 2014-10-26 01:02:49 +04:00
Leonid Bugaev 43920a96c0 Added docs on running single test 2014-10-25 21:14:08 +04:00
Leonid Bugaev 4cf3bf0ae4 Refactor plugin registration 2014-10-25 21:08:54 +04:00
Leonid Bugaev 095c717f6a Update README.md 2014-10-21 15:43:53 +04:00
Leonid Bugaev abc44c2fa5 Fix tcp_message typo 2014-10-13 20:24:07 +04:00
Leonid Bugaev 2d66ccc62d Add limiter support to output-file 2014-10-13 19:37:43 +04:00
Leonid Bugaev b257231d51 Ensure that channel not closed 2 times in row
Fix #114
2014-10-13 19:24:42 +04:00
Leonid Bugaev 1cab1ecb0e Add EmitFile for test purpose 2014-10-13 18:54:47 +04:00
Leonid Bugaev 983bc728fa Increase buffer to 5 megabytes 2014-10-13 18:26:08 +04:00
Leonid Bugaev 4b592e0e2d Fix Makefile (it should use tabs) 2014-10-13 18:08:37 +04:00
Leonid Bugaev 1637dd491f Convert tabs to spaces and force go fmt 2014-10-13 18:05:16 +04:00
Leonid Bugaev 9e63fb5b36 Merge pull request #125 from buger/add-http-input
Add http input
2014-10-13 18:01:12 +04:00
Leonid Bugaev 04a8cceb35 Updated docs 2014-10-13 17:59:32 +04:00
Leonid Bugaev 6ddf4df21a Add comment to make 2014-10-13 17:42:08 +04:00
Leonid Bugaev 51f5a3e9ec Merge branch 'master' into add-http-input 2014-10-13 17:38:27 +04:00
Leonid Bugaev 4c0c9e97c5 Add http input 2014-10-13 17:37:22 +04:00
Leonid Bugaev 4f64b71adb Merge pull request #124 from buger/workers-pool-refactoring
Refactor http output
2014-10-13 16:52:06 +04:00
Leonid Bugaev be7e27010d Refactor http output 2014-10-13 15:24:43 +04:00
Leonid Bugaev 758995d8f5 Fix main goroutine 2014-10-09 23:16:56 +04:00
Leonid Bugaev 5f883e08ce Run race detection for tests 2014-10-09 22:45:15 +04:00
Leonid Bugaev 3a43e697d5 Update release task to work with docker 2014-10-09 13:43:12 +04:00
160 changed files with 18576 additions and 2170 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"contributors": ["buger"]
}
+28
View File
@@ -0,0 +1,28 @@
version = 1
exclude_patterns = [
"vendor/**"
]
[[analyzers]]
name = "go"
enabled = true
[analyzers.meta]
import_paths = ["github.com/ankitdobhal/goreplay"]
[[analyzers]]
name = "docker"
enabled = true
[[analyzers]]
name = "ruby"
enabled = true
[[analyzers]]
name = "javascript"
enabled = true
[analyzers.meta]
environment = ["nodejs"]
+3
View File
@@ -0,0 +1,3 @@
*.tar.gz
gor
gor.test
+10
View File
@@ -0,0 +1,10 @@
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
pull-request-branch-name:
# Separate sections of the branch name with a hyphen
separator: "-"
+38
View File
@@ -0,0 +1,38 @@
name: ci
on:
release:
types: [published]
jobs:
docker-build-and-push:
runs-on: ubuntu-latest
steps:
-
name: "Checkout repository"
uses: actions/checkout@v2
-
name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v1
-
name: "Cache Docker layers"
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
-
name: "Login to Container Registry"
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: "Build and push docker image"
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ github.repository }}:${{ github.event.release.tag_name }}
build-args: RELEASE_VERSION=${{ github.event.release.tag_name }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,mode=max,dest=/tmp/.buildx-cache
+29
View File
@@ -0,0 +1,29 @@
name: test
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
go-version: [1.14.x, 1.15.x] # two latest minor versions
runs-on: ubuntu-latest
steps:
- name: update package index
run: sudo apt-get update
- name: install libpcap
run: sudo apt-get install libpcap-dev -y
- name: install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: checkout code
uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: |
~/go/pkg/mod # Module download cache
~/.cache/go-build # Build cache (Linux)
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: test
run: sudo go test ./... -v -timeout 120s
+28
View File
@@ -1,2 +1,30 @@
*.swp
*.gor
*.rpm
*.dep
*.pkg
*.out
*.bin
*.gz
*.zip
*.class
*.test
.idea
*.iml
gor
*.mprof
*.pcap
.DS_Store
goreplay
corpus
crashers
suppressions
View File
+5
View File
@@ -0,0 +1,5 @@
POST /post HTTP/1.1
Content-Length: 7
Host: www.w3.org
a=1&b=2
-3
View File
@@ -1,3 +0,0 @@
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 && go test -v"
-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
+94
View File
@@ -0,0 +1,94 @@
END-USER LICENSE AGREEMENT
------------------------------------------------------------------------------
IMPORTANT: THIS SOFTWARE END-USER LICENSE AGREEMENT ("EULA") IS A LEGAL AGREEMENT (“Agreement”) BETWEEN YOU (THE CUSTOMER, EITHER AS AN INDIVIDUAL OR, IF PURCHASED OR OTHERWISE ACQUIRED BY OR FOR AN ENTITY, AS AN ENTITY) AND GoReplay LLC. READ IT CAREFULLY BEFORE COMPLETING THE INSTALLATION PROCESS AND USING GOREPLAY PRO AND RELATED SOFTWARE COMPONENTS (“SOFTWARE”). IT PROVIDES A LICENSE TO USE THE SOFTWARE AND CONTAINS WARRANTY INFORMATION AND LIABILITY DISCLAIMERS. BY INSTALLING AND USING THE SOFTWARE, YOU ARE CONFIRMING YOUR ACCEPTANCE OF THE SOFTWARE AND AGREEING TO BECOME BOUND BY THE TERMS OF THIS AGREEMENT.
------------------------------------------------------------------------------
In order to use the Software under this Agreement, you must receive a “Source URL” at the time of purchase, in accordance with the scope of use and other terms specified for each type of Software and as set forth in this Section 1 of this Agreement.
1. License Grant
1.1 General Use. This Agreement grants you a non-exclusive, non-transferable, limited license to the use rights for the Software, without the right to grant sublicenses, subject to the terms and conditions in this Agreement. The Software is licensed, not sold.
1.2 Unlimited Organization License. If you purchased an Organization License (included with the GoReplay Pro Software), you may install the Software on an unlimited number of Hosts. “Host” means any physical or virtual machine which is controlled by you. You may concurrently run the software on an unlimited number of Hosts.
1.3 Appliance License. If you purchased an Appliance License, you may distribute the Software in any applications, frameworks, or elements (collectively referred to as an “Application” or “Applications”) that you develop using the Software in accordance with this EULA, provided that such distribution does not violate the restrictions set forth in section 3 of this EULA. You must not remove, obscure or interfere with any copyright, acknowledgment, attribution, trademark, warning or disclaimer statement affixed to, incorporated in or otherwise applied in connection with the Software. You are required to ensure that the Software is not reused by or with any applications other than those with which you distribute it as permitted herein. For example, if You install the Software on a customer’s server, that customer is not permitted to use the Software independently of your Application. You must inform GoReplay LLC of your knowledge of any infringing use of the Software by any of your customers. You are liable for compliance by those third parties with the terms and conditions of this EULA. You will not owe GoReplay LLC any royalties for your distribution of the Software in accordance with this EULA.
1.4 Archive Copies. You are entitled to make a reasonable amount of copies of the Software for archival purposes. Each copy must reproduce all copyright and other proprietary rights notices on or in the Software Product.
1.5 Electronic Delivery. All Software and license documentation shall be delivered by electronic means unless otherwise specified on the applicable invoice or at the time of purchase. Software shall be deemed delivered when it is made available for download by you (“Delivery”).
2. Modifications. GoReplay LLC shall provide you with source code so that you can create Modifications of the original software. “Modification” means: (a) any addition to or deletion from the contents of a file included in the original Software or previous Modifications created by You, or (b) any new file that contains any part of the original Software or previous Modifications. While you retain all rights to any original work authored by you as part of the Modifications, We continue to own all copyright and other intellectual property rights in the Software.
3. Restricted Uses.
3.1 You shall not (and shall not allow any third party to): (a) decompile, disassemble, or otherwise reverse engineer the Software or attempt to reconstruct or discover any source code, underlying ideas, algorithms, file formats or programming interfaces of the Software by any means whatsoever (except and only to the extent that applicable law prohibits or restricts reverse engineering restrictions); (b) distribute, sell, sublicense, rent, lease or use the Software for time sharing, hosting, service provider or like purposes, except as expressly permitted under this Agreement; (c) redistribute the Software or Modifications other than by including the Software or a portion thereof within your own product, which must have substantially different functionality than the Software or Modifications and must not allow any third party to use the Software or Modifications, or any portions thereof, for software development or application development purposes; (d) redistribute the Software as part of a product, "appliance" or "virtual server"; (e) redistribute the Software on any server which is not directly under your control; (f) remove any product identification, proprietary, copyright or other notices contained in the Software; (g) modify any part of the Software, create a derivative work of any part of the Software (except as permitted in Section 4), or incorporate the Software, except to the extent expressly authorized in writing by GoReplay LLC; (h) publicly disseminate performance information or analysis (including, without limitation, benchmarks) from any source relating to the Software; (i) utilize any equipment, device, software, or other means designed to circumvent or remove any form of Source URL or copy protection used by GoReplay LLC in connection with the Software, or use the Software together with any authorization code, Source URL, serial number, or other copy protection device not supplied by GoReplay LLC; (j) use the Software to develop a product which is competitive with any GoReplay LLC product offerings; or (k) use unauthorized Source URLS or keycode(s) or distribute or publish Source URLs or keycode(s), except as may be expressly permitted by GoReplay LLC in writing. If your unique Source URL is ever published, GoReplay LLC reserves the right to terminate your access without notice.
3.2 UNDER NO CIRCUMSTANCES MAY YOU USE THE SOFTWARE AS PART OF A PRODUCT OR SERVICE THAT PROVIDES SIMILAR FUNCTIONALITY TO THE SOFTWARE ITSELF.
The Open Source version of the Software (“LGPL Version”) is licensed
under the terms of the GNU Lesser General Public License versions 3.0
(“LGPL”) and not under this EULA.
4. Ownership. Notwithstanding anything to the contrary contained herein, except for the limited license rights expressly provided herein, GoReplay LLC and its suppliers have and will retain all rights, title and interest (including, without limitation, all patent, copyright, trademark, trade secret and other intellectual property rights) in and to the Software and all copies, modifications and derivative works thereof (including any changes which incorporate any of your ideas, feedback or suggestions). You acknowledge that you are obtaining only a limited license right to the Software, and that irrespective of any use of the words “purchase”, “sale” or like terms hereunder no ownership rights are being conveyed to you under this Agreement or otherwise.
5. Fees and Payment. The Software license fees will be due and payable in full as set forth in the applicable invoice or at the time of purchase. If the Software does not function properly within two weeks of purchase, please contact us within those two weeks for a refund. You shall be responsible for all taxes, withholdings, duties and levies arising from the order (excluding taxes based on the net income of GoReplay LLC).
6. Support, Maintenance and Services. Subject to the terms and conditions of this Agreement, as set forth in your invoice, and as set forth on the GoReplay Pro support page (https://github.com/buger/goreplay/wiki/Pro-Support), support and maintenance services may be included with the purchase of your license subscription.
7. Term of Agreement.
7.1 Term. This Agreement is effective as of the Delivery of the Software and expires at such time as all license and service subscriptions hereunder have expired in accordance with their own terms (the “Term”). For clarification, the term of your license under this Agreement may be perpetual, limited for Evaluation Version, or designated as a fixed-term license in the Invoice, and shall be specified at your time of purchase. Either party may terminate this Agreement (including all related Invoices) if the other party: (a) fails to cure any material breach of this Agreement within thirty (30) days after written notice of such breach, provided that GoReplay LLC may terminate this Agreement immediately upon any breach of Section 3 or if you exceed any other restrictions contained in Section 1, unless otherwise specified in this agreement; (b) ceases operation without a successor; or (c) seeks protection under any bankruptcy, receivership, trust deed, creditors arrangement, composition or comparable proceeding, or if any such proceeding is instituted against such party (and not dismissed within sixty (60) days)). Termination is not an exclusive remedy and the exercise by either party of any remedy under this Agreement will be without prejudice to any other remedies it may have under this Agreement, by law, or otherwise.
7.2 Termination. Upon any termination of this Agreement, you shall cease any and all use of any Software and destroy all copies thereof.
7.3 Expiration of License. Upon the expiration of any term under this Agreement, (a) all Software updates and services pursuant to the license shall cease, (b) you may only continue to run existing installations of the Software, (c) you may not install the Software on any additional Hosts, and (d) any new installation of the Software shall require the purchase of a new license subscription from GoReplay LLC.
8. Disclaimer of Warranties. The Software is provided "as is," with all faults, defects and errors, and without warranty of any kind. GoReplay LLC does not warrant that the Software will be free of bugs, errors, viruses or other defects, and GoReplay LLC shall have no liability of any kind for the use of or inability to use the Software, the Software content or any associated service, and you acknowledge that it is not technically practicable for GoReplay LLC to do so.
To the maximum extent permitted by applicable law, GoReplay LLC disclaims all warranties, express, implied, arising by law or otherwise, regarding the Software, the Software content and their respective performance or suitability for your intended use, including without limitation any implied warranty of merchantability, fitness for a particular purpose.
Notwithstanding the foregoing, GoReplay LLC represents and warrants that it either owns the entire right to, title to, interest in, or has the right to license, the Software, and your proper use of the same will not violate any intellectual property or proprietary rights of another.
9. Limitation of Liability.
In no event will either party be liable for any direct, indirect, consequential, incidental, special, exemplary, or punitive damages or liabilities whatsoever arising from or relating to the Software, the Software content or this Agreement, whether based on contract, tort (including negligence), strict liability or other theory, even if such party has been advised of the possibility of such damages.
In no event will GoReplay LLC liability exceed the Software license price as indicated in the invoice. The existence of more than one claim will not enlarge or extend this limit.
10. Remedies. Your exclusive remedy and GoReplay LLC’ entire liability for breach of this Agreement shall be limited, at GoReplay LLC’ sole and exclusive discretion, to (a) replacement of any defective software or documentation; or (b) refund of the license fee paid to GoReplay LLC, payable in accordance with GoReplay LLC' refund policy.
11. Acknowledgements.
11.1 Consent to the Use of Data. You agree that GoReplay LLC and its affiliates may collect and use technical information gathered as part of the product support services to be used in an anonymized manner. GoReplay LLC may use this information solely to improve products and services and will not disclose this information in a form that personally identifies you.
11.2 Verification. We or a certified auditor acting on our behalf, may, upon its reasonable request and at its expense, audit you with respect to the use of the Software, subject to your reasonable information security and confidentiality protections. Such audit may be conducted by mail, electronic means or through an in-person visit to your place of business. Any such in-person audit shall be conducted during regular business hours at your facilities and shall not unreasonably interfere with your business activities. We shall not remove, copy, or redistribute any electronic material during the course of an audit. If an audit reveals that you are using the Software in a way that is in material violation of the terms of the EULA, then you shall pay our reasonable costs of conducting the audit. In the case of a material violation, you agree to pay Us any amounts owing that are attributable to the unauthorized use. In the alternative, We reserve the right, at our sole option, to terminate the licenses for the Software.
11.3 Government End Users. If the Software and related documentation are supplied to or purchased by or on behalf of the United States Government, then the Software is deemed to be "commercial software" as that term is used in the Federal Acquisition Regulation system. Rights of the United States shall not exceed the minimum rights set forth in FAR 52.227-19 for "restricted computer software". All other terms and conditions of this Agreement apply.
12. Third Party Software. Examples included in Software may provide links to third party libraries or code (collectively “Third Party Software”) to implement various functions. Third Party Software does not comprise part of the Software. In some cases, access to Third Party Software may be included along with the Software delivery as a convenience for demonstration purposes. Such source code and libraries may be included in the “…/examples” source tree delivered with the Software and do not comprise the Software. Licensee acknowledges (1) that some part of Third Party Software may require additional licensing of copyright and patents from the owners of such, and (2) that distribution of any of the Software referencing or including any portion of a Third Party Software may require appropriate licensing from such third parties.
13. Miscellaneous
13.1 Entire Agreement. This Agreement sets forth our entire agreement with respect to the Software and the subject matter hereof and supersedes all prior and contemporaneous understandings and agreements whether written or oral.
13.2 Amendment. GoReplay LLC reserves the right, in its sole discretion, to amend this Agreement from time. Amendments to this Agreement can be located at: https://github.com/buger/goreplay/blob/master/COMM-LICENSE.
13.3 Assignment. You may not assign this Agreement or any of its rights under this Agreement without the prior written consent of GoReplay LLC and any attempted assignment without such consent shall be void.
13.4 Export Compliance. Each party agrees to comply with all applicable laws and regulations, including laws, regulations, orders or other restrictions on export, re-export or redistribution of software.
13.5 Indemnification. You agree to defend, indemnify, and hold harmless GoReplay LLC from and against any lawsuits, claims, losses, damages, fines and expenses (including attorneys' fees and costs) arising out of your use of the Software or breach of this Agreement.
13.6 Governing Law. This Agreement is governed by the laws of the State of Oregon and the United States without regard to conflicts of laws provisions thereof, and without regard to the United Nations Convention on the International Sale of Goods or the Uniform Computer Information Transactions Act, as currently enacted by any jurisdiction or as may be codified or amended from time to time by any jurisdiction. The jurisdiction and venue for actions related to the subject matter hereof shall be the state of Oregon and United States federal courts located in Portland, Oregon, and both parties hereby submit to the personal jurisdiction of such courts.
13.7 Attorneys’ Fees and Costs. The prevailing party in any action to enforce this Agreement will be entitled to recover its attorneys’ fees and costs in connection with such action.
13.8 Severability. If any provision of this Agreement is held by a court of competent jurisdiction to be invalid, illegal, or unenforceable, the remainder of this Agreement will remain in full force and effect.
13.9 Waiver. Failure or neglect by either party to enforce at any time any of the provisions of this licence Agreement shall not be construed or deemed to be a waiver of that party's rights under this Agreement.
13.10 Headings. The headings of sections and paragraphs of this Agreement are for convenience of reference only and are not intended to restrict, affect or be of any weight in the interpretation or construction of the provisions of such sections or paragraphs.
14. Contact Information. If you have any questions about this EULA, or if you want to contact GoReplay LLC for any reason, please direct correspondence to info@gortool.com
+8 -4
View File
@@ -1,7 +1,11 @@
FROM google/golang
FROM alpine:3.12 as builder
WORKDIR /gopath/src/gor
ARG RELEASE_VERSION
ADD . /gopath/src/gor
RUN apk add --no-cache ca-certificates openssl
RUN wget https://github.com/buger/goreplay/releases/download/${RELEASE_VERSION}/gor_${RELEASE_VERSION}_x64.tar.gz -O gor.tar.gz
RUN tar xzf gor.tar.gz
RUN go get
FROM scratch
COPY --from=builder /gor .
ENTRYPOINT ["./gor"]
+14
View File
@@ -0,0 +1,14 @@
FROM golang:1.14
RUN apt-get update && apt-get install ruby vim-common -y
RUN apt-get install flex bison -y
RUN wget http://www.tcpdump.org/release/libpcap-1.9.1.tar.gz && tar xzf libpcap-1.9.1.tar.gz && cd libpcap-1.9.1 && ./configure && make install
RUN go get github.com/google/gopacket
RUN go get -u golang.org/x/lint/golint
WORKDIR /go/src/github.com/buger/goreplay/
ADD . /go/src/github.com/buger/goreplay/
RUN go get
+20 -183
View File
@@ -1,191 +1,28 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (c) 2011-present Leonid Bugaev
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Portions of this software are licensed as follows:
1. Definitions.
* All content residing under the "doc/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license".
* Files with the following names "pro.go", "s3_reader.go", "output_s3.go" are released under the commercial license specified in "COMM-LICENSE" file.
* Content outside of the above mentioned directories or restrictions above is available under the "LGPLv3" license as defined below.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
GoReplay is an Open Source project licensed under the terms of
the LGPLv3 license. Please see <http://www.gnu.org/licenses/lgpl-3.0.html>
for license text.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
As a special exception to the GNU Lesser General Public License version 3
("LGPL3"), the copyright holders of this Library give you permission to
convey to a third party a Combined Work that links statically or dynamically
to this Library without providing any Minimal Corresponding Source or
Minimal Application Code as set out in 4d or providing the installation
information set out in section 4e, provided that you comply with the other
provisions of LGPL3 and provided that you meet, for the Application the
terms and conditions of the license(s) which apply to the Application.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
TLDR: You are free to use Gor subpackages like `byteutils` or `proto` in your commercial projects.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
GoReplay Pro has a commercial-friendly license allowing private forks
and modifications of GoReplay. Please see https://goreplay.org/pro.html for
more detail. You can find the commercial license terms in COMM-LICENSE.
+139 -18
View File
@@ -1,25 +1,146 @@
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 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 = $(shell ls -1 *.go | grep -v _test.go)
SOURCE_PATH = /go/src/github.com/buger/goreplay/
PORT = 8000
FADDR = :8000
CONTAINER=gor
PREFIX=
RUN = docker run -v `pwd`:$(SOURCE_PATH) -e AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) -e AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) -p 0.0.0.0:$(PORT):$(PORT) -t -i $(CONTAINER)
BENCHMARK = BenchmarkRAWInput
TEST = TestRawListenerBench
BIN_NAME = gor
VERSION = DEV-$(shell date +%s)
LDFLAGS = -ldflags "-X main.VERSION=$(VERSION)$(PREFIX) -extldflags \"-static\" -X main.DEMO=$(DEMO)"
MAC_LDFLAGS = -ldflags "-X main.VERSION=$(VERSION)$(PREFIX) -X main.DEMO=$(DEMO)"
FADDR = ":8000"
all: build-x86 build-x64
FPMCOMMON= \
--name goreplay \
--description "GoReplay is an open-source network monitoring tool which can record your live traffic, and use it for shadowing, load testing, monitoring and detailed analysis." \
-v $(VERSION) \
--vendor "Leonid Bugaev" \
-m "<support@goreplay.org>" \
--url "https://goreplay.org" \
-s dir \
-C /tmp/gor-build \
build-x64:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build
tar -czf gor_x64.tar.gz gor
rm gor
release: release-x64 release-mac
build-x86:
GOOS=linux GOARCH=386 CGO_ENABLED=0 go build
tar -czf gor_x86.tar.gz gor
rm gor
release-bin:
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i $(CONTAINER) go build -o $(BIN_NAME) -tags netgo $(LDFLAGS)
dbuild:
docker build -t gor .
release-bin-mac:
GOOS=darwin go build -o $(BIN_NAME) $(MAC_LDFLAGS)
dtest:
docker run -v `pwd`:/gopath/src/gor -t -i gor go test -v
release-x64:
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i $(CONTAINER) go build -o $(BIN_NAME) -tags netgo $(LDFLAGS)
tar -czf gor_$(VERSION)$(PREFIX)_x64.tar.gz $(BIN_NAME)
mkdir -p /tmp/gor-build
mv ./$(BIN_NAME) /tmp/gor-build/$(BIN_NAME)
cd /tmp/gor-build
rm -f goreplay_$(VERSION)_amd64.deb
rm -f goreplay-$(VERSION)-1.x86_64.rpm
fpm $(FPMCOMMON) -a amd64 -t deb ./=/usr/local/bin
fpm $(FPMCOMMON) -a amd64 -t rpm ./=/usr/local/bin
rm -rf /tmp/gor-build
drun:
docker run -v `pwd`:/gopath/src/gor -t -i gor go run $(SOURCE) --input-dummy=0 --output-dummy=0 --verbose
release-x86:
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=386 -i $(CONTAINER) go build -o $(BIN_NAME) -tags netgo $(LDFLAGS)
tar -czf gor_$(VERSION)$(PREFIX)_x86.tar.gz $(BIN_NAME)
rm $(BIN_NAME)
dbash:
docker run -v `pwd`:/gopath/src/gor -t -i gor /bin/bash
release-mac:
go build -o $(BIN_NAME) $(MAC_LDFLAGS)
tar -czf gor_$(VERSION)$(PREFIX)_mac.tar.gz $(BIN_NAME)
mkdir -p /tmp/gor-build
mv ./$(BIN_NAME) /tmp/gor-build/$(BIN_NAME)
cd /tmp/gor-build
rm -f goreplay-$(VERSION).pkg
fpm $(FPMCOMMON) -a amd64 -t osxpkg ./=/usr/local/bin
rm -rf /tmp/gor-build
install:
go install $(MAC_LDFLAGS)
build:
docker build -t $(CONTAINER) -f Dockerfile.dev .
profile:
go build && ./$(BIN_NAME) --output-http="http://localhost:9000" --input-dummy 0 --input-raw :9000 --input-http :9000 --memprofile=./mem.out --cpuprofile=./cpu.out --stats --output-http-stats --output-http-timeout 100ms
lint:
$(RUN) golint $(PKG)
race:
$(RUN) go test ./... $(ARGS) -v -race -timeout 15s
test:
$(RUN) go test ./. -timeout 120s $(LDFLAGS) $(ARGS) -v
test_all:
$(RUN) go test ./... -timeout 120s $(LDFLAGS) $(ARGS) -v
testone:
$(RUN) go test ./. -timeout 60s $(LDFLAGS) -run $(TEST) $(ARGS) -v
cover:
$(RUN) go test $(ARGS) -race -v -timeout 15s -coverprofile=coverage.out
go tool cover -html=coverage.out
fmt:
$(RUN) gofmt -w -s ./..
vet:
$(RUN) go vet
bench:
$(RUN) go test $(LDFLAGS) -v -run NOT_EXISTING -bench $(BENCHMARK) -benchtime 5s
profile_test:
$(RUN) go test $(LDFLAGS) -run $(TEST) ./capture/. $(ARGS) -memprofile mem.mprof -cpuprofile cpu.out
$(RUN) go test $(LDFLAGS) -run $(TEST) ./capture/. $(ARGS) -c
# Used mainly for debugging, because docker container do not have access to parent machine ports
run:
$(RUN) go run $(LDFLAGS) $(SOURCE) --input-dummy=0 --output-http="http://localhost:9000" --input-raw-track-response --input-raw 127.0.0.1:9000 --verbose 0 --middleware "./examples/middleware/echo.sh" --output-file requests.gor
run-2:
$(RUN) go run $(LDFLAGS) $(SOURCE) --input-raw :8000 --input-raw-bpf-filter "dst port 8000" --output-stdout --output-http "http://localhost:8000" --input-dummy=0
run-3:
sudo -E go run $(SOURCE) --input-tcp :27001 --output-stdout
run-arg:
sudo -E go run $(SOURCE) $(ARGS)
file-server:
go run $(SOURCE) file-server $(FADDR)
readpcap:
go run $(SOURCE) --input-raw $(FILE) --input-raw-track-response --input-raw-engine pcap_file --output-stdout
record:
$(RUN) go run $(SOURCE) --input-dummy=0 --output-file=requests.gor --verbose --debug
replay:
$(RUN) go run $(SOURCE) --input-file=requests.bin --output-tcp=:9000 --verbose -h
bash:
$(RUN) /bin/bash
FPMCOMMON= \
--name gor \
--description "GoReplay is an open-source network monitoring tool which can record your live traffic, and use it for shadowing, load testing, monitoring and detailed analysis." \
-v $(VERSION) \
--vendor "Leonid Bugaev" \
-m "<support@goreplay.org>" \
--url "https://goreplay.org" \
-s dir \
-C /tmp/gor-build \
build_packages:
mkdir -p /tmp/gor-build
go build -i -o /tmp/gor-build/$(BIN_NAME)
fpm $(FPMCOMMON) -a amd64 -t deb ./=/usr/local/bin
fpm $(FPMCOMMON) -a amd64 -t rpm ./=/usr/local/bin
+73 -357
View File
@@ -1,357 +1,73 @@
[![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
Gor is a simple http traffic replication tool written in Go.
Its main goal is to replay traffic from production servers to staging and dev environments.
Now you can test your code on real user sessions in an automated and repeatable fashion.
**No more falling down in production!**
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)
## Examples
### Capture traffic from port
```bash
# Run on servers where you want to catch traffic. You can run it on each `web` machine.
sudo gor --input-raw :80 --output-tcp replay.local:28020
# Replay server (replay.local).
gor --input-tcp replay.local:28020 --output-http http://staging.com
```
### Using 1 Gor instance for both listening and replaying
It's recommended to use separate server for replaying traffic, but if you have enough CPU resources you can use single Gor instance.
```
sudo gor --input-raw :80 --output-http "http://staging.com"
```
## Advanced use
### Rate limiting
Both replay and listener support rate limiting. It can be useful if you want
forward only part of production traffic and not overload your staging
environment.
Current throttling works like this: If for current second it reached specified requests limit - disregard the rest, on next second counter reseted.
You can specify your desired requests per second using the
"|" operator after the server address:
#### Limiting replay
```
# staging.server will not get more than 10 requests per second
gor --input-tcp :28020 --output-http "http://staging.com|10"
```
#### Limiting listener
```
# replay server will not get more than 10 requests per second
# useful for high-load environments
gor --input-raw :80 --output-tcp "replay.local:28020|10"
```
#### Match on regexp of url
```
# only forward requests being sent to the api... domains
gor --input-raw :8080 --output-http staging.com --output-http-url-regexp ^www.
```
#### 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
```
#### 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:
```
gor --input-tcp :28020 --output-http "http://staging.com" --output-http "http://dev.com" --split-output true
```
### Saving requests to file
You can save requests to file, and replay them later:
```
# write to file
gor --input-raw :80 --output-file requests.gor
# read from file
gor --input-file requests.gor --output-http "http://staging.com"
```
**Note:** Replay will preserve the original time differences between requests.
### Injecting headers
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:
```
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
```
### Basic Auth
If your development or staging environment is protected by Basic Authentication then those credentials can be injected in during the replay:
```
gor --input-raw :80 --output-http "http://user:pass@staging .com"
```
Note: This will overwrite any Authorization headers in the original request.
#### 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
### ElasticSearch
For deep response analyze based on url, cookie, user-agent and etc. you can export response metadata to ElasticSearch. See [ELASTICSEARCH.md](ELASTICSEARCH.md) for more details.
```
gor --input-tcp :80 --output-http "http://staging.com" --output-http-elasticsearch "es_host:api_port/index_name"
```
## Additional help
Feel free to ask question directly by email or by creating github issue.
## Latest releases (including binaries)
https://github.com/buger/gor/releases
## Command line reference
`gor -h` output:
```
-cpuprofile="": write cpu profile to file
-memprofile="": write memory profile to this file
-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-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
-output-dummy=[]: Used for testing inputs. Just prints data coming from inputs.
-output-file=[]: Write incoming requests to file:
gor --input-raw :80 --output-file ./requests.gor
-output-http=[]: Forwards incoming requests to given http address.
# Redirect all incoming requests to staging.com address
gor --input-raw :80 --output-http http://staging.com
-output-http-elasticsearch="": Send request and response stats to ElasticSearch:
gor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'
-output-http-header=[]: 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.
-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
```
## Building from source
1. Setup standard Go environment http://golang.org/doc/code.html and ensure that $GOPATH environment variable properly set.
2. `go get github.com/buger/gor`.
3. `cd $GOPATH/src/github.com/buger/gor`
4. `go build` to get binary, or `go test` to run tests
## Development
Project contains Docker environment.
1. Build container: `make dbuild`
2. Run tests: `make dtest`
3. Bash access to container: `make dbash`. Inside container you have python to run simple web server `python -m SimpleHTTPServer 8080` and `curl` to make http requests.
## Questions and support
All bug-reports and suggestions should go though Github Issues or our [Google Group](https://groups.google.com/forum/#!forum/gor-users). Or you can just send email to gor-users@googlegroups.com
If you have some private questions you can send direct mail to leonsbox@gmail.com
## FAQ
### What OS are supported?
For now only Linux based. *BSD (including MacOS is not supported yet, check https://github.com/buger/gor/issues/22 for details)
### Why does the `--input-raw` requires sudo or root access?
Listener works by sniffing traffic from a given port. It's accessible
only by using sudo or root access.
### I'm getting 'too many open files' error
Typical linux shell has a small open files soft limit at 1024. You can easily raise that when you do this before starting your gor replay process:
ulimit -n 64000
More about ulimit: http://blog.thecodingmachine.com/content/solving-too-many-open-files-exception-red5-or-any-other-application
### 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
To achieve the top most performance you should tune the source server system limits:
net.ipv4.tcp_max_tw_buckets = 65536
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 0
net.ipv4.tcp_max_syn_backlog = 131072
net.ipv4.tcp_syn_retries = 3
net.ipv4.tcp_synack_retries = 3
net.ipv4.tcp_retries1 = 3
net.ipv4.tcp_retries2 = 8
net.ipv4.tcp_rmem = 16384 174760 349520
net.ipv4.tcp_wmem = 16384 131072 262144
net.ipv4.tcp_mem = 262144 524288 1048576
net.ipv4.tcp_max_orphans = 65536
net.ipv4.tcp_fin_timeout = 10
net.ipv4.tcp_low_latency = 1
net.ipv4.tcp_syncookies = 0
## Contributing
1. Fork it
2. Create your feature branch (git checkout -b my-new-feature)
3. Commit your changes (git commit -am 'Added some feature')
4. Push to the branch (git push origin my-new-feature)
5. Create new Pull Request
## Companies using Gor
* [Granify](http://granify.com)
* [GOV.UK](https://www.gov.uk) ([Government Digital Service](http://digital.cabinetoffice.gov.uk/))
* [theguardian.com](http://theguardian.com)
* [TomTom](http://www.tomtom.com/)
* To add your company drop me a line to github.com/buger or leonsbox@gmail.com
[![GitHub release](https://img.shields.io/github/release/buger/gor.svg?maxAge=3600)](https://github.com/buger/goreplay/releases) [![codebeat](https://codebeat.co/badges/6427d589-a78e-416c-a546-d299b4089893)](https://codebeat.co/projects/github-com-buger-gor) [![Go Report Card](https://goreportcard.com/badge/github.com/buger/gor)](https://goreportcard.com/report/github.com/buger/gor) [![Join the chat at https://gitter.im/buger/gor](https://badges.gitter.im/buger/gor.svg)](https://gitter.im/buger/gor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com)
![Go Replay](http://i.imgur.com/ZG2ki5n.png)
## https://goreplay.org/
GoReplay is an open-source network monitoring tool which can record your live traffic, and use it for shadowing, load testing, monitoring and detailed analysis.
## About
As your application grows, the effort required to test it also grows exponentially. GoReplay offers you the simple idea of reusing your existing traffic for testing, which makes it incredibly powerful. Our state of art technique allows you to analyze and record your application traffic without affecting it. This eliminates the risks that come with putting a third party component in the critical path.
GoReplay increases your confidence in code deployments, configuration and infrastructure changes.
GoReplay offers unique approach for shadowing. Instead of being a proxy, GoReplay in background listen for traffic on your network interface, requiring no changes in your production infrastructure, rather then running GoReplay daemon on the same machine as your service.
![Diagram](https://i.imgur.com/IN2xfDm.png)
Check [latest documentation](http://github.com/buger/goreplay/wiki).
## Installation
Download latest binary from https://github.com/buger/goreplay/releases or [compile by yourself](https://github.com/buger/goreplay/wiki/Compilation).
## Getting started
The most basic setup will be `sudo ./gor --input-raw :8000 --output-stdout` which acts like tcpdump.
If you already have test environment you can start replaying: `sudo ./gor --input-raw :8000 --output-http http://staging.env`.
See the our [documentation](https://github.com/buger/goreplay/wiki/) and [Getting started](https://github.com/buger/goreplay/wiki/Getting-Started) page for more info.
## Newsletter
Subscribe to our [newsletter](https://www.getdrip.com/forms/89690474/submissions/new) to stay informed about the latest features and changes to Gor project.
## Want to Upgrade?
We have created a [GoReplay PRO](https://goreplay.org/pro.html) extension which provides additional features such as support for binary protocols like Thrift or ProtocolBuffers, saving and replaying from cloud storage, TCP sessions replication, etc. The PRO version also includes a commercial-friendly license, dedicated support, and it also allows you to support high-quality open source development.
## Problems?
If you have a problem, please review the [FAQ](https://github.com/buger/goreplay/wiki/FAQ) and [Troubleshooting](https://github.com/buger/goreplay/wiki/Troubleshooting) wiki pages. Searching the [issues](https://github.com/buger/goreplay/issues) for your problem is also a good idea.
All bug-reports and suggestions should go through Github Issues or our [Google Group](https://groups.google.com/forum/#!forum/gor-users) (you can just send email to gor-users@googlegroups.com).
If you have a private question feel free to send email to support@gortool.com.
## Contributing
1. Fork it
2. Create your feature branch (git checkout -b my-new-feature)
3. Commit your changes (git commit -am 'Added some feature')
4. Push to the branch (git push origin my-new-feature)
5. Create new Pull Request
## Companies using Gor
* [GOV.UK](https://www.gov.uk) - UK Government Digital Service
* [theguardian.com](http://theguardian.com) - Most popular online newspaper in the UK
* [TomTom](http://www.tomtom.com/) - Global leader in navigation, traffic and map products, GPS Sport Watches and fleet management solutions.
* [3SCALE](http://www.3scale.net/) - API infrastructure to manage your APIs for internal or external users
* [Optionlab](http://www.opinionlab.com) - Optimize customer experience and drive engagement across multiple channels
* [TubeMogul](http://tubemogul.com) - Software for Brand Advertising
* [Videology](http://www.videologygroup.com/) - Video advertising platform
* [ForeksMobile](http://foreksmobile.com/) - One of the leading financial application development company in Turkey
* [Granify](http://granify.com) - AI backed SaaS solution that enables online retailers to maximise their sales
* And many more!
If you are using Gor, we are happy to add you to the list and share your story, just write to: hello@goreplay.org
## Author
Leonid Bugaev, [@buger](https://twitter.com/buger), https://leonsbox.com
+52
View File
@@ -0,0 +1,52 @@
// Package byteutils provides helpers for working with byte slices
package byteutils
import (
"unsafe"
)
// 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
}
if lenDiff < 0 {
copy(a[from:], new)
copy(a[from+len(new):], a[to:])
return a[:len(a)+lenDiff]
}
// same size
copy(a[from:], new)
return a
}
// SliceToString preferred for large body payload (zero allocation and faster)
func SliceToString(buf []byte) string {
return *(*string)(unsafe.Pointer(&buf))
}
+41
View File
@@ -0,0 +1,41 @@
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")
}
}
func BenchmarkStringtoSlice(b *testing.B) {
var s string
var buf [1 << 20]byte
for i := 0; i < b.N; i++ {
s = SliceToString(buf[:])
}
_ = s // avoid gc to optimize away the loop body
}
+546
View File
@@ -0,0 +1,546 @@
package capture
import (
"context"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"runtime"
"sync"
"time"
"github.com/buger/goreplay/size"
"github.com/buger/goreplay/tcp"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"github.com/pbnjay/memory"
"golang.org/x/sys/unix"
)
// PacketHandler is a function that is used to handle packets
type PacketHandler func(*tcp.Packet)
// PcapOptions options that can be set on a pcap capture handle,
// these options take effect on inactive pcap handles
type PcapOptions struct {
BufferTimeout time.Duration `json:"input-raw-buffer-timeout"`
TimestampType string `json:"input-raw-timestamp-type"`
BPFFilter string `json:"input-raw-bpf-filter"`
BufferSize size.Size `json:"input-raw-buffer-size"`
Promiscuous bool `json:"input-raw-promisc"`
Monitor bool `json:"input-raw-monitor"`
Snaplen bool `json:"input-raw-override-snaplen"`
}
// Listener handle traffic capture, this is its representation.
type Listener struct {
sync.Mutex
Transport string // transport layer default to tcp
Activate func() error // function is used to activate the engine. it must be called before reading packets
Handles map[string]gopacket.ZeroCopyPacketDataSource
Interfaces []net.Interface
loopIndex int
Reading chan bool // this channel is closed when the listener has started reading packets
PcapOptions
Engine EngineType
port uint16 // src or/and dst port
trackResponse bool
host string // pcap file name or interface (name, hardware addr, index or ip address)
maxAllowedMem int
closeDone chan struct{}
quit chan struct{}
}
// EngineType ...
type EngineType uint8
// Available engines for intercepting traffic
const (
EnginePcap EngineType = 1 << iota
EnginePcapFile
EngineRawSocket
)
// Set is here so that EngineType can implement flag.Var
func (eng *EngineType) Set(v string) error {
switch v {
case "", "libpcap":
*eng = EnginePcap
case "pcap_file":
*eng = EnginePcapFile
case "raw_socket", "af_packet":
*eng = EngineRawSocket
default:
return fmt.Errorf("invalid engine %s", v)
}
return nil
}
func (eng *EngineType) String() (e string) {
switch *eng {
case EnginePcapFile:
e = "pcap_file"
case EnginePcap:
e = "libpcap"
case EngineRawSocket:
e = "raw_socket"
default:
e = ""
}
return e
}
// NewListener creates and initialize a new Listener. if transport or/and engine are invalid/unsupported
// is "tcp" and "pcap", are assumed. l.Engine and l.Transport can help to get the values used.
// if there is an error it will be associated with getting network interfaces
func NewListener(host string, port uint16, transport string, engine EngineType, trackResponse bool, maxAllowedMem int) (l *Listener, err error) {
l = &Listener{}
l.host = host
l.port = port
l.Transport = "tcp"
if transport != "" {
l.Transport = transport
}
l.Handles = make(map[string]gopacket.ZeroCopyPacketDataSource)
l.trackResponse = trackResponse
l.closeDone = make(chan struct{})
l.quit = make(chan struct{})
l.Reading = make(chan bool)
l.maxAllowedMem = maxAllowedMem
switch engine {
default:
l.Engine = EnginePcap
l.Activate = l.activatePcap
case EngineRawSocket:
l.Engine = EngineRawSocket
l.Activate = l.activateRawSocket
case EnginePcapFile:
l.Engine = EnginePcapFile
l.Activate = l.activatePcapFile
return
}
err = l.setInterfaces()
if err != nil {
return nil, err
}
return
}
// SetPcapOptions set pcap options for all yet to be actived pcap handles
// setting this on already activated handles will not have any effect
func (l *Listener) SetPcapOptions(opts PcapOptions) {
l.PcapOptions = opts
}
// Listen listens for packets from the handles, and call handler on every packet received
// until the context done signal is sent or there is unrecoverable error on all handles.
// this function must be called after activating pcap handles
func (l *Listener) Listen(ctx context.Context, handler PacketHandler) (err error) {
l.read(handler)
done := ctx.Done()
select {
case <-done:
close(l.quit) // signal close on all handles
<-l.closeDone // wait all handles to be closed
err = ctx.Err()
case <-l.closeDone: // all handles closed voluntarily
}
return
}
// ListenBackground is like listen but can run concurrently and signal error through channel
func (l *Listener) ListenBackground(ctx context.Context, handler PacketHandler) chan error {
err := make(chan error, 1)
go func() {
defer close(err)
if e := l.Listen(ctx, handler); err != nil {
err <- e
}
}()
return err
}
// Filter returns automatic filter applied by goreplay
// to a pcap handle of a specific interface
func (l *Listener) Filter(ifi net.Interface) (filter string) {
// https://www.tcpdump.org/manpages/pcap-filter.7.html
port := fmt.Sprintf("portrange 0-%d", 1<<16-1)
if l.port != 0 {
port = fmt.Sprintf("port %d", l.port)
}
filter = fmt.Sprintf("%s dst %s", l.Transport, port)
if l.trackResponse {
filter = fmt.Sprintf("%s %s", l.Transport, port)
}
if listenAll(l.host) || isDevice(l.host, ifi) {
return "(" + filter + ")"
}
filter = fmt.Sprintf("(host %s and (%s))", l.host, filter)
log.Println("BPF filter: " + filter)
return
}
// PcapDumpHandler returns a handler to write packet data in PCAP
// format, See http://wiki.wireshark.org/Development/LibpcapFileFormathandler.
// if link layer is invalid Ethernet is assumed
// func PcapDumpHandler(file *os.File, link layers.LinkType) (handler func(packet *tcp.Packet) error, err error) {
// if link.String() == "" {
// link = layers.LinkTypeEthernet
// }
// w := NewWriterNanos(file)
// err = w.WriteFileHeader(64<<10, link)
// if err != nil {
// return nil, err
// }
// return func(packet *tcp.Packet) error {
// return w.WritePacket(*packet.Info, packet.Data)
// }, nil
// }
// PcapHandle returns new pcap Handle from dev on success.
// this function should be called after setting all necessary options for this listener
func (l *Listener) PcapHandle(ifi net.Interface) (handle *pcap.Handle, err error) {
var inactive *pcap.InactiveHandle
inactive, err = pcap.NewInactiveHandle(ifi.Name)
if err != nil {
return nil, fmt.Errorf("inactive handle error: %q, interface: %q", err, ifi.Name)
}
defer inactive.CleanUp()
if l.TimestampType != "" {
var ts pcap.TimestampSource
ts, err = pcap.TimestampSourceFromString(l.TimestampType)
err = inactive.SetTimestampSource(ts)
if err != nil {
return nil, fmt.Errorf("%q: supported timestamps: %q, interface: %q", err, inactive.SupportedTimestamps(), ifi.Name)
}
}
if l.Promiscuous {
if err = inactive.SetPromisc(l.Promiscuous); err != nil {
return nil, fmt.Errorf("promiscuous mode error: %q, interface: %q", err, ifi.Name)
}
}
if l.Monitor {
if err = inactive.SetRFMon(l.Monitor); err != nil && !errors.Is(err, pcap.CannotSetRFMon) {
return nil, fmt.Errorf("monitor mode error: %q, interface: %q", err, ifi.Name)
}
}
var snap int
if l.Snaplen {
snap = 64<<10 + 200
} else if ifi.MTU > 0 {
snap = ifi.MTU + 200
}
err = inactive.SetSnapLen(snap)
if err != nil {
return nil, fmt.Errorf("snapshot length error: %q, interface: %q", err, ifi.Name)
}
if l.BufferSize > 0 {
err = inactive.SetBufferSize(int(l.BufferSize))
if err != nil {
return nil, fmt.Errorf("handle buffer size error: %q, interface: %q", err, ifi.Name)
}
}
if l.BufferTimeout == 0 {
l.BufferTimeout = pcap.BlockForever
}
err = inactive.SetTimeout(l.BufferTimeout)
if err != nil {
return nil, fmt.Errorf("handle buffer timeout error: %q, interface: %q", err, ifi.Name)
}
handle, err = inactive.Activate()
if err != nil {
return nil, fmt.Errorf("PCAP Activate device error: %q, interface: %q", err, ifi.Name)
}
if l.BPFFilter != "" {
if l.BPFFilter[0] != '(' || l.BPFFilter[len(l.BPFFilter)-1] != ')' {
l.BPFFilter = "(" + l.BPFFilter + ")"
}
} else {
l.BPFFilter = l.Filter(ifi)
}
fmt.Println("Interface:", ifi.Name, ". BPF Filter:", l.BPFFilter)
err = handle.SetBPFFilter(l.BPFFilter)
if err != nil {
handle.Close()
return nil, fmt.Errorf("BPF filter error: %q%s, interface: %q", err, l.BPFFilter, ifi.Name)
}
return
}
// SocketHandle returns new unix ethernet handle associated with this listener settings
func (l *Listener) SocketHandle(ifi net.Interface) (handle Socket, err error) {
handle, err = NewSocket(ifi)
if err != nil {
return nil, fmt.Errorf("sock raw error: %q, interface: %q", err, ifi.Name)
}
if err = handle.SetPromiscuous(l.Promiscuous || l.Monitor); err != nil {
return nil, fmt.Errorf("promiscuous mode error: %q, interface: %q", err, ifi.Name)
}
if l.BPFFilter != "" {
if l.BPFFilter[0] != '(' || l.BPFFilter[len(l.BPFFilter)-1] != ')' {
l.BPFFilter = "(" + l.BPFFilter + ")"
}
} else {
l.BPFFilter = l.Filter(ifi)
}
fmt.Println("BPF Filter: ", l.BPFFilter)
if err = handle.SetBPFFilter(l.BPFFilter); err != nil {
handle.Close()
return nil, fmt.Errorf("BPF filter error: %q%s, interface: %q", err, l.BPFFilter, ifi.Name)
}
handle.SetLoopbackIndex(int32(l.loopIndex))
return
}
func (l *Listener) read(handler PacketHandler) {
l.Lock()
defer l.Unlock()
for key, handle := range l.Handles {
go func(key string, hndl gopacket.ZeroCopyPacketDataSource) {
defer l.closeHandles(key)
linkSize := 14
linkType := int(layers.LinkTypeEthernet)
if _, ok := hndl.(*pcap.Handle); ok {
linkType = int(hndl.(*pcap.Handle).LinkType())
linkSize, ok = pcapLinkTypeLength(linkType)
if !ok {
if os.Getenv("GORDEBUG") != "0" {
log.Printf("can not identify link type of an interface '%s'\n", key)
}
return // can't find the linktype size
}
}
ticker := time.NewTicker(time.Second)
for {
select {
case <-l.quit:
return
case <-ticker.C:
if l.maxAllowedMem > 0 {
usedMemory, _ := memUsage()
if int(usedMemory) > l.maxAllowedMem {
log.Printf("[WARNING] Using more memory (%vmb) than allowed (%vmb). Enabling throttling for 1 second\n", bToMb(usedMemory), bToMb(uint64(l.maxAllowedMem)))
time.Sleep(time.Second)
}
}
default:
data, ci, err := hndl.ZeroCopyReadPacketData()
if err == nil {
pckt, err := tcp.ParsePacket(data, linkType, linkSize, &ci)
if err == nil {
handler(pckt)
}
continue
}
if enext, ok := err.(pcap.NextError); ok && enext == pcap.NextErrorTimeoutExpired {
continue
}
if eno, ok := err.(unix.Errno); ok && eno.Temporary() {
continue
}
if enet, ok := err.(*net.OpError); ok && (enet.Temporary() || enet.Timeout()) {
continue
}
if err == io.EOF || err == io.ErrClosedPipe {
log.Printf("stopped reading from %s interface with error %s\n", key, err)
return
}
log.Printf("stopped reading from %s interface with error %s\n", key, err)
return
}
}
}(key, handle)
}
close(l.Reading)
}
func (l *Listener) closeHandles(key string) {
l.Lock()
defer l.Unlock()
if handle, ok := l.Handles[key]; ok {
if _, ok = handle.(Socket); ok {
handle.(Socket).Close()
} else {
handle.(*pcap.Handle).Close()
}
delete(l.Handles, key)
if len(l.Handles) == 0 {
close(l.closeDone)
}
}
}
func (l *Listener) activatePcap() error {
var e error
var msg string
for _, ifi := range l.Interfaces {
var handle *pcap.Handle
handle, e = l.PcapHandle(ifi)
if e != nil {
msg += ("\n" + e.Error())
continue
}
l.Handles[ifi.Name] = handle
}
if len(l.Handles) == 0 {
return fmt.Errorf("pcap handles error:%s", msg)
}
return nil
}
func (l *Listener) activateRawSocket() error {
if runtime.GOOS != "linux" {
return fmt.Errorf("sock_raw is not stabilized on OS other than linux")
}
var msg string
var e error
for _, ifi := range l.Interfaces {
var handle Socket
handle, e = l.SocketHandle(ifi)
if e != nil {
msg += ("\n" + e.Error())
continue
}
l.Handles[ifi.Name] = handle
}
if len(l.Handles) == 0 {
return fmt.Errorf("raw socket handles error:%s", msg)
}
return nil
}
func (l *Listener) activatePcapFile() (err error) {
var handle *pcap.Handle
var e error
if handle, e = pcap.OpenOffline(l.host); e != nil {
return fmt.Errorf("open pcap file error: %q", e)
}
if l.BPFFilter != "" {
if l.BPFFilter[0] != '(' || l.BPFFilter[len(l.BPFFilter)-1] != ')' {
l.BPFFilter = "(" + l.BPFFilter + ")"
}
} else {
addr := l.host
l.host = ""
l.BPFFilter = l.Filter(net.Interface{})
l.host = addr
}
if e = handle.SetBPFFilter(l.BPFFilter); e != nil {
handle.Close()
return fmt.Errorf("BPF filter error: %q, filter: %s", e, l.BPFFilter)
}
l.Handles["pcap_file"] = handle
return
}
func (l *Listener) setInterfaces() (err error) {
var ifis []net.Interface
ifis, err = net.Interfaces()
if err != nil {
return
}
for i := range ifis {
if ifis[i].Flags&net.FlagLoopback != 0 {
l.loopIndex = ifis[i].Index
}
if ifis[i].Flags&net.FlagUp == 0 {
continue
}
if isDevice(l.host, ifis[i]) {
l.Interfaces = []net.Interface{ifis[i]}
return
}
addrs, e := ifis[i].Addrs()
if e != nil {
// don't give up on a failure from a single interface
continue
}
for _, addr := range addrs {
if cutMask(addr) == l.host {
l.Interfaces = []net.Interface{ifis[i]}
return
}
}
}
l.Interfaces = ifis
return
}
func cutMask(addr net.Addr) string {
mask := addr.String()
for i, v := range mask {
if v == '/' {
return mask[:i]
}
}
return mask
}
func isDevice(addr string, ifi net.Interface) bool {
return addr == ifi.Name || addr == fmt.Sprintf("%d", ifi.Index) || (addr != "" && addr == ifi.HardwareAddr.String())
}
func listenAll(addr string) bool {
switch addr {
case "", "0.0.0.0", "[::]", "::":
return true
}
return false
}
func pcapLinkTypeLength(lType int) (int, bool) {
switch layers.LinkType(lType) {
case layers.LinkTypeEthernet:
return 14, true
case layers.LinkTypeNull, layers.LinkTypeLoop:
return 4, true
case layers.LinkTypeRaw, 12, 14:
return 0, true
case layers.LinkTypeIPv4, layers.LinkTypeIPv6:
// (TODO:) look out for IP encapsulation?
return 0, true
case layers.LinkTypeLinuxSLL:
return 16, true
case layers.LinkTypeFDDI:
return 13, true
case 226 /*DLT_IPNET*/ :
// https://www.tcpdump.org/linktypes/LINKTYPE_IPNET.html
return 24, true
default:
return 0, false
}
}
func memUsage() (uint64, uint64) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
// fmt.Printf("Current memory usage: ")
// // For info on each, see: https://golang.org/pkg/runtime/#MemStats
// fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc))
// fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc))
// fmt.Printf("\tSys = %v MiB", bToMb(m.Sys))
// fmt.Printf("\tNumGC = %v\n", m.NumGC)
// fmt.Printf("Total available memory %v MiB\n", bToMb(memory.TotalMemory()))
// fmt.Printf("Currently using %.2f %% of memory\n", 100*float64(m.Alloc+m.Sys)/float64(memory.TotalMemory()))
return m.Alloc + m.Sys, memory.TotalMemory()
}
func bToMb(b uint64) uint64 {
return b / 1024 / 1024
}
+313
View File
@@ -0,0 +1,313 @@
package capture
import (
"context"
"encoding/binary"
"io/ioutil"
"net"
"os"
"sync/atomic"
"testing"
"time"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
)
var LoopBack = func() net.Interface {
ifis, _ := net.Interfaces()
for _, v := range ifis {
if v.Flags&net.FlagLoopback != 0 {
return v
}
}
return ifis[0]
}()
func TestSetInterfaces(t *testing.T) {
l := &Listener{}
l.host = "127.0.0.1"
l.setInterfaces()
if len(l.Interfaces) != 1 {
t.Error("expected a single interface")
}
l.host = LoopBack.HardwareAddr.String()
l.setInterfaces()
if l.Interfaces[0].Name != LoopBack.Name && len(l.Interfaces) != 1 {
t.Error("interface should be loop back interface")
}
l.host = ""
l.setInterfaces()
if len(l.Interfaces) < 1 {
t.Error("should get all interfaces")
}
}
func TestBPFFilter(t *testing.T) {
l := &Listener{}
l.host = "127.0.0.1"
l.Transport = "tcp"
l.setInterfaces()
filter := l.Filter(l.Interfaces[0])
if filter != "(tcp dst portrange 0-65535 and host 127.0.0.1)" {
t.Error("wrong filter", filter)
}
l.port = 8000
l.trackResponse = true
filter = l.Filter(l.Interfaces[0])
if filter != "(tcp port 8000 and host 127.0.0.1)" {
t.Error("wrong filter")
}
}
func TestPcapDump(t *testing.T) {
f, err := ioutil.TempFile("", "pcap_file")
if err != nil {
t.Error(err)
}
h, _ := PcapDumpHandler(f, layers.LinkTypeLoop)
packets := Packets(1, 5, 5, 4)
for i := 0; i < len(packets); i++ {
if i == 1 {
// change dst port
binary.BigEndian.PutUint16(packets[i].TransLayer[2:], 8001)
}
if i == 4 {
packets[i].Info.CaptureLength = 40
}
h(packets[i])
}
name := f.Name()
f.Close()
testPcapDumpEngine(name, t)
}
func testPcapDumpEngine(f string, t *testing.T) {
defer os.Remove(f)
l, err := NewListener(f, 8000, "", EnginePcapFile, true)
err = l.Activate()
if err != nil {
t.Errorf("expected error to be nil, got %q", err)
return
}
pckts := 0
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err = l.Listen(ctx, func(packet *Packet) {
if packet.Info.CaptureLength != 57 {
t.Errorf("expected packet length to be %d, got %d", 57, packet.Info.CaptureLength)
}
pckts++
})
if err != nil {
t.Errorf("expected error to be nil, got %q", err)
}
if pckts != 3 {
t.Errorf("expected %d packets, got %d packets", 3, pckts)
}
}
func TestPcapHandler(t *testing.T) {
l, err := NewListener(LoopBack.Name, 8000, "", EnginePcap, true)
if err != nil {
t.Errorf("expected error to be nil, got %v", err)
return
}
err = l.Activate()
if err != nil {
t.Errorf("expected error to be nil, got %v", err)
return
}
defer l.Handles[LoopBack.Name].(*pcap.Handle).Close()
if err != nil {
t.Errorf("expected error to be nil, got %v", err)
return
}
for i := 0; i < 5; i++ {
_, _ = net.Dial("tcp", "127.0.0.1:8000")
}
sts, _ := l.Handles[LoopBack.Name].(*pcap.Handle).Stats()
if sts.PacketsReceived < 5 {
t.Errorf("expected >=5 packets got %d", sts.PacketsReceived)
}
}
// func TestSocketHandler(t *testing.T) {
// l, err := NewListener(LoopBack.Name, 8000, "", EngineRawSocket, true)
// err = l.Activate()
// if err != nil {
// return
// }
// defer l.Handles[LoopBack.Name].(*SockRaw).Close()
// if err != nil {
// t.Errorf("expected error to be nil, got %v", err)
// return
// }
// for i := 0; i < 5; i++ {
// _, _ = net.Dial("tcp", "127.0.0.1:8000")
// }
// sts, _ := l.Handles[LoopBack.Name].(*SockRaw).Stats()
// if sts.Packets < 5 {
// t.Errorf("expected >=5 packets got %d", sts.Packets)
// }
// }
func BenchmarkPcapDump(b *testing.B) {
f, err := ioutil.TempFile("", "pcap_file")
if err != nil {
b.Error(err)
return
}
now := time.Now()
defer os.Remove(f.Name())
h, _ := PcapDumpHandler(f, layers.LinkTypeLoop)
packets := Packets(1, b.N, 5, 4)
for i := 0; i < len(packets); i++ {
h(packets[i])
}
f.Close()
b.Logf("%d packets in %s", b.N, time.Since(now))
}
func BenchmarkPcapFile(b *testing.B) {
f, err := ioutil.TempFile("", "pcap_file")
if err != nil {
b.Error(err)
return
}
defer os.Remove(f.Name())
h, _ := PcapDumpHandler(f, layers.LinkTypeLoop)
packets := Packets(1, b.N, 5, 4)
for i := 0; i < len(packets); i++ {
h(packets[i])
}
name := f.Name()
f.Close()
b.ResetTimer()
var l *Listener
l, err = NewListener(name, 8000, "", EnginePcapFile, true)
if err != nil {
b.Error(err)
return
}
err = l.Activate()
if err != nil {
b.Error(err)
return
}
now := time.Now()
pckts := 0
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if err = l.Listen(ctx, func(packet *Packet) {
if packet.Info.CaptureLength != 49 {
b.Errorf("expected packet length to be %d, got %d", 49, packet.Info.CaptureLength)
}
pckts++
}); err != nil {
b.Error(err)
}
b.Logf("%d/%d packets in %s", pckts, b.N, time.Since(now))
}
// used to benchmark sock engine
var buf [1024]byte
func init() {
for i := 0; i < len(buf); i++ {
buf[i] = 0xff
}
}
func handler(n, counter *int32) PacketHandler {
return func(p *Packet) {
nn := int32(len(p.Data))
atomic.AddInt32(n, nn)
atomic.AddInt32(counter, 1)
}
}
func BenchmarkPcap(b *testing.B) {
var err error
n := new(int32)
counter := new(int32)
l, err := NewListener(LoopBack.Name, 8000, "", EnginePcap, false)
if err != nil {
b.Error(err)
return
}
l.PcapOptions.BPFFilter = "udp dst port 8000 and host 127.0.0.1"
err = l.Activate()
if err != nil {
b.Error(err)
return
}
errCh := l.ListenBackground(context.Background(), handler(n, counter))
select {
case <-l.Reading:
case err = <-errCh:
b.Error(err)
return
}
var conn net.Conn
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
conn, err = net.Dial("udp", "127.0.0.1:8000")
if err != nil {
b.Error(err)
return
}
b.StartTimer()
_, err = conn.Write(buf[:])
if err != nil {
b.Error(err)
return
}
}
b.ReportMetric(float64(atomic.LoadInt32(n)), "buf")
b.ReportMetric(float64(atomic.LoadInt32(counter)), "packets")
}
func BenchmarkRawSocket(b *testing.B) {
var err error
n := new(int32)
counter := new(int32)
l, err := NewListener(LoopBack.Name, 8000, "", EngineRawSocket, false)
if err != nil {
b.Error(err)
return
}
l.PcapOptions.BPFFilter = "udp dst port 8000 and host 127.0.0.1"
err = l.Activate()
if err != nil {
b.Error(err)
return
}
errCh := l.ListenBackground(context.Background(), handler(n, counter))
select {
case <-l.Reading:
case err = <-errCh:
b.Error(err)
return
}
var conn net.Conn
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
conn, err = net.Dial("udp", "127.0.0.1:8000")
if err != nil {
b.Error(err)
return
}
b.StartTimer()
_, err = conn.Write(buf[:])
if err != nil {
b.Error(err)
return
}
}
b.ReportMetric(float64(atomic.LoadInt32(n)), "buf")
b.ReportMetric(float64(atomic.LoadInt32(counter)), "packets")
}
+35
View File
@@ -0,0 +1,35 @@
/*
Package capture provides traffic sniffier using AF_PACKET, pcap or pcap file.
it allows you to listen for traffic from any port (e.g. sniffing) because they operate on IP level.
Ports is TCP/IP feature, same as flow control, reliable transmission and etc.
Currently this package implements TCP layer: flow control is managed under tcp package.
BPF filters can also be applied.
example:
// for the transport should be "tcp"
listener, err := capture.NewListener(host, port, transport, engine, trackResponse)
if err != nil {
// handle error
}
listener.SetPcapOptions(opts)
err = listner.Activate()
if err != nil {
// handle it
}
if err := listener.Listen(context.Background(), handler); err != nil {
// handle error
}
// or
errCh := listener.ListenBackground(context.Background(), handler) // runs in the backgorund
select {
case err := <- errCh:
// handle error
case <-quit:
//
case <- l.Reading: // if we have started reading
}
*/
package capture // import github.com/buger/goreplay/capture
+126
View File
@@ -0,0 +1,126 @@
// https://github.com/google/gopacket/blob/403ca653c4/pcapgo/read.go
package capture
import (
"encoding/binary"
"fmt"
"io"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
// Writer wraps an underlying io.Writer to write packet data in PCAP
// format. See http://wiki.wireshark.org/Development/LibpcapFileFormat
// for information on the file format.
//
// For those that care, we currently write v2.4 files with nanosecond
// or microsecond timestamp resolution and little-endian encoding.
type Writer struct {
w io.Writer
tsScaler int
// Moving this into the struct seems to save an allocation for each call to writePacketHeader
buf [16]byte
}
const magicNanoseconds = 0xA1B23C4D
const magicMicroseconds = 0xA1B2C3D4
const versionMajor = 2
const versionMinor = 4
// NewWriterNanos returns a new writer object, for writing packet data out
// to the given writer. If this is a new empty writer (as opposed to
// an append), you must call WriteFileHeader before WritePacket. Packet
// timestamps are written with nanosecond precision.
//
// // Write a new file:
// f, _ := os.Create("/tmp/file.pcap")
// w := pcapgo.NewWriterNanos(f)
// w.WriteFileHeader(65536, layers.LinkTypeEthernet) // new file, must do this.
// w.WritePacket(gopacket.CaptureInfo{...}, data1)
// f.Close()
// // Append to existing file (must have same snaplen and linktype)
// f2, _ := os.OpenFile("/tmp/fileNano.pcap", os.O_APPEND, 0700)
// w2 := pcapgo.NewWriter(f2)
// // no need for file header, it's already written.
// w2.WritePacket(gopacket.CaptureInfo{...}, data2)
// f2.Close()
func NewWriterNanos(w io.Writer) *Writer {
return &Writer{w: w, tsScaler: nanosPerNano}
}
// NewWriter returns a new writer object, for writing packet data out
// to the given writer. If this is a new empty writer (as opposed to
// an append), you must call WriteFileHeader before WritePacket.
// Packet timestamps are written witn microsecond precision.
//
// // Write a new file:
// f, _ := os.Create("/tmp/file.pcap")
// w := pcapgo.NewWriter(f)
// w.WriteFileHeader(65536, layers.LinkTypeEthernet) // new file, must do this.
// w.WritePacket(gopacket.CaptureInfo{...}, data1)
// f.Close()
// // Append to existing file (must have same snaplen and linktype)
// f2, _ := os.OpenFile("/tmp/file.pcap", os.O_APPEND, 0700)
// w2 := pcapgo.NewWriter(f2)
// // no need for file header, it's already written.
// w2.WritePacket(gopacket.CaptureInfo{...}, data2)
// f2.Close()
func NewWriter(w io.Writer) *Writer {
return &Writer{w: w, tsScaler: nanosPerMicro}
}
// WriteFileHeader writes a file header out to the writer.
// This must be called exactly once per output.
func (w *Writer) WriteFileHeader(snaplen uint32, linktype layers.LinkType) error {
var buf [24]byte
if w.tsScaler == nanosPerMicro {
binary.LittleEndian.PutUint32(buf[0:4], magicMicroseconds)
} else {
binary.LittleEndian.PutUint32(buf[0:4], magicNanoseconds)
}
binary.LittleEndian.PutUint16(buf[4:6], versionMajor)
binary.LittleEndian.PutUint16(buf[6:8], versionMinor)
// bytes 8:12 stay 0 (timezone = UTC)
// bytes 12:16 stay 0 (sigfigs is always set to zero, according to
// http://wiki.wireshark.org/Development/LibpcapFileFormat
binary.LittleEndian.PutUint32(buf[16:20], snaplen)
binary.LittleEndian.PutUint32(buf[20:24], uint32(linktype))
_, err := w.w.Write(buf[:])
return err
}
const nanosPerMicro = 1000
const nanosPerNano = 1
func (w *Writer) writePacketHeader(ci gopacket.CaptureInfo) error {
t := ci.Timestamp
if t.IsZero() {
t = time.Now()
}
secs := t.Unix()
usecs := t.Nanosecond() / w.tsScaler
binary.LittleEndian.PutUint32(w.buf[0:4], uint32(secs))
binary.LittleEndian.PutUint32(w.buf[4:8], uint32(usecs))
binary.LittleEndian.PutUint32(w.buf[8:12], uint32(ci.CaptureLength))
binary.LittleEndian.PutUint32(w.buf[12:16], uint32(ci.Length))
_, err := w.w.Write(w.buf[:])
return err
}
// WritePacket writes the given packet data out to the file.
func (w *Writer) WritePacket(ci gopacket.CaptureInfo, data []byte) error {
if ci.CaptureLength != len(data) {
return fmt.Errorf("capture length %d does not match data length %d", ci.CaptureLength, len(data))
}
if ci.CaptureLength > ci.Length {
return fmt.Errorf("invalid capture info %+v: capture length > length", ci)
}
if err := w.writePacketHeader(ci); err != nil {
return fmt.Errorf("error writing packet header: %v", err)
}
_, err := w.w.Write(data)
return err
}
+197
View File
@@ -0,0 +1,197 @@
package capture
import (
"encoding/binary"
"errors"
"testing"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
func generateHeader4(seq uint32, length uint16) []byte {
hdr := make([]byte, 4+24+24, 4+24+24)
binary.BigEndian.PutUint32(hdr, uint32(layers.ProtocolFamilyIPv4))
ip := hdr[4:]
ip[0] = 4<<4 | 6
binary.BigEndian.PutUint16(ip[2:4], length+24+24)
ip[9] = uint8(layers.IPProtocolTCP)
copy(ip[12:16], []byte{127, 0, 0, 1})
copy(ip[16:], []byte{127, 0, 0, 1})
// set tcp header
tcp := ip[24:]
tcp[12] = 6 << 4
binary.BigEndian.PutUint16(tcp, 5535)
binary.BigEndian.PutUint16(tcp[2:], 8000)
binary.BigEndian.PutUint32(tcp[4:], seq)
return hdr
}
func generateHeader6(seq uint32, length uint16) []byte {
hdr := make([]byte, 4+40+32+24, 4+40+32+24)
binary.BigEndian.PutUint32(hdr, uint32(layers.ProtocolFamilyIPv6Linux))
ip := hdr[4:]
ip[0] = 6 << 4
binary.BigEndian.PutUint16(ip[4:], length+32+24)
var ipAddr [16]byte
ipAddr[15] = 0x01
copy(ip[8:], ip[:])
copy(ip[24:], ip[:])
copy(ip[40:], []byte{
// net-layer (IPv6-Opts)
0x2b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
uint8(layers.IPProtocolTCP), 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
})
// set tcp header
tcp := ip[40+32:]
tcp[12] = 6 << 4
binary.BigEndian.PutUint16(tcp, 5535)
binary.BigEndian.PutUint16(tcp[2:], 8000)
binary.BigEndian.PutUint32(tcp[4:], seq)
return hdr
}
func Packets(start uint32, _len int, length uint16, version byte) []*Packet {
var packets = make([]*Packet, _len)
for i := start; i < start+uint32(_len); i++ {
var h []byte
if version == 4 {
h = generateHeader4(i, length)
} else {
h = generateHeader6(i, length)
}
d := append(h, make([]byte, int(length))...)
ci := &gopacket.CaptureInfo{Length: len(d), CaptureLength: len(d), Timestamp: time.Now()}
packets[i-start] = NewPacket(d, int(layers.LinkTypeLoop), 4, ci)
}
return packets
}
func TestIPv4Packet(t *testing.T) {
pckt := packet(append(generateHeader4(1024, 10), make([]byte, 10)...))
if pckt.Err != nil {
t.Error(pckt)
return
}
if err := packet(pckt.Data[:2]).Err; !errors.Is(err, ErrHdrLength("Link")) {
t.Errorf("should fail with %q, got %q", ErrHdrLength("Link"), err)
return
}
if err := packet(pckt.Data[:20]).Err; !errors.Is(err, ErrHdrLength("IPv4")) {
t.Errorf("should fail with %q, got %q", ErrHdrLength("IPv4"), err)
return
}
if err := packet(pckt.Data[:27]).Err; !errors.Is(err, ErrHdrLength("IPv4 opts")) {
t.Errorf("should fail with %q, got %q", ErrHdrLength("IPv4 opts"), err)
return
}
if err := packet(pckt.Data[:40]).Err; !errors.Is(err, ErrHdrLength("TCP")) {
t.Errorf("should fail with %q, got %q", ErrHdrLength("TCP opts"), err)
return
}
if err := packet(pckt.Data[:50]).Err; !errors.Is(err, ErrHdrLength("TCP opts")) {
t.Errorf("should fail with %q, got %q", ErrHdrLength("TCP opts"), err)
return
}
pckt.TransLayer[12] = 0x10
if err := packet(pckt.Data[:50]).Err; !errors.Is(err, ErrHdrInvalid("TCP's data offset")) {
t.Errorf("should fail with %q, got %q", ErrHdrInvalid("TCP's data offset"), err)
return
}
pckt.TransLayer[12] = 0x60
if err := packet(pckt.Data[:28]).Err; !errors.Is(err, ErrHdrMissing("TCP")) {
t.Errorf("should fail with %q, got %q", ErrHdrMissing("TCP"), err)
return
}
pckt.NetLayer[9] = 0x02
if err := packet(pckt.Data).Err; !errors.Is(err, ErrHdrExpected("TCP")) {
t.Errorf("should fail with %q, got %q", ErrHdrExpected("TCP"), err)
return
}
pckt.NetLayer[9] = 0x06
pckt.NetLayer[0] = 0x44
if err := packet(pckt.Data).Err; !errors.Is(err, ErrHdrInvalid("IPv4's IHL")) {
t.Errorf("should fail with %q, got %q", ErrHdrInvalid("IPv4's IHL"), err)
return
}
pckt.NetLayer[0] = 0x56
if err := packet(pckt.Data).Err; !errors.Is(err, ErrHdrExpected("IPv4 or IPv6")) {
t.Errorf("should fail with %q, got %q", ErrHdrExpected("IPv4 or IPv6"), err)
return
}
}
func TestIPv6Packet(t *testing.T) {
pckt := packet(append(generateHeader6(1024, 10), make([]byte, 10)...))
if pckt.Err != nil {
t.Error(pckt)
return
}
if err := packet(pckt.Data[:4]).Err; !errors.Is(err, ErrHdrMissing("IPv4 or IPv6")) {
t.Errorf("should fail with %q, got %q", ErrHdrMissing("IPv4 or IPv6"), err)
return
}
if err := packet(pckt.Data[:40]).Err; !errors.Is(err, ErrHdrLength("IPv6")) {
t.Errorf("should fail with %q, got %q", ErrHdrLength("IPv6"), err)
return
}
if err := packet(pckt.Data[:52]).Err; !errors.Is(err, ErrHdrLength("IPv6 opts")) {
t.Errorf("should fail with %q, got %q", ErrHdrLength("IPv6 opts"), err)
return
}
if err := packet(pckt.Data[:80]).Err; !errors.Is(err, ErrHdrLength("TCP")) {
t.Errorf("should fail with %q, got %q", ErrHdrLength("TCP opts"), err)
return
}
if err := packet(pckt.Data[:98]).Err; !errors.Is(err, ErrHdrLength("TCP opts")) {
t.Errorf("should fail with %q, got %q", ErrHdrLength("TCP opts"), err)
return
}
pckt.TransLayer[12] = 0x10
if err := packet(pckt.Data).Err; !errors.Is(err, ErrHdrInvalid("TCP's data offset")) {
t.Errorf("should fail with %q, got %q", ErrHdrInvalid("TCP's data offset"), err)
return
}
pckt.TransLayer[12] = 0x60
if err := packet(pckt.Data[:76]).Err; !errors.Is(err, ErrHdrMissing("TCP")) {
t.Errorf("should fail with %q, got %q", ErrHdrMissing("TCP"), err)
return
}
pckt.NetLayer[56] = 0x02
if err := packet(pckt.Data).Err; !errors.Is(err, ErrHdrExpected("TCP")) {
t.Errorf("should fail with %q, got %q", ErrHdrExpected("TCP"), err)
return
}
pckt.NetLayer[56] = 0x06
}
func packet(data []byte) *Packet {
return NewPacket(data, int(layers.LinkTypeLoop), 4, &gopacket.CaptureInfo{})
}
func BenchmarkNewPacketIPv4(b *testing.B) {
data := append(generateHeader4(1204, 10), make([]byte, 10)...)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := packet(data).Err; err != nil {
b.Error(err)
}
}
}
func BenchmarkNewPacketIPv6(b *testing.B) {
data := append(generateHeader6(1024, 10), make([]byte, 10)...)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := packet(data).Err; err != nil {
b.Error(err)
}
}
}
+260
View File
@@ -0,0 +1,260 @@
package capture
import (
"fmt"
"net"
"sync"
"time"
"unsafe"
"golang.org/x/sys/unix"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
)
const (
// ETHALL htons(ETH_P_ALL)
ETHALL uint16 = unix.ETH_P_ALL<<8 | unix.ETH_P_ALL>>8
// BLOCKSIZE ring buffer block_size
BLOCKSIZE = 64 << 10
// BLOCKNR ring buffer block_nr
BLOCKNR = (2 << 20) / BLOCKSIZE // 2mb / 64kb
// FRAMESIZE ring buffer frame_size
FRAMESIZE = BLOCKSIZE
// FRAMENR ring buffer frame_nr
FRAMENR = BLOCKNR * BLOCKSIZE / FRAMESIZE
// MAPHUGE2MB 2mb huge map
MAPHUGE2MB = 21 << unix.MAP_HUGE_SHIFT
)
var tpacket2hdrlen = tpAlign(int(unsafe.Sizeof(unix.Tpacket2Hdr{})))
// SockRaw is a linux M'maped af_packet socket
type SockRaw struct {
mu sync.Mutex
fd int
ifindex int
snaplen int
pollTimeout uintptr
frame uint32 // current frame
buf []byte // points to the memory space of the ring buffer shared with the kernel.
loopIndex int32 // this field must filled to avoid reading packet twice on a loopback device
}
// NewSocket returns new M'maped sock_raw on packet version 2.
func NewSocket(ifi net.Interface) (*SockRaw, error) {
// sock create
fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_RAW, int(ETHALL))
if err != nil {
return nil, err
}
sock := &SockRaw{
fd: fd,
ifindex: ifi.Index,
snaplen: FRAMESIZE,
pollTimeout: ^uintptr(0),
}
// set packet version
err = unix.SetsockoptInt(fd, unix.SOL_PACKET, unix.PACKET_VERSION, unix.TPACKET_V2)
if err != nil {
unix.Close(fd)
return nil, fmt.Errorf("setsockopt packet_version: %v", err)
}
// bind to interface
addr := unix.RawSockaddrLinklayer{
Family: unix.AF_PACKET,
Protocol: ETHALL,
Ifindex: int32(ifi.Index),
}
_, _, e := unix.Syscall(
unix.SYS_BIND,
uintptr(fd),
uintptr(unsafe.Pointer(&addr)),
uintptr(unix.SizeofSockaddrLinklayer),
)
if e != 0 {
unix.Close(fd)
return nil, e
}
// create shared-memory ring buffer
tp := &unix.TpacketReq{
Block_size: BLOCKSIZE,
Block_nr: BLOCKNR,
Frame_size: FRAMESIZE,
Frame_nr: FRAMENR,
}
err = unix.SetsockoptTpacketReq(sock.fd, unix.SOL_PACKET, unix.PACKET_RX_RING, tp)
if err != nil {
unix.Close(fd)
return nil, fmt.Errorf("setsockopt packet_rx_ring: %v", err)
}
sock.buf, err = unix.Mmap(
sock.fd,
0,
BLOCKSIZE*BLOCKNR,
unix.PROT_READ|unix.PROT_WRITE,
unix.MAP_SHARED|MAPHUGE2MB,
)
if err != nil {
unix.Close(fd)
return nil, fmt.Errorf("socket mmap error: %v", err)
}
return sock, nil
}
// ReadPacketData implements gopacket.PacketDataSource.
func (sock *SockRaw) ZeroCopyReadPacketData() (buf []byte, ci gopacket.CaptureInfo, err error) {
sock.mu.Lock()
defer sock.mu.Unlock()
var tpHdr *unix.Tpacket2Hdr
poll := &unix.PollFd{
Fd: int32(sock.fd),
Events: unix.POLLIN,
}
var i int
read:
i = int(sock.frame * FRAMESIZE)
tpHdr = (*unix.Tpacket2Hdr)(unsafe.Pointer(&sock.buf[i]))
sock.frame = (sock.frame + 1) % FRAMENR
if tpHdr.Status&unix.TP_STATUS_USER == 0 {
_, _, e := unix.Syscall(unix.SYS_POLL, uintptr(unsafe.Pointer(poll)), 1, sock.pollTimeout)
if e != 0 && e != unix.EINTR {
return buf, ci, e
}
// it might be some other frame with data!
if tpHdr.Status&unix.TP_STATUS_USER == 0 {
goto read
}
}
tpHdr.Status = unix.TP_STATUS_KERNEL
sockAddr := (*unix.RawSockaddrLinklayer)(unsafe.Pointer(&sock.buf[i+tpacket2hdrlen]))
// parse out repeating packets on loopback
if sockAddr.Ifindex == sock.loopIndex && sock.frame%2 != 0 {
goto read
}
ci.Length = int(tpHdr.Len)
ci.Timestamp = time.Unix(int64(tpHdr.Sec), int64(tpHdr.Nsec))
ci.InterfaceIndex = int(sockAddr.Ifindex)
buf = make([]byte, tpHdr.Snaplen)
ci.CaptureLength = copy(buf, sock.buf[i+int(tpHdr.Mac):])
return
}
// Close closes the underlying socket
func (sock *SockRaw) Close() (err error) {
sock.mu.Lock()
defer sock.mu.Unlock()
if sock.fd != -1 {
unix.Munmap(sock.buf)
sock.buf = nil
err = unix.Close(sock.fd)
sock.fd = -1
}
return
}
// SetSnapLen sets the maximum capture length to the given value.
// for this to take effects on the kernel level SetBPFilter should be called too.
func (sock *SockRaw) SetSnapLen(snap int) error {
sock.mu.Lock()
defer sock.mu.Unlock()
if snap < 0 {
return fmt.Errorf("expected %d snap length to be at least 0", snap)
}
if snap > FRAMESIZE {
snap = FRAMESIZE
}
sock.snaplen = snap
return nil
}
// SetTimeout sets poll wait timeout for the socket.
// negative value will block forever
func (sock *SockRaw) SetTimeout(t time.Duration) error {
sock.mu.Lock()
defer sock.mu.Unlock()
sock.pollTimeout = uintptr(t)
return nil
}
// GetSnapLen returns the maximum capture length
func (sock *SockRaw) GetSnapLen() int {
sock.mu.Lock()
defer sock.mu.Unlock()
return sock.snaplen
}
// SetBPFFilter compiles and sets a BPF filter for the socket handle.
func (sock *SockRaw) SetBPFFilter(expr string) error {
sock.mu.Lock()
defer sock.mu.Unlock()
if expr == "" {
return unix.SetsockoptInt(sock.fd, unix.SOL_SOCKET, unix.SO_DETACH_FILTER, 0)
}
filter, err := pcap.CompileBPFFilter(layers.LinkTypeEthernet, sock.snaplen, expr)
if err != nil {
return err
}
if len(filter) > int(^uint16(0)) {
return fmt.Errorf("filters out of range 0-%d", ^uint16(0))
}
if len(filter) == 0 {
return unix.SetsockoptInt(sock.fd, unix.SOL_SOCKET, unix.SO_DETACH_FILTER, 0)
}
fprog := &unix.SockFprog{
Len: uint16(len(filter)),
Filter: &(*(*[]unix.SockFilter)(unsafe.Pointer(&filter)))[0],
}
return unix.SetsockoptSockFprog(sock.fd, unix.SOL_SOCKET, unix.SO_ATTACH_FILTER, fprog)
}
// SetPromiscuous sets promiscous mode to the required value. for better result capture on all interfaces instead.
// If it is enabled, traffic not destined for the interface will also be captured.
func (sock *SockRaw) SetPromiscuous(b bool) error {
sock.mu.Lock()
defer sock.mu.Unlock()
mreq := unix.PacketMreq{
Ifindex: int32(sock.ifindex),
Type: unix.PACKET_MR_PROMISC,
}
opt := unix.PACKET_ADD_MEMBERSHIP
if !b {
opt = unix.PACKET_DROP_MEMBERSHIP
}
return unix.SetsockoptPacketMreq(sock.fd, unix.SOL_PACKET, opt, &mreq)
}
// Stats returns number of packets and dropped packets. This will be the number of packets/dropped packets since the last call to stats (not the cummulative sum!).
func (sock *SockRaw) Stats() (*unix.TpacketStats, error) {
sock.mu.Lock()
defer sock.mu.Unlock()
return unix.GetsockoptTpacketStats(sock.fd, unix.SOL_PACKET, unix.PACKET_STATISTICS)
}
// SetLoopbackIndex necessary to avoid reading packet twice on a loopback device
func (sock *SockRaw) SetLoopbackIndex(i int32) {
sock.mu.Lock()
defer sock.mu.Unlock()
sock.loopIndex = i
}
// WritePacketData transmits a raw packet.
func (sock *SockRaw) WritePacketData(pkt []byte) error {
_, err := unix.Write(sock.fd, pkt)
return err
}
func tpAlign(x int) int {
return int((uint(x) + unix.TPACKET_ALIGNMENT - 1) &^ (unix.TPACKET_ALIGNMENT - 1))
}
+13
View File
@@ -0,0 +1,13 @@
// +build !linux
package capture
import (
"errors"
"net"
)
// NewSocket returns new M'maped sock_raw on packet version 2.
func NewSocket(_ net.Interface) (Socket, error) {
return nil, errors.New("afpacket socket is only available on linux")
}
+20
View File
@@ -0,0 +1,20 @@
package capture
import (
"time"
"github.com/google/gopacket"
)
// Socket is any interface that defines the behaviors of Socket
type Socket interface {
ZeroCopyReadPacketData() ([]byte, gopacket.CaptureInfo, error)
WritePacketData([]byte) error
SetBPFFilter(string) error
SetPromiscuous(bool) error
SetSnapLen(int) error
GetSnapLen() int
SetTimeout(time.Duration) error
SetLoopbackIndex(i int32)
Close() error
}
+7
View File
@@ -0,0 +1,7 @@
dependencies:
pre:
- sudo apt-get install libpcap-dev -y
test:
override:
- sudo bash -l -c "export GOPATH='/home/ubuntu/.go_workspace:/usr/local/go_workspace:/home/ubuntu/.go_project' && GORACE='halt_on_error=1' /usr/local/go/bin/go test ./... -v -timeout 120s -race"
+1
View File
@@ -0,0 +1 @@
docs.goreplay.org
+49
View File
@@ -0,0 +1,49 @@
Think about Gor more like a network analyzer or tcpdump on steroids, it is not a proxy and does not affect your app anyhow. You specify application port, and it will capture and replay incoming data.
Simplest setup will be:
```bash
# Run on servers where you want to catch traffic. You can run it on every `web` machine.
sudo gor --input-raw :80 --output-http http://staging.com
```
It will record and replay traffic from the same machine. However, it is possible to use [[Aggregator-forwarder setup]], when Gor on your web machines forward traffic to Gor aggregator instance running on the separate server.
> You may notice that it require `sudo`: to analyze network Gor need permissions which available only to root users. However, it is possible to configure Gor [beign run for non-root users](Running as a non-root user).
### Forwarding to multiple addresses
You can forward traffic to multiple endpoints.
```
gor --input-tcp :28020 --output-http "http://staging.com" --output-http "http://dev.com"
```
### Splitting traffic
By default, it will send same traffic to all outputs, but you have options to equally split it (round-robin) using `--split-output` option.
```
gor --input-raw :80 --output-http "http://staging.com" --output-http "http://dev.com" --split-output true
```
### Tracking responses
By default `input-raw` does not intercept responses, only requests. You can turn response tracking using `--input-raw-track-response` option. When enable you will be able to access response information in middleware and `output-file`.
### Traffic interception engine
By default, Gor will use `libpcap` for intercepting traffic, it should work in most cases. If you have any troubles with it, you may try alternative engine: `raw_socket`.
```
sudo gor --input-raw :80 --input-raw-engine "raw_socket" --output-http "http://staging.com"
```
You can read more about [[Replaying HTTP traffic]].
### Tracking original IP addresses
You can use `--input-raw-realip-header` option to specify header name: If not blank, injects header with given name and real IP value to the request payload. Usually, this header should be named: `X-Real-IP`, but you can specify any name.
`gor --input-raw :80 --input-raw-realip-header "X-Real-IP" ...`
***
Also you may want to know about [[Rate limiting]], [[Request rewriting]] and [[Request filtering]]
+30
View File
@@ -0,0 +1,30 @@
We provide pre-compiled binaries for Mac and Linux, but you are free to compile Gor by yourself.
Gor is written using Go, so first you need to download it from here https://golang.org/, use the latest stable version.
The only Gor dependency is [libpcap](https://github.com/the-tcpdump-group/libpcap), which is the interface to various kernel packet capture mechanisms, and https://github.com/google/gopacket, which is a Go wrapper around libpcap. Latest libpcap version can be obtained at http://www.tcpdump.org/release/. Libpcap itself depend on `flex` and `bison` packages, many operating systems already have them installed.
```bash
# Fetch libpcap dependencies. Depending on your OS, instead of `apt` you will use `yum` or `rpm`, or `brew` on Mac.
sudo apt-get install flex bison -y
# Download latest stable release, compile and install it
wget http://www.tcpdump.org/release/libpcap-1.7.4.tar.gz && tar xzf libpcap-1.7.4.tar.gz
cd libpcap-1.7.4
./configure && make install
# Lets fetch Gor source code
mkdir $HOME/gocode
# See more information about GOPATH https://github.com/golang/go/wiki/GOPATH
export GOPATH=$HOME/gocode
# Fetch code from the Github
go get github.com/buger/gor
# Compile from source
cd $HOME/gocode/src/github.com/buger/gor
go build LDFLAGS = -ldflags "-extldflags \"-static\""
```
After you finished, you should see `gor` binary in current directory.
+39
View File
@@ -0,0 +1,39 @@
## STEP 1: Install Docker
For local development we recommend to use Docker.
If you don’t have it you can read how to install it here:
https://docs.docker.com/engine/getstarted/step_one/#step-3-verify-your-installation
## STEP 2: Download repository
`git clone git@github.com:buger/goreplay.git`
## STEP 3: Setup container
```
cd ./goreplay
make build
```
## Testing
To run tests execute next command:
```
make test
```
You can copy the command that is produced and modify it. For example, if you need to run one test copy the command and add `-run TestName`, e.g.:
```
docker run -v `pwd`:/go/src/github.com/buger/gor/ -p 0.0.0.0:8000:8000 -t -i gor:go go test ./. -run TestEmitterFiltered -timeout 60s -ldflags "-X main.VERSION=DEV-1482398347 -extldflags \"-static\"" -v
```
## Building
To get a binary file run
```
make release-bin
```
+26
View File
@@ -0,0 +1,26 @@
Sometimes it makes sense to use separate Gor instance for replaying traffic and performing things like load testing, so your production machines do not spend precious resources. It is possible to configure Gor on your web machines forward traffic to Gor aggregator instance running on the separate server.
```bash
# Run on servers where you want to catch traffic. You can run it on each `web` machine.
sudo gor --input-raw :80 --output-tcp replay.local:28020
# Replay server (replay.local).
gor --input-tcp replay.local:28020 --output-http http://staging.com
```
If you have multiple replay machines you can split traffic among them using `--split-output` option: it will equally split all incoming traffic to all outputs using round robin algorithm.
```
gor --input-raw :80 --split-output --output-tcp replay1.local:28020 --output-tcp replay2.local:28020
```
[GoReplay PRO](https://goreplay.org/pro.html) support accurate recording and replaying of tcp sessions, and when `--recognize-tcp-sessions` option is passed, instead of round-robin it will use a smarter algorithm which ensures that same sessions will be sent to the same replay instance.
In case if you are planning a large load testing, you may consider use separate master instance which will control Gor slaves which actually replay traffic. For example:
```
# This command will read multiple log files, replay them on 10x speed and loop them if needed for 30 seconds, and will distributed traffic (tcp session aware) among multiple workers
gor --input-file logs_from_multiple_machines.*|1000% --input-file-loop --exit-after 30s --recognize-tcp-sessions --split-output --output-tcp worker1.local --output-tcp worker2.local:27017 --output-tcp worker3.local:27017 ... --output-tcp workerN.local:27017
# worker
gor --input-tcp :27017 --ouput-http load_test.target
```
+38
View File
@@ -0,0 +1,38 @@
Gor can export requests and replayed response data to ElasticSearch:
```
./gor --input-raw :8000 --output-http http://staging.com --output-http-elasticsearch localhost:9200/gor
```
You don't have to create the index upfront. That will be done for you automatically.
### Format
Following structure represents ES format:
```
type ESRequestResponse struct {
ReqURL string `json:"Req_URL"`
ReqMethod string `json:"Req_Method"`
ReqUserAgent string `json:"Req_User-Agent"`
ReqAcceptLanguage string `json:"Req_Accept-Language,omitempty"`
ReqAccept string `json:"Req_Accept,omitempty"`
ReqAcceptEncoding string `json:"Req_Accept-Encoding,omitempty"`
ReqIfModifiedSince string `json:"Req_If-Modified-Since,omitempty"`
ReqConnection string `json:"Req_Connection,omitempty"`
ReqCookies string `json:"Req_Cookies,omitempty"`
RespStatus string `json:"Resp_Status"`
RespStatusCode string `json:"Resp_Status-Code"`
RespProto string `json:"Resp_Proto,omitempty"`
RespContentLength string `json:"Resp_Content-Length,omitempty"`
RespContentType string `json:"Resp_Content-Type,omitempty"`
RespTransferEncoding string `json:"Resp_Transfer-Encoding,omitempty"`
RespContentEncoding string `json:"Resp_Content-Encoding,omitempty"`
RespExpires string `json:"Resp_Expires,omitempty"`
RespCacheControl string `json:"Resp_Cache-Control,omitempty"`
RespVary string `json:"Resp_Vary,omitempty"`
RespSetCookie string `json:"Resp_Set-Cookie,omitempty"`
Rtt int64 `json:"RTT"`
Timestamp time.Time
}
```
+32
View File
@@ -0,0 +1,32 @@
### What OS are supported?
Gor will run everywhere where [libpcap](http://www.tcpdump.org/) works, and it works on most of the platforms. However, currently, we test it on Linux and Mac. See more about [[Compilation]].
### Why does the `--input-raw` requires sudo or root access?
Listener works by sniffing traffic from a given port. It's accessible
only by using sudo or root access. But it is possible to [[Running as non root user]].
### How do you deal with user session to replay the traffic correctly?
You can rewrite session related headers/params to match your staging environment. If you require custom logic (e.g random token based auth) follow this discussion: https://github.com/buger/gor/issues/154
### Can I use Gor to intercept SSL traffic?
Basic idea is that SSL was made to protect itself from traffic interception. There 2 options:
1. Move SSL handling to proxy like Nginx or Amazon ELB. And allow Gor to listen on upstreams.
2. Use `--input-http` so you can duplicate request payload directly from your app to Gor, but it will require your app modifications.
More can be find here: https://github.com/buger/gor/issues/85
### Is there a limit for size of HTTP request when using output-http?
Due to the fact that Gor can't guarantee interception of all packets, for large payloads > 200kb there is chance of missing some packets and corrupting body. Treat it as a feature and chance to test broken bodies handling :)
The only way to guarantee delivery is using `--input-http`, but you will miss some features.
### I'm getting 'too many open files' error
Typical Linux shell has a small open files soft limit at 1024. You can easily raise that when you do this before starting your gor replay process:
ulimit -n 64000
More about ulimit: http://www.thecodingmachine.com/solving-the-too-many-open-files-exception-in-red5-or-any-other-application/
### The CPU average across my load-balanced targets is higher than the source
If you are replaying traffic from multiple listeners to a load-balanced target and you use sticky sessions, you may observe that the target servers have a higher CPU load than the listener servers. This may be because the sticky session cookie of the original load balancer is not honored by the target load balancer thus resulting in requests that would normally hit the same target server hitting different servers on the backend thus reducing some caching benefits gained via the load balancing. Try running just one listener against one replay target and see if the CPU utilization comparison is more accurate.
Also see [[Troubleshooting]].
+74
View File
@@ -0,0 +1,74 @@
#### Overview
Middleware is a program that accepts request and response payload at STDIN and emits modified requests at STDOUT. You can implement any custom logic like stripping private data, advanced rewriting, support for oAuth and etc. Check examples [included into our repo](https://github.com/buger/gor/tree/master/examples/middleware).
```
Original request +--------------+
+-------------+----------STDIN---------->+ |
| Gor input | | Middleware |
+-------------+----------STDIN---------->+ |
Original response (1) +------+---+---+
| ^
+-------------+ Modified request v |
| Gor output +<---------STDOUT-----------------+ |
+-----+-------+ |
| |
| Replayed response |
+------------------STDIN----------------->----+
```
(1): Original responses will only be sent to the middleware if the `--input-raw-track-response` option is specified.
Middleware can be written in any language, see `examples/middleware` folder for examples.
Middleware program should accept the fact that all communication with Gor is asynchronous, there is no guarantee that original request and response messages will come one after each other. Your app should take care of the state if logic depends on original or replayed response, see `examples/middleware/token_modifier.go` as example.
Simple bash echo middleware (returns same request) will look like this:
```bash
while read line; do
echo $line
done
```
Middleware can be enabled using `--middleware` option, by specifying path to executable file:
```
gor --input-raw :80 --middleware "/opt/middleware_executable" --output-http "http://staging.server"
```
#### Communication protocol
All messages should be hex encoded, new line character specifieds the end of the message, eg. new message per line.
Decoded payload consist of 2 parts: header and HTTP payload, separated by new line character.
Example request payload:
```
1 932079936fa4306fc308d67588178d17d823647c 1439818823587396305
GET /a HTTP/1.1
Host: 127.0.0.1
```
Example response payload (note: you will only receive this if you specify `--input-raw-track-response`)
```
2 8e091765ae902fef8a2b7d9dd960e9d52222bd8c 1439818823587996305 2782013
HTTP/1.1 200 OK
Date: Mon, 17 Aug 2015 13:40:23 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8
```
Header contains request meta information separated by spaces. First value is payload type, possible values: `1` - request, `2` - original response, `3` - replayed response.
Next goes request id: unique among all requests (sha1 of time and Ack), but remain same for original and replayed response, so you can create associations between request and responses. The third argument is the time when request/response was initiated/received. Forth argument is populated only for responses and means latency.
HTTP payload is unmodified HTTP requests/responses intercepted from network. You can read more about request format [here](http://www.jmarshall.com/easy/http/), [here](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol) and [here](http://www.w3.org/Protocols/rfc2616/rfc2616.html). You can operate with payload as you want, add headers, change path, and etc. Basically you just editing a string, just ensure that it is RCF compliant.
At the end modified (or untouched) request should be emitted back to STDOUT, keeping original header, and hex-encoded. If you want to filter request, just not send it. Emitting responses back is required, even if you did not touch them.
#### Advanced example
Imagine that you have auth system that randomly generate access tokens, which used later for accessing secure content. Since there is no pre-defined token value, naive approach without middleware (or if middleware use only request payloads) will fail, because replayed server have own tokens, not synced with origin. To fix this, our middleware should take in account responses of replayed and origin server, store `originalToken -> replayedToken` aliases and rewrite all requests using this token to use replayed alias. See [examples/middleware/token_modifier.go](https://github.com/buger/gor/tree/master/examples/middleware/token_modifier.go) and [middleware_test.go#TestTokenMiddleware](https://github.com/buger/gor/tree/master/middleware_test.go) as example of described scheme.
***
You may also read about [[Request filtering]], [[Rate limiting]] and [[Request rewriting]].
+37
View File
@@ -0,0 +1,37 @@
Rate limiting can be useful if you only want to forward parts of incoming traffic, for example, to not overload your test environment. There are two strategies: dropping random requests or dropping fractions of requests based on Header or URL param value.
### Dropping random requests
Every input and output support random rate limiting.
There are two limiting algorithms: absolute or percentage based.
**Absolute**: If for current second it reached specified requests limit - disregard the rest, on next second counter reset.
**Percentage**: For input-file it will slowdown or speedup request execution, for the rest it will use the random generator to decide if request pass or not based on the chance you specified.
You can specify your desired limit using the "|" operator after the server address, see examples below.
#### Limiting replay using absolute number
```
# staging.server will not get more than ten requests per second
gor --input-tcp :28020 --output-http "http://staging.com|10"
```
#### Limiting listener using percentage based limiter
```
# replay server will not get more than 10% of requests
# useful for high-load environments
gor --input-raw :80 --output-tcp "replay.local:28020|10%"
```
### Consistent limiting based on Header or URL param value
If you have unique user id (like API key) stored in header or URL you can consistently forward specified percent of traffic only for the fraction of this users.
Basic formula looks like this: `FNV32-1A_hashing(value) % 100 >= chance`. Examples:
```
# Limit based on header value
gor --input-raw :80 --output-tcp "replay.local:28020|10%" --http-header-limiter "X-API-KEY: 10%"
# Limit based on header value
gor --input-raw :80 --output-tcp "replay.local:28020|10%" --http-param-limiter "api_key: 10%"
```
When limiting based on header or param only percentage based limiting supported.
+46
View File
@@ -0,0 +1,46 @@
Gor can replay HTTP traffic using `--output-http` option:
```bash
sudo ./gor --input-raw :8000 --output-http="http://staging.env"
```
You can [filter](Request filtering), [rate limit](Rate limiting) and [rewrite](Request rewriting) requests on the fly.
### HTTP output workers
By default Gor creates a dynamic pool of workers: it starts with 10 and creates more HTTP output workers when the HTTP output queue length is greater than 10. The number of workers created (N) is equal to the queue length at the time which it is checked and found to have a length greater than 10. The queue length is checked every time a message is written to the HTTP output queue. No more workers will be spawned until that request to spawn N workers is satisfied. If a dynamic worker cannot process a message at that time, it will sleep for 100 milliseconds. If a dynamic worker cannot process a message for 2 seconds it dies.
You may specify fixed number of workers using `--output-http-workers=20` option.
### Following redirects
By default Gor will ignore all redirects since they are handled by clients using your app, but in scenarios where your replayed environment introduces new redirects, you can enable them like this:
```
gor --input-tcp replay.local:28020 --output-http http://staging.com --output-http-redirects 2
```
The given example will follow up to 2 redirects per request.
### HTTP timeouts
By default http timeout for both request and response is 5 seconds. You can override it like this:
```
gor --input-tcp replay.local:28020 --output-http http://staging.com --output-http-timeout 30s
```
### Response buffer
By default, to reduce memory consumption, internal HTTP client will fetch max 200kb of the response body (used if you use middleware), by you can increase limit using `--output-http-response-buffer` option (accepts number of bytes).
### Basic Auth
If your development or staging environment is protected by Basic Authentication then those credentials can be injected in during the replay:
```
gor --input-raw :80 --output-http "http://user:pass@staging.com"
```
Note: This will overwrite any Authorization headers in the original request.
### Multiple domains support
If you app accepts traffic from multiple domains, and you want to keep original headers, there is specific `--http-original-host` with tells Gor do not touch Host header at all.
***
You may also read about [[Saving and Replaying from file]]
+35
View File
@@ -0,0 +1,35 @@
Filtering is useful when you need to capture only specific part of traffic, like API requests. It is possible to filter by URL, HTTP header or HTTP method.
#### Allow url regexp
```
# only forward requests being sent to the /api endpoint
gor --input-raw :8080 --output-http staging.com --http-allow-url /api
```
#### Disallow url regexp
```
# only forward requests NOT being sent to the /api... endpoint
gor --input-raw :8080 --output-http staging.com --http-disallow-url /api
```
#### Filter based on regexp of header
```
# only forward requests with an api version of 1.0x
gor --input-raw :8080 --output-http staging.com --http-allow-header api-version:^1\.0\d
# only forward requests NOT containing User-Agent header value "Replayed by Gor"
gor --input-raw :8080 --output-http staging.com --http-disallow-header "User-Agent: Replayed by Gor"
```
#### Filter based on HTTP method
Requests not matching a specified whitelist can be filtered out. For example to strip non-nullipotent requests:
```
gor --input-raw :80 --output-http "http://staging.server" \
--http-allow-method GET \
--http-allow-method OPTIONS
```
-----
You may also read about [[Request rewriting]], [[Rate limiting]] and [[Middleware]]
+38
View File
@@ -0,0 +1,38 @@
Gor supports rewriting of URLs, URL params and headers, see below.
Rewriting may be useful if you test environment does not have the same data as your production, and you want to perform all actions in the context of `test` user: for example rewrite all API tokens to some test value. Other possible use cases are toggling features on/off using custom headers or rewriting URL's if they changed in the new environment.
For more complex logic you can use [Middleware](middleware.md).
#### Rewrite URL based on a mapping
`--http-rewrite-url` expects value in "<search>:<replace>" format: ":" is a dilimiter. In `<replace>` section you may use captured regexp group values. This works similar to `replace` method in Javascript or `gsub` in Ruby.
```
# Rewrites all `/v1/user/<user_id>/ping` requests to `/v2/user/<user_id>/ping`
gor --input-raw :8080 --output-http staging.com --http-rewrite-url /v1/user/([^\\/]+)/ping:/v2/user/$1/ping
```
#### Set URL param
Set request url param, if param already exists it will be overwritten.
```
gor --input-raw :8080 --output-http staging.com --http-set-param api_key=1
```
#### Set Header
Set request header, if header already exists it will be overwritten. May be useful if you need to identify requests generated by Gor or enable feature flagged functionality in an application:
```
gor --input-raw :80 --output-http "http://staging.server" \
--http-set-header "User-Agent: Replayed by Gor" \
--http-set-header "Enable-Feature-X: true"
```
#### Host header
Host header gets special treatment. By default Host get set to the value specified in --output-http. If you manually set --http-set-header "Host: anonther.com", Gor will not override Host value.
If you app accepts traffic from multiple domains, and you want to keep original headers, there is specific `--http-original-host` with tells Gor do not touch Host header at all.
***
You may also read about [[Request filtering]], [[Rate limiting]] and [[Middleware]]
+17
View File
@@ -0,0 +1,17 @@
You can enable Gor for non-root users in a secure method by using the following commands
```
# Following commands assume that you put `gor` binary to /usr/local/bin
add gor
addgroup <username> gor
chgrp gor /usr/local/bin/gor
chmod 0750 /usr/local/bin/gor
setcap "cap_net_raw,cap_net_admin+eip" /usr/local/bin/gor
```
As a brief explanation of the above.
* We create a group called gor.
* We then add the user you want to the new group so they will be able to use gor without sudo
* We then change the user/group of gor binary the new group.
* We then make sure the permissions are set on gor binary so that members of the group can execute it but other normal users cannot.
* We then use `setcap` to give the CAP_NET_RAW and CAP_NET_ADMIN privilege to the executable when it runs. This is so that Gor can open its raw socket which is not normally permitted unless you are root.
+105
View File
@@ -0,0 +1,105 @@
You can save requests to file, and replay them later. While replaying it will preserve the original time differences between requests. If you apply [percentage based limiting](Rate Limiting) timing between requests will be reduced or increased appropriately: this approach opens possibilities like load testing, see below.
```bash
# write to file
gor --input-raw :80 --output-file requests.log
# read from file
gor --input-file requests.gor --output-http "http://staging.com"
```
By default Gor writes files in chunks. This configurable using `--output-file-append` option: the flushed chunk is appended to existence file or not. The default is **false**. By default, `--output-file` flushes each chunk to a different path.
```bash
gor ... --output-file %Y%m%d.log
# append false
20140608_0.log
20140608_1.log
20140609_0.log
20140609_1.log
```
This makes parallel file processing easy. But if you want to disable this behavior, you can disable it by adding `--output-file-append` option:
```bash
gor ... --output-file %Y%m%d.log --output-file-append
# append true
20140608.log
20140609.log
```
If you run gor multiple times, and it finds existing files, it will continue from last known index.
### Chunk size
You can set chunk limits using `--output-file-size-limit` and `--output-file-queue-limit` options.
The length of the chunk queue and the size of each chunk, respectively. The default values are 256 and 32mb, respectively. The suffixes “k” (KB), “m” (MB), and “g” (GB) can be used for `output-file-size-limit`.
If you want to have only size constraint, you can set `--output-file-queue-limit` to 0, and vice versa.
```bash
gor --input-raw :80 --output-file %Y-%m-%d.gz --output-file-size-limit 256m --output-file-queue-limit 0
```
### Using date variables in file names
For example, you can tell to create new file each hour: `--output-file /mnt/logs/requests-%Y-%m-%d-%H.log`
It will create new file for each hour: requests-2016-06-01-12.log, requests-2016-06-01-13.log, ...
The time format used as part of the file name. The following characters are replaced with actual values when the file is created:
* `%Y`: year including the century (at least 4 digits)
* `%m`: month of the year (01..12)
* `%d`: Day of the month (01..31)
* `%H`: Hour of the day, 24-hour clock (00..23)
* `%M`: Minute of the hour (00..59)
* `%S`: Second of the minute (00..60)
The default format is `%Y%m%d%H`, which creates one file per hour.
### GZIP compression
To read or write GZIP compressed files ensure that file extension ends with ".gz": `--output-file log.gz`
### Replaying from multiple files
`--input-file` accepts file pattern, for example: `--input-file logs-2016-05-*`: it will replay all the files, sorting them in lexicographical order.
### Buffered file output
Gor has memory buffer when it writes to file, and continuously flush changes to the file. Flushing to file happens if the buffer is filled, forced flush every 1 second, or if Gor is closed. You can change it using `--output-file-flush-interval` option. It most cases it should not be touched.
### File format
HTTP requests stored as it is, plain text: headers and bodies. Requests separated by `\n🐵🙈🙉\n` line (using such sequence for uniqueness and fun). Before each request goes single line with meta information containing payload type (1 - request, 2 - response, 3 - replayed response), unique request ID (request and response have the same) and timestamp when request was made. An example of 2 requests:
```
1 d7123dasd913jfd21312dasdhas31 127345969\n
GET / HTTP/1.1\r\n
\r\n
\n
🐵🙈🙉
\n
POST /upload HTTP/1.1\r\n
Content-Length: 7\r\n
Host: www.w3.org\r\n
\r\n
a=1&b=2
```
Note that technically \r and \n symbols are invisible, and indicate new lines. I made them visible in example just to show how it looks on byte level.
Making it text friendly allows writing simple parsers and use console tools like `grep` to do an analysis. You can even edit them manually, but be sure that your file editor does not change line endings.
## Performance testing
Currently, this functionality supported only by `input-file` and only when using percentage based limiter. Unlike default limiter for `input-file` instead of dropping requests it will slowdown or speedup request emitting. Note that **limiter is applied to input**:
```
# Replay from file on 2x speed
gor --input-file "requests.gor|200%" --output-http "staging.com"
```
Use `--stats --output-http-stats` to see latency stats.
### Looping files for replaying indefinitely
You can loop the same set of files, so when the last one replays all the requests, it will not stop, and will start from first one again. Having the only small amount of requests you can do extensive performance testing.
Pass `--input-file-loop` to make it work.
***
You may also read about [[Capturing and replaying traffic]] and [[Rate limiting]]
+90
View File
@@ -0,0 +1,90 @@
Gor can report stats on the `output-tcp` and `output-http` request queues. Stats are reported to the console every 5 seconds in the form `latest,mean,max,count,count/second` by using the `--output-http-stats` and `--output-tcp-stats` options.
Examples:
```
2014/04/23 21:17:50 output_tcp:latest,mean,max,count,count/second
2014/04/23 21:17:50 output_tcp:0,0,0,0,0
2014/04/23 21:17:55 output_tcp:1,1,2,68,13
2014/04/23 21:18:00 output_tcp:1,1,2,92,18
2014/04/23 21:18:05 output_tcp:1,1,2,119,23
```
```
2014/04/23 21:19:46 output_http:latest,mean,max,count,count/second
2014/04/23 21:19:46 output_http:0,0,0,0,0
2014/04/23 21:19:51 output_http:0,0,0,0,0
2014/04/23 21:19:56 output_http:0,0,0,0,0
2014/04/23 21:20:01 output_http:1,0,1,50,10
2014/04/23 21:20:06 output_http:1,1,4,72,14
2014/04/23 21:20:11 output_http:1,0,1,179,35
2014/04/23 21:20:16 output_http:1,0,1,148,29
2014/04/23 21:20:21 output_http:1,1,2,91,18
2014/04/23 21:20:26 output_http:1,1,2,150,30
2014/04/23 21:18:15 output_http:100,99,100,70,14
2014/04/23 21:18:21 output_http:100,99,100,55,11
```
### How can I tell if I have bottlenecks?
Key areas that sometimes experience bottlenecks are the output-tcp and output-http functions which have internal queues for requests. Each queue has an upper limit of 100. Enable stats reporting to see if any queues are experiencing bottleneck behavior.
#### Output HTTP bottlenecks
When running a Gor replay the output-http feature may bottleneck if:
* the replay has inadequate bandwidth. If the replay is receiving or sending more messages than its network adapter can handle the output-http-stats may report that the output-http queue is filling up. See if there is a way to upgrade the replay's bandwidth.
* with `--output-http-workers` set to anything other than `-1` the `-output-http` target is unable to respond to messages in a timely manner. The http output workers which take messages off the output-http queue, process the request, and ensure that the request did not result in an error may not be able to keep up with the number of incoming requests. If the replay is not using dynamic worker scaling (`--output-http-workers=-1`) The optimal number of output-http-workers can be determined with the formula `output-workers = (Average number of requests per second)/(Average target response time per second)`.
#### Output TCP bottlenecks
When using the Gor listener the output-tcp feature may bottleneck if:
* the replay is unable to accept and process more requests than the listener is able generate. Prior to troubleshooting the output-tcp bottleneck, ensure that the replay target is not experiencing any bottlenecks.
* the replay target has inadequate bandwidth to handle all its incoming requests. If a replay target's incoming bandwidth is maxed out the output-tcp-stats may report that the output-tcp queue is filling up. See if there is a way to upgrade the replay's bandwidth.
#### Tuning
To achieve the top most performance you should tune the source server system limits:
net.ipv4.tcp_max_tw_buckets = 65536
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 0
net.ipv4.tcp_max_syn_backlog = 131072
net.ipv4.tcp_syn_retries = 3
net.ipv4.tcp_synack_retries = 3
net.ipv4.tcp_retries1 = 3
net.ipv4.tcp_retries2 = 8
net.ipv4.tcp_rmem = 16384 174760 349520
net.ipv4.tcp_wmem = 16384 131072 262144
net.ipv4.tcp_mem = 262144 524288 1048576
net.ipv4.tcp_max_orphans = 65536
net.ipv4.tcp_fin_timeout = 10
net.ipv4.tcp_low_latency = 1
net.ipv4.tcp_syncookies = 0
***
### Gor is crashing with following stacktrace
```
fatal error: unexpected signal during runtime execution
[signal 0xb code=0x1 addr=0x63 pc=0x7ffcdfdf8b2c]
runtime stack:
runtime.throw(0xad8380, 0x2a)
/usr/local/go/src/runtime/panic.go:547 +0x90
runtime.sigpanic()
/usr/local/go/src/runtime/sigpanic_unix.go:12 +0x5a
goroutine 103 [syscall, locked to thread]:
runtime.cgocall(0x7b35a0, 0xc82121f1e8, 0x0)
/usr/local/go/src/runtime/cgocall.go:123 +0x11b fp=0xc82121f188 sp=0xc82121f158
net._C2func_getaddrinfo(0x7ffcec0008c0, 0x0, 0xc821b221e0, 0xc8217b2b18, 0x0, 0x0, 0x0)
??:0 +0x55 fp=0xc82121f1e8 sp=0xc82121f188
net.cgoLookupIPCNAME(0x7fffb17208ab, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb17200)
```
There is a chance that you hit Go bug. The crash comes from the CGO version of DNS resolver.
By default Go based version used, but ins some cases [it switches to CGO based](https://golang.org/pkg/net/#hdr-Name_Resolution). It is possible to force Go based DNS resolver using GODEBUG environment variable:
`sudo GODEBUG="netdns=go" ./gor --input-raw :80 --output-http staging.env`
Also, see [[FAQ]]
+1
View File
@@ -0,0 +1 @@
[Website](https://goreplay.org) | [PRO version](https://goreplay.org/pro.html) | [[Getting started]] | [[FAQ]] | [Join newsletter](https://www.getdrip.com/forms/89690474/submissions/new)
+1
View File
@@ -0,0 +1 @@
theme: jekyll-theme-cayman
+19
View File
@@ -0,0 +1,19 @@
Collaboration is difficult with commercial closed source but I do want to keep as much of the OSS ethos as possible available to customers who want to fix it themselves.
## Legal
In order to unambiguously own and sell Gor commercial products, I must have the copyright associated with the entire codebase. Any code you create which is merged must be owned by me. That's not me trying to be a jerk, that's just the way it works.
## Application
If you wish access to the product repository so you can send a PR, just open a new Gor issue and include the following info:
1. the email address that bought the license, a max of one collaborator per license
1. the following statement "I assign all rights, including copyright, to any future Gor work by myself to Leonid Bugaev"
You should be granted access to the private repo soon after.
## Notes
1. You should **never** work on the master branch. Only I may merge changes.
1. I may revoke access for any reason at any time. Access is not guaranteed with purchase.
+58
View File
@@ -0,0 +1,58 @@
### What are GoReplay PRO and GoReplay Enterprise?
[GoReplay PRO](https://goreplay.org/pro.html) and GoReplay Enterprise are extensions to GoReplay which add more functionality and provide additional support options for customers.
### Is there a trial version?
There's no free trial but we do offer a 14 day period with full refund if it does not work for you.
### What is the license?
See [COMM-LICENSE](https://github.com/buger/gor/blob/master/COMM-LICENSE) in the root of the GoReplay repo.
### How does PRO licensing work?
Every organization running GoReplay Pro on its own servers must have a license. There's no limit to the amount of servers or environments used by that organization.
### How does Enterprise licensing work?
Every organization running Gor Enterprise on its own servers must have a license. There's **no limit** to the amount of servers or environments used by that organization.
### What happens if my subscription lapses?
You must have an active subscription to run GoReplay Pro or Enterprise. After a one week grace period, you'll lose access to binaries and priority support. You won't get any more updates or bug fixes.
### How do I buy GoReplay Enterprise?
Send email to [support&#64;gortool.com](mailto:support&#64;gortool.com) with your info. A PDF quote will be emailed to you with the price. Reply to that email with your purchase order or just "Sounds good" and we will send an invoice which can be paid with a credit card, ACH bank transfer or a paper check.
### Can I upgrade from GoReplay Pro?
Yes! Current subscribers can upgrade by [requesting a quote](mailto:support&#64;gortool.com). **Please note that you are an existing Pro subscriber.** We will add a one-time discount on your first invoice to reflect any remaining Pro subscription credit. If you purchased GoReplay Pro 6 months ago for $950, you'll get a $475 discount.
### Can I distribute GoReplay PRO or Enterprise to my customers?
This is a common requirement for "on-site installs" or "appliances" sold to large corporations.
The standard license is appropriate for SaaS usage as it does not allow distribution. GoReplay PRO and Enterprise have an Appliance license option which **does** allow you to distribute them. The Appliance license is $9,500/yr for Pro and $19,500/yr for Enterprise. It allows you to distribute the Pro or Enterprise binaries as part of your application and each of your customers to run GoReplay Pro or Enterprise. Email [support&#64;gortool.com](mailto:support&#64;gortool.com) to purchase.
### Can you transfer a license?
Licenses are **not** transferable to another company. We will transfer the license from a user-specific email to a group email address (e.g. john_smith@example.com -> tech@example.com) but only for **the same domain**. It is strongly recommended that you buy the license using a group email address so the license is not attached to any one employee's email address.
### What does the license require me to do?
Your purchase gets you a unique access URL for downloading the Pro and/or Enterprise binaries. The license agreement requires you to keep this access URL private. If we find your access URL is ever publicized:
1. We'll send you a warning email with details. You need to remove the content and send a new email address so we can generate a new access URL for you. The old access URL will stop working immediately so you'll need to update your apps.
2. If your access URL is publicized a second time, we reserve the right to permanently remove access.
### Can I get a refund?
Yes, up to two weeks after purchase. Let us know the reason and maybe we can help but either way it's not a problem. Email [support&#64;gortool.com](mailto:support&#64;gortool.com).
### How do I update my credit card info?
If you purchased GoReplay Enterprise, there's nothing to do. Each annual invoice is paid separately.
If you purchased GoReplay PRO, log into [Gumroad](https://gumroad.com) with your email address, click the Billing tab and enter your new card. I can't provide support for the Gumroad website and don't have the ability to edit customer info - if you can't log in or change your credit card, you can always let your current subscription expire and purchase a new subscription.
+11
View File
@@ -0,0 +1,11 @@
Gor offers only community support. Gor Pro and Enterprise offer priority support via email.
## Priority Support
Covers 1 incident per quarter, with a max response time of 2 working days. Scope is limited to Gor and Gor Pro and Enterprise features and APIs, not the application or infrastructure. For support, email **support** AT **gortool.com**. Please email using the same domain as the original license email or explain your connection to the licensed company.
More aggressive support contracts (phone, quicker response time) are available separately, email with your needs.
## Onboarding
Enterprise customers may request a one hour video chat session with @buger to discuss their application(s), requirements and how best to leverage the various Gor features. Contact support to set up your session.
+39
View File
@@ -0,0 +1,39 @@
.wy-breadcrumbs li {
display: inline-block;
}
.wy-breadcrumbs li.wy-breadcrumbs-aside {
float: right;
}
.wy-breadcrumbs li a {
display: inline-block;
padding: 5px;
}
.wy-breadcrumbs li a:first-child {
padding-left: 0;
}
.wy-breadcrumbs-extra {
margin-bottom: 0;
color: #b3b3b3;
font-size: 80%;
display: inline-block;
}
@media screen and (max-width: 480px) {
.wy-breadcrumbs-extra {
display: none;
}
.wy-breadcrumbs li.wy-breadcrumbs-aside {
display: none;
}
}
@media print {
.wy-breadcrumbs li.wy-breadcrumbs-aside {
display: none;
}
}
+355
View File
@@ -0,0 +1,355 @@
.codeblock-example {
border: 1px solid #e1e4e5;
border-bottom: none;
padding: 24px;
padding-top: 48px;
font-weight: 500;
background: #fff;
position: relative;
}
.codeblock-example:after {
content: "Example";
position: absolute;
top: 0;
left: 0;
background: #9B59B6;
color: #fff;
padding: 6px 12px;
}
.codeblock-example.prettyprint-example-only {
border: 1px solid #e1e4e5;
margin-bottom: 24px;
}
.codeblock,
pre.literal-block,
.rst-content .literal-block,
.rst-content pre.literal-block,
div[class^='highlight'] {
border: 1px solid #e1e4e5;
padding: 0;
overflow-x: auto;
background: #fff;
margin: 1px 0 24px;
}
.codeblock div[class^='highlight'],
pre.literal-block div[class^='highlight'],
.rst-content .literal-block div[class^='highlight'],
div[class^='highlight'] div[class^='highlight'] {
border: none;
background: none;
margin: 0;
}
div[class^='highlight'] td.code {
width: 100%;
}
.linenodiv pre {
border-right: solid 1px #e6e9ea;
margin: 0;
padding: 12px;
font-family: "Source Code Pro","Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;
font-size: 12px;
line-height: 1.5;
color: #d9d9d9;
}
div[class^='highlight'] pre {
white-space: pre;
margin: 0;
padding: 12px;
font-family: "Source Code Pro","Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;
font-size: 12px;
line-height: 1.5;
display: block;
overflow: auto;
color: #404040;
}
@media print {
.codeblock,
pre.literal-block,
.rst-content .literal-block,
.rst-content pre.literal-block,
div[class^='highlight'],
div[class^='highlight'] pre {
white-space: pre-wrap;
}
}
.hll {
background-color: #ffc;
margin: 0 -12px;
padding: 0 12px;
display: block;
}
.c {
color: #998;
font-style: italic;
}
.err {
color: #a61717;
background-color: #e3d2d2;
}
.k {
font-weight: 700;
}
.o {
font-weight: 700;
}
.cm {
color: #998;
font-style: italic;
}
.cp {
color: #999;
font-weight: 700;
}
.c1 {
color: #998;
font-style: italic;
}
.cs {
color: #999;
font-weight: 700;
font-style: italic;
}
.gd {
color: #000;
background-color: #fdd;
}
.gd .x {
color: #000;
background-color: #faa;
}
.ge {
font-style: italic;
}
.gr {
color: #a00
}
.gh {
color: #999;
}
.gi {
color: #000;
background-color: #dfd;
}
.gi .x {
color: #000;
background-color: #afa;
}
.go {
color: #888;
}
.gp {
color: #555;
}
.gs {
font-weight: 700;
}
.gu {
color: purple;
font-weight: 700;
}
.gt {
color: #a00;
}
.kc {
font-weight: 700;
}
.kd {
font-weight: 700;
}
.kn {
font-weight: 700;
}
.kp {
font-weight: 700;
}
.kr {
font-weight: 700;
}
.kt {
color: #458;
font-weight: 700;
}
.m {
color: #099;
}
.s {
color: #d14;
}
.n {
color: #333;
}
.na {
color: teal;
}
.nb {
color: #0086b3;
}
.nc {
color: #458;
font-weight: 700;
}
.no {
color: teal;
}
.ni {
color: purple;
}
.ne {
color: #900;
font-weight: 700;
}
.nf {
color: #900;
font-weight: 700;
}
.nn {
color: #555;
}
.nt {
color: navy;
}
.nv {
color: teal;
}
.ow {
font-weight: 700;
}
.w {
color: #bbb;
}
.mf {
color: #099;
}
.mh {
color: #099;
}
.mi {
color: #099;
}
.mo {
color: #099;
}
.sb {
color: #d14;
}
.sc {
color: #d14;
}
.sd {
color: #d14;
}
.s2 {
color: #d14
}
.se {
color: #d14;
}
.sh {
color: #d14;
}
.si {
color: #d14;
}
.sx {
color: #d14;
}
.sr {
color: #009926;
}
.s1 {
color: #d14;
}
.ss {
color: #990073;
}
.bp {
color: #999;
}
.vc {
color: teal;
}
.vg {
color: teal;
}
.vi {
color: teal;
}
.il {
color: #099;
}
.gc {
color: #999;
background-color: #EAF2F5;
}
+3442
View File
File diff suppressed because it is too large Load Diff
+171
View File
@@ -0,0 +1,171 @@
code {
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace !important;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
background-color: #f7f7f7 !important;
color: #333 !important;
font-size: 14px !important;
border: none !important;
padding: 5px !important;
}
pre > code {
line-height: 20px;
padding: 16px !important;
}
blockquote {
color: #777 !important;
border-left: 4px solid rgb(221, 221, 221);
padding-left: 16px;
padding-right: 16px;
margin-left: 0px !important;
}
/* Visited links purple is not a good idea */
a:visited {
color: #008BF3;
}
a {
color: #008BF3;
}
h3, h4, h5, h6 {
color: #333 !important;
}
.rst-versions {
display: none !important;
}
/* The next and previous button we don't need */
.rst-footer-buttons {
display: none;
}
/* Foldable parts */
details > * {
margin-left: 20px;
}
details > summary {
margin-left: 0px; /* To overwrite the margin from above */
font-size: 120%;
cursor: pointer
}
/* Fix the design not having the correct spacing */
.wy-menu-vertical .subnav li.current > a {
padding: 0.4045em 2.427em;
}
.toctree-l3 {
padding-left: 1.0em;
}
footer .fastlane {
margin: 20px 0;
}
footer .fastlane iframe {
vertical-align: middle;
}
/* Custom Syntax highlighting to look more like GitHub.com */
.hljs-symbol {
color: #0086b3;
}
.hljs-keyword {
font-weight: normal;
color: #a71d5d;
}
.hljs-string {
color: #183691;
}
/* Header Anchors */
/* From https://github.com/facelessuser/pymdown-extensions/blob/bf18d0635e9d91b0f98eacdcaa10f26e0ace0f20/doc_theme/css/theme_custom.css#L322-L395 */
.rst-content .headeranchor-link {
position: absolute;
top: 0;
bottom: 0;
left: 0;
display: block;
padding-right: 6px;
padding-left: 30px;
margin-left: -30px;
}
.rst-content .headeranchor-link:focus {
outline: none;
}
.rst-content h1,
.rst-content h2,
.rst-content h3,
.rst-content h4,
.rst-content h5,
.rst-content h6 {
position: relative;
}
.rst-content h1 .headeranchor,
.rst-content h2 .headeranchor,
.rst-content h3 .headeranchor,
.rst-content h4 .headeranchor,
.rst-content h5 .headeranchor,
.rst-content h6 .headeranchor {
display: none;
color: #000;
vertical-align: middle;
}
.rst-content h1:hover .headeranchor-link,
.rst-content h2:hover .headeranchor-link,
.rst-content h3:hover .headeranchor-link,
.rst-content h4:hover .headeranchor-link,
.rst-content h5:hover .headeranchor-link,
.rst-content h6:hover .headeranchor-link {
height: 1em;
padding-left: 8px;
margin-left: -30px;
text-decoration: none;
}
.rst-content h1:hover .headeranchor-link .headeranchor,
.rst-content h2:hover .headeranchor-link .headeranchor,
.rst-content h3:hover .headeranchor-link .headeranchor,
.rst-content h4:hover .headeranchor-link .headeranchor,
.rst-content h5:hover .headeranchor-link .headeranchor,
.rst-content h6:hover .headeranchor-link .headeranchor {
display: inline-block;
}
.rst-content .headeranchor {
font: normal normal 16px FontAwesome;
line-height: 1;
display: inline-block;
text-decoration: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.rst-content .headeranchor:before {
content: '\f0c1';
}
/* index.md badges */
@media screen and (max-width: 768px) {
.badge img {
width: auto;
}
}
+435
View File
@@ -0,0 +1,435 @@
.wy-affix {
position: fixed;
top: 1.618em;
}
.wy-menu a:hover {
text-decoration: none;
}
.wy-menu-vertical header,
.wy-menu-vertical p.caption {
height: 32px;
display: inline-block;
line-height: 32px;
padding: 0 1.618em;
margin-bottom: 0;
margin-top: 14px;
display: block;
font-weight: 700;
text-transform: uppercase;
font-size: 85%;
color: #ccc;
white-space: nowrap;
}
.wy-menu-vertical span {
color: #666;
}
.wy-menu-vertical ul {
margin-bottom: 0;
}
.wy-menu-vertical li.divide-top {
border-top: solid 1px #404040;
}
.wy-menu-vertical li.divide-bottom {
border-bottom: solid 1px #404040;
}
.wy-menu-vertical li.current {
background-color: #e5e5e5;
}
.wy-menu-vertical li.current a {
color: rgba(0, 93, 255, 0.7);
border-right: none;
}
.wy-menu-vertical li.current a:hover {
color: rgba(0, 93, 255, 0.9);
}
.wy-menu-vertical li code,
.wy-menu-vertical li .rst-content tt,
.rst-content .wy-menu-vertical li tt {
border: none;
background: inherit;
color: inherit;
padding-left: 0;
padding-right: 0
}
.wy-menu-vertical li span.toctree-expand {
display: block;
float: left;
margin-left: -1.2em;
font-size: .8em;
line-height: 1.6em;
color: #999;
}
.wy-menu-vertical li.on a,
.wy-menu-vertical li.current>a {
color: rgba(0, 93, 255, 0.9);
font-weight: 700;
position: relative;
background: #fafafa;
border: none;
}
/*.wy-menu-vertical li.on a:hover span.toctree-expand,
.wy-menu-vertical li.current>a:hover span.toctree-expand {
color: gray;
}*/
.wy-menu-vertical li.on a span.toctree-expand,
.wy-menu-vertical li.current>a span.toctree-expand {
display: block;
font-size: .8em;
line-height: 1.6em;
color: #333;
}
.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul,
.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul {
display: none;
}
.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul,
.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul {
display: block;
}
.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a {
display: block;
padding: .4045em 4.045em;
}
.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand {
color: #999;
}
.wy-menu-vertical li.toctree-l2 span.toctree-expand {
color: #999;
}
.wy-menu-vertical li.toctree-l3 {
background-color: #eee;
font-size: .9em;
}
.wy-menu-vertical li.toctree-l3.current>a {
padding: .4045em 4.045em;
}
.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a {
display: block;
padding: .4045em 5.663em;
border-top: none;
border-bottom: none;
}
.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand {
color: rgba(0, 93, 255, 0.9);
}
.wy-menu-vertical li.toctree-l3 span.toctree-expand {
color: #999;
}
.wy-menu-vertical li.toctree-l4 {
font-size: .9em;
}
.wy-menu-vertical li.current ul {
display: block;
}
.wy-menu-vertical .local-toc li ul {
display: block;
}
.wy-menu-vertical li ul li a {
margin-bottom: 0;
color: rgba(0, 93, 255, 0.7);
font-weight: 400;
}
.wy-menu-vertical a {
display: inline-block;
line-height: 18px;
padding: .4045em 1.618em;
display: block;
position: relative;
font-size: 90%;
color: rgba(0, 93, 255, 0.7);
}
.wy-menu-vertical li.on a:hover,
.wy-menu-vertical li.current>a:hover {
background-color: #fafafa;
}
.wy-menu-vertical a:hover {
color: rgba(0, 93, 255, 0.9);
cursor: pointer;
background-color: #fafafa;
}
.wy-menu-vertical a:hover span.toctree-expand {
color: rgba(0, 93, 255, 0.5);
}
.wy-menu-vertical a:active span.toctree-expand {
color: rgba(0, 93, 255, 0.7);
}
/* Search */
.wy-side-nav-search {
z-index: 200;
background-color: #fafafa;
border-bottom: #333;
text-align: center;
padding: .809em;
display: block;
color: #333;
margin-bottom: .809em
}
.wy-side-nav-search input[type=text] {
width: 100%;
color: #333;
border-radius: 3px;
outline: 0;
padding: 10px;
background-color: #fff;
border: solid 1px #6d6d6d;
box-shadow: none
}
.wy-side-nav-search img {
display: block;
margin: auto auto .809em;
height: 45px;
width: 45px;
background-color: #2980B9;
padding: 5px;
border-radius: 100%
}
.wy-side-nav-search>a,
.wy-side-nav-search .wy-dropdown>a {
color: #333;
font-size: 100%;
font-weight: 700;
display: inline-block;
padding: 4px 6px;
margin-bottom: .809em
}
.wy-side-nav-search>a:hover,
.wy-side-nav-search .wy-dropdown>a:hover {
background: rgba(255,255,255,0.1)
}
.wy-side-nav-search>a img.logo,
.wy-side-nav-search .wy-dropdown>a img.logo {
display: block;
margin: 0 auto;
height: auto;
width: auto;
border-radius: 0;
max-width: 100%;
background: transparent
}
.wy-side-nav-search>a.icon img.logo,
.wy-side-nav-search .wy-dropdown>a.icon img.logo {
margin-top: .85em
}
.wy-nav .wy-menu-vertical header {
color: #2980B9
}
.wy-nav .wy-menu-vertical a {
color: #b3b3b3
}
.wy-nav .wy-menu-vertical a:hover {
background-color: #2980B9;
color: #fff
}
[data-menu-wrap] {
-webkit-transition: all .2s ease-in;
-moz-transition: all .2s ease-in;
transition: all .2s ease-in;
position: absolute;
opacity: 1;
width: 100%;
opacity: 0
}
[data-menu-wrap].move-center {
left: 0;
right: auto;
opacity: 1
}
[data-menu-wrap].move-left {
right: auto;
left: -100%;
opacity: 0
}
[data-menu-wrap].move-right {
right: -100%;
left: auto;
opacity: 0
}
.wy-body-for-nav {
background: left repeat-y #fcfcfc;
background-image: url(data:image/png;
base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC);
background-size: 300px 1px
}
.wy-grid-for-nav {
position: absolute;
width: 100%;
height: 100%
}
.wy-nav-side {
position: fixed;
top: 0;
bottom: 0;
left: 0;
padding-bottom: 2em;
width: 300px;
overflow-x: hidden;
overflow-y: scroll;
min-height: 100%;
background-color: #fafafa;
z-index: 200;
border-right: 2px solid #eee;
}
.wy-nav-top {
display: none;
background-color: #333;
color: #fff;
padding: .4045em .809em;
position: relative;
line-height: 50px;
text-align: center;
font-size: 100%;
*zoom: 1
}
.wy-nav-top:before,
.wy-nav-top:after {
display: table;
content: ""
}
.wy-nav-top:after {
clear: both
}
.wy-nav-top a {
color: #fff;
font-weight: 700
}
.wy-nav-top img {
margin-right: 12px;
height: 45px;
width: 45px;
background-color: #2980B9;
padding: 5px;
border-radius: 100%
}
.wy-nav-top i {
font-size: 30px;
line-height: 50px;
float: left;
cursor: pointer
}
.wy-nav-content-wrap {
margin-left: 300px;
background: #fcfcfc;
min-height: 100%
}
.wy-nav-content {
padding: 1.618em 3.236em;
height: 100%;
max-width: 1100px;
margin: auto
}
.wy-body-mask {
position: fixed;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.2);
display: none;
z-index: 499
}
.wy-body-mask.on {
display: block
}
@media screen and (max-width: 768px) {
.wy-body-for-nav {
background: #fcfcfc
}
.wy-nav-top {
display: block
}
.wy-nav-side {
left: -300px
}
.wy-nav-side.shift {
width: 85%;
left: 0
}
.wy-nav-content-wrap {
margin-left: 0
}
.wy-nav-content-wrap .wy-nav-content {
padding: 1.618em
}
.wy-nav-content-wrap.shift {
position: fixed;
min-width: 100%;
left: 85%;
top: 0;
height: 100%;
overflow: hidden
}
}
@media screen and (min-width: 1400px) {
.wy-nav-content {
margin: 0;
background: #fcfcfc
}
}
+17
View File
@@ -0,0 +1,17 @@
### Overview
Gor architecture tries to follow UNIX philosophy: everything made of pipes, various inputs multiplexing data to outputs.
You can [rate limit](/rate-limiting.md), [filter](request-filtering.md), [rewrite](request-rewriting.md) requests or even use your own [middleware](middleware.md) to implement custom logic. Also, it is possible to replay requests at the higher rate for [load testing](saving-and-replaying-from-file.md).
### Available inputs
* `--input-raw` - used to capture HTTP traffic, you should specify IP address or interface and application port. More about [[Capturing and replaying traffic]].
* `--input-file` - accepts file which previously was recorded using ` --output-file`. More about [[Saving and Replaying from file]]
* `--input-tcp` - used by Gor aggregation instance if you decided forward traffic from multiple forwarder Gor instances to it. Read about using [[Aggregator-forwarder setup]].
### Available outputs
* `--output-http` - replay HTTP traffic to given endpoint, accepts base url. Read [more about it](Replaying HTTP traffic)
* `--output-file` - records incoming traffic to the file. More about [[Saving and Replaying from file]]
* `--output-tcp` - forward incoming data to another Gor instance, used in conjunction with `--input-tcp`. Read more about [[Aggregator-forwarder setup]].
* `--output-stdout` - used for debugging, outputs all data to stdout.
+44
View File
@@ -0,0 +1,44 @@
### Dependencies
To start working with Gor, you need to have a web server running on your machine, and a terminal to run commands. If you are just poking around, you can quickly start the server by calling `gor file-server :8000`, this will start a simple file server of the current directory on port `8000`.
### Installing Gor
Download the latest Gor binary from https://github.com/buger/gor/releases (we provide precompiled binaries for Windows, Linux x64 and Mac OS), or you can compile by yourself [[Compilation]].
Once the archive is downloaded and uncompressed, you can run Gor from the current directory, or you may want to copy binary to your PATH (for Linux and Mac OS it can be `/usr/local/bin`).
### Capturing web traffic
Now run this command in terminal: `sudo ./gor --input-raw :8000 --output-stdout`
This command says to listen for all network activity happening on port 8000 and log it to stdout.
If you are familiar with `tcpdump`, we are going to implement similar functionality.
> You may notice that it uses `sudo` and asks for the password: to analyze network, Gor needs permissions which are available only to super users.
> However, it is possible to configure Gor [being run for non-root users](Running-as-non-root-user).
Make a few requests by opening `http://localhost:8000` in your browser, or just by calling curl in terminal `curl http://localhost:8000`. You should see that `gor` outputs all the HTTP requests and responses right to the terminal window where it is running.
**Gor is not a proxy:** you do not need to put 3-rd party tool to your critical path. Instead Gor just silently analyzes the traffic of your application and does not affect it anyhow.
### Replaying
Now it's time to replay your original traffic to another environment. Let's start the same file web server but on a different port: `gor file-server :8001`.
Instead of `--output-stdout` we will use `--output-http` and provide URL of second server: `sudo ./gor --input-raw :8000 --output-http="http://localhost:8001"`
Make few requests to first server. You should see them replicated to the second one, voila!
### Saving requests to file and replaying them later
Sometimes it's not possible to replay requests in real time; Gor allows you to save requests to the file and replay them later.
First use `--output-file` to save them: `sudo ./gor --input-raw :8000 --output-file=requests.gor`. This will create new file and continuously write all captured requests to it.
Let's re-run Gor, but now to replay requests from file: `./gor --input-file requests.gor --output-http="http://localhost:8001"`. You should see all the recorded requests coming to the second server, and they will be replayed in the same order and with exactly same timing as they were recorded.
Next: [[The Basics]]
### Watch an overview:
![YOUTUBE](https://www.youtube.com/watch?v=CxuKZcMKaW4)
+29
View File
@@ -0,0 +1,29 @@
Gor is an open-source tool for capturing and replaying live HTTP traffic into a test environment in order to continuously test your system with real data. It can be used to increase confidence in code deployments, configuration changes and infrastructure changes.
Read for more info:
* [[Getting Started]]
* [[The Basics]]
* [[Capturing and replaying traffic]]
* [[Replaying HTTP traffic]]
* [[[PRO] Replaying Binary protocols]]
* [[[PRO] Recording and replaying keep alive TCP sessions]]
* [[Saving and Replaying from file]]
* [Performance testing](https://github.com/buger/gor/wiki/Saving-and-Replaying-from-file#performance-testing)
* [[Rate limiting]]
* [[Request filtering]]
* [[Request rewriting]]
* [[Middleware]]
* [[Distributed configuration]]
* [[Exporting to ElasticSearch]]
* [[FAQ]]
* [[Troubleshooting]]
## Commercial Aspects
* [[Commercial Support]]
* [[Commercial FAQ]]
* [[Commercial collaboration]]
Next: [Getting Started](Getting-Started)
+7
View File
@@ -0,0 +1,7 @@
$(document).ready(function () {
$('img[alt="YOUTUBE"]').each(function () {
var id = $(this).attr('src').split('/')[$(this).attr('src').split('/').length - 1].split("=")[1];
var video = '<iframe style="width: 100%;height: 450px;" src="https://www.youtube.com/embed/' + id + '" frameborder="0" allowfullscreen></iframe>';
$(this).replaceWith(video);
});
});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
> **This feature available only in PRO version. See https://goreplay.org/pro.html for details.**
By default, GoReplay does not guarantee that when you record keep-alive TCP session, it will be replayed in the same TCP connection as well. This is ok for most of the cases, but it does not give an accurate number of TCP sessions while replaying, also may cause issues if your application state depends on TCP session (do not mess with HTTP session).
[GoReplay PRO](https://goreplay.org/pro.html) extension adds support for accurate recording and replaying of keep-alive TCP sessions. Separate connection to your server is created per original session and it makes benchmarks and tests incredibly accurate. To enable session recognition you just need to pass `--recognize-tcp-sessions` option.
```
gor --input-raw :80 --recognize-tcp-sessions --output-http http://test.target
```
Note that enabling this option also change algorithm of distributing traffic when using `--split-output`, see [Distributed configuration].
+14
View File
@@ -0,0 +1,14 @@
> **This feature available only in PRO version. See https://goreplay.org/pro.html for details.**
Gor includes basic support for working with binary formats like `thrift` or `protocol-buffers`. To start set `--input-raw-protocol` to 'binary' (by default 'http'). For replaying, you should use `--output-binary`, example:
```
gor --input-raw :80 --input-raw-protocol binary --output-binary staging:8081
```
While working with `--input-raw` you may notice a 2-second delay before messages are emitted to the outputs. This behaviour is expected and happening because for general binary protocol it is impossible to know when TCP message ends, so Gor has to set inactivity timeout. Each protocol has own rules (for example write message length as first bytes), and require individual handling to know message length. We consider improving detailed protocol support for `thrift`, `protocol-buffer` and etc.
Note that you can use all load testing features for binary protocols. For example, the following command will loop and replay recorded payload on 10x speed for 30 seconds:
```
gor --input-file './binary*.gor|1000%' --output-binary staging:9091 --input-file-loop --exit-after 30s
```
+85 -71
View File
@@ -2,67 +2,81 @@ package main
import (
"encoding/json"
"github.com/buger/elastigo/api"
"github.com/buger/elastigo/core"
"log"
"net/http"
"regexp"
"net/url"
"strings"
"time"
"github.com/buger/goreplay/proto"
elastigo "github.com/mattbaird/elastigo/lib"
)
type ESUriErorr struct{}
func (e *ESUriErorr) Error() string {
return "Wrong ElasticSearch URL format. Expected to be: host:port/index_name"
return "Wrong ElasticSearch URL format. Expected to be: scheme://host/index_name"
}
type ESPlugin struct {
Active bool
ApiPort string
eConn *elastigo.Conn
Host string
Index string
indexor *core.BulkIndexer
indexor *elastigo.BulkIndexer
done chan bool
}
type ESRequestResponse struct {
ReqUrl 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 string `json:"Req_URL"`
ReqMethod string `json:"Req_Method"`
ReqUserAgent string `json:"Req_User-Agent"`
ReqAcceptLanguage string `json:"Req_Accept-Language,omitempty"`
ReqAccept string `json:"Req_Accept,omitempty"`
ReqAcceptEncoding string `json:"Req_Accept-Encoding,omitempty"`
ReqIfModifiedSince string `json:"Req_If-Modified-Since,omitempty"`
ReqConnection string `json:"Req_Connection,omitempty"`
ReqCookies string `json:"Req_Cookies,omitempty"`
RespStatus string `json:"Resp_Status"`
RespStatusCode string `json:"Resp_Status-Code"`
RespProto string `json:"Resp_Proto,omitempty"`
RespContentLength string `json:"Resp_Content-Length,omitempty"`
RespContentType string `json:"Resp_Content-Type,omitempty"`
RespTransferEncoding string `json:"Resp_Transfer-Encoding,omitempty"`
RespContentEncoding string `json:"Resp_Content-Encoding,omitempty"`
RespExpires string `json:"Resp_Expires,omitempty"`
RespCacheControl string `json:"Resp_Cache-Control,omitempty"`
RespVary string `json:"Resp_Vary,omitempty"`
RespSetCookie string `json:"Resp_Set-Cookie,omitempty"`
Rtt int64 `json:"RTT"`
Timestamp time.Time
}
// Parse ElasticSearch URI
//
// Proper format is: host:port/index_name
func parseURI(URI string) (err error, host string, port string, index string) {
rURI := regexp.MustCompile("(.+):([0-9]+)/(.+)")
match := rURI.FindAllStringSubmatch(URI, -1)
// Proper format is: scheme://[userinfo@]host/index_name
// userinfo is: user[:password]
// net/url.Parse() does not fail if scheme is not provided but actualy does not
// handle URI properly.
// So we must 'validate' URI format to match requirements to use net/url.Parse()
func parseURI(URI string) (err error, index string) {
if len(match) == 0 {
parsedUrl, parseErr := url.Parse(URI)
if parseErr != nil {
err = new(ESUriErorr)
return
}
// check URL validity by extracting host and index values.
host := parsedUrl.Host
urlPathParts := strings.Split(parsedUrl.Path, "/")
index = urlPathParts[len(urlPathParts)-1]
// force index specification in uri : ie no implicit index
if host == "" || index == "" {
err = new(ESUriErorr)
} else {
host = match[0][1]
port = match[0][2]
index = match[0][3]
}
return
@@ -71,36 +85,35 @@ func parseURI(URI string) (err error, host string, port string, index string) {
func (p *ESPlugin) Init(URI string) {
var err error
err, p.Host, p.ApiPort, p.Index = parseURI(URI)
err, p.Index = parseURI(URI)
if err != nil {
log.Fatal("Can't initialize ElasticSearch plugin.", err)
}
api.Domain = p.Host
api.Port = p.ApiPort
p.eConn = elastigo.NewConn()
p.indexor = core.NewBulkIndexerErrors(50, 60)
p.eConn.SetFromUrl(URI)
p.indexor = p.eConn.NewBulkIndexerErrors(50, 60)
p.done = make(chan bool)
p.indexor.Run(p.done)
p.indexor.Start()
// Only start the ErrorHandler goroutine when in verbose mode
// no need to burn ressources otherwise
// go p.ErrorHandler()
go p.ErrorHandler()
log.Println("Initialized Elasticsearch Plugin")
Debug(1, "Initialized Elasticsearch Plugin")
return
}
func (p *ESPlugin) IndexerShutdown() {
p.done <- true
p.indexor.Stop()
return
}
func (p *ESPlugin) ErrorHandler() {
for {
errBuf := <-p.indexor.ErrorChannel
log.Println(errBuf.Err)
Debug(1, "[ELASTICSEARCH]", errBuf.Err)
}
}
@@ -111,8 +124,9 @@ 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 {
// ResponseAnalyze send req and resp to ES
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,34 +134,34 @@ 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: string(proto.Path(req)),
ReqMethod: string(proto.Method(req)),
ReqUserAgent: string(proto.Header(req, []byte("User-Agent"))),
ReqAcceptLanguage: string(proto.Header(req, []byte("Accept-Language"))),
ReqAccept: string(proto.Header(req, []byte("Accept"))),
ReqAcceptEncoding: string(proto.Header(req, []byte("Accept-Encoding"))),
ReqIfModifiedSince: string(proto.Header(req, []byte("If-Modified-Since"))),
ReqConnection: string(proto.Header(req, []byte("Connection"))),
ReqCookies: string(proto.Header(req, []byte("Cookie"))),
RespStatus: string(proto.Status(resp)),
RespStatusCode: string(proto.Status(resp)),
RespProto: string(proto.Method(resp)),
RespContentLength: string(proto.Header(resp, []byte("Content-Length"))),
RespContentType: string(proto.Header(resp, []byte("Content-Type"))),
RespTransferEncoding: string(proto.Header(resp, []byte("Transfer-Encoding"))),
RespContentEncoding: string(proto.Header(resp, []byte("Content-Encoding"))),
RespExpires: string(proto.Header(resp, []byte("Expires"))),
RespCacheControl: string(proto.Header(resp, []byte("Cache-Control"))),
RespVary: string(proto.Header(resp, []byte("Vary"))),
RespSetCookie: string(proto.Header(resp, []byte("Set-Cookie"))),
Rtt: rtt,
Timestamp: t,
}
j, err := json.Marshal(&esResp)
if err != nil {
log.Println(err)
Debug(0, "[ELASTIC-RESPONSE]", err)
} else {
p.indexor.Index(p.Index, "RequestResponse", "", "", &t, j, true)
p.indexor.Index(p.Index, "RequestResponse", "", "", "", &t, j)
}
return
}
+140
View File
@@ -0,0 +1,140 @@
package main
import (
"testing"
)
const expectedIndex = "gor"
func assertExpectedGorIndex(index string, t *testing.T) {
if expectedIndex != index {
t.Fatalf("Expected index %s but got %s", expectedIndex, index)
}
}
func assertExpectedIndex(expectedIndex string, index string, t *testing.T) {
if expectedIndex != index {
t.Fatalf("Expected index %s but got %s", expectedIndex, index)
}
}
func assertExpectedError(returnedError error, t *testing.T) {
expectedError := new(ESUriErorr)
if expectedError != returnedError {
t.Errorf("Expected err %s but got %s", expectedError, returnedError)
}
}
func assertNoError(returnedError error, t *testing.T) {
if nil != returnedError {
t.Errorf("Expected no err but got %s", returnedError)
}
}
// Argument host:port/index_name
// i.e : localhost:9200/gor
// Fail because scheme is mandatory
func TestElasticConnectionBuildFailWithoutScheme(t *testing.T) {
uri := "localhost:9200/" + expectedIndex
err, _ := parseURI(uri)
assertExpectedError(err, t)
}
// Argument scheme://Host:port
// i.e : http://localhost:9200
// Fail : explicit index is required
func TestElasticConnectionBuildFailWithoutIndex(t *testing.T) {
uri := "http://localhost:9200"
err, index := parseURI(uri)
assertExpectedIndex("", index, t)
assertExpectedError(err, t)
}
// Argument scheme://Host/index_name
// i.e : http://localhost/gor
func TestElasticConnectionBuildFailWithoutPort(t *testing.T) {
uri := "http://localhost/" + expectedIndex
err, index := parseURI(uri)
assertNoError(err, t)
assertExpectedGorIndex(index, t)
}
// Argument scheme://Host:port/index_name
// i.e : http://localhost:9200/gor
func TestElasticLocalConnectionBuild(t *testing.T) {
uri := "http://localhost:9200/" + expectedIndex
err, index := parseURI(uri)
assertNoError(err, t)
assertExpectedGorIndex(index, t)
}
// Argument scheme://Host:port/index_name
// i.e : http://localhost.local:9200/gor or https://localhost.local:9200/gor
func TestElasticSimpleLocalWithSchemeConnectionBuild(t *testing.T) {
uri := "http://localhost.local:9200/" + expectedIndex
err, index := parseURI(uri)
assertNoError(err, t)
assertExpectedGorIndex(index, t)
}
// Argument scheme://Host:port/index_name
// i.e : http://localhost.local:9200/gor or https://localhost.local:9200/gor
func TestElasticSimpleLocalWithHTTPSConnectionBuild(t *testing.T) {
uri := "https://localhost.local:9200/" + expectedIndex
err, index := parseURI(uri)
assertNoError(err, t)
assertExpectedGorIndex(index, t)
}
// Argument scheme://Host:port/index_name
// i.e : localhost.local:9200/pathtoElastic/gor
func TestElasticLongPathConnectionBuild(t *testing.T) {
uri := "http://localhost.local:9200/pathtoElastic/" + expectedIndex
err, index := parseURI(uri)
assertNoError(err, t)
assertExpectedGorIndex(index, t)
}
// Argument scheme://Host:userinfo@port/index_name
// i.e : http://user:password@localhost.local:9200/gor
func TestElasticBasicAuthConnectionBuild(t *testing.T) {
uri := "http://user:password@localhost.local:9200/" + expectedIndex
err, index := parseURI(uri)
assertNoError(err, t)
assertExpectedGorIndex(index, t)
}
// Argument scheme://Host:port/path/index_name
// i.e : http://localhost.local:9200/path/gor or https://localhost.local:9200/path/gor
func TestElasticComplexPathConnectionBuild(t *testing.T) {
uri := "http://localhost.local:9200/path/" + expectedIndex
err, index := parseURI(uri)
assertNoError(err, t)
assertExpectedGorIndex(index, t)
}
+182 -31
View File
@@ -1,53 +1,204 @@
package main
import (
"fmt"
"hash/fnv"
"io"
"log"
"runtime"
"sync"
"time"
"github.com/buger/goreplay/byteutils"
"github.com/pbnjay/memory"
)
func Start(stop chan int) {
for _, in := range Plugins.Inputs {
go CopyMulty(in, Plugins.Outputs...)
}
// Emitter represents an abject to manage plugins communication
type Emitter struct {
sync.WaitGroup
plugins *InOutPlugins
}
select {
case <-stop:
return
// NewEmitter creates and initializes new Emitter object.
func NewEmitter() *Emitter {
return &Emitter{}
}
// Start initialize loop for sending data from inputs to outputs
func (e *Emitter) Start(plugins *InOutPlugins, middlewareCmd string) {
if Settings.CopyBufferSize < 1 {
Settings.CopyBufferSize = 5 << 20
}
e.plugins = plugins
if middlewareCmd != "" {
middleware := NewMiddleware(middlewareCmd)
for _, in := range plugins.Inputs {
middleware.ReadFrom(in)
}
e.plugins.Inputs = append(e.plugins.Inputs, middleware)
e.plugins.All = append(e.plugins.All, middleware)
e.Add(1)
go func() {
defer e.Done()
if err := CopyMulty(middleware, plugins.Outputs...); err != nil {
Debug(2, fmt.Sprintf("[EMITTER] error during copy: %q", err))
}
}()
} else {
for _, in := range plugins.Inputs {
e.Add(1)
go func(in PluginReader) {
defer e.Done()
if err := CopyMulty(in, plugins.Outputs...); err != nil {
Debug(2, fmt.Sprintf("[EMITTER] error during copy: %q", err))
}
}(in)
}
}
}
// Copy from 1 reader to multiple writers
func CopyMulty(src io.Reader, writers ...io.Writer) (err error) {
buf := make([]byte, 32*1024)
// Close closes all the goroutine and waits for it to finish.
func (e *Emitter) Close() {
fmt.Println("Closing!")
for _, p := range e.plugins.All {
if cp, ok := p.(io.Closer); ok {
cp.Close()
}
}
if len(e.plugins.All) > 0 {
// wait for everything to stop
e.Wait()
}
fmt.Println("Closed!")
e.plugins.All = nil // avoid Close to make changes again
}
// CopyMulty copies from 1 reader to multiple writers
func CopyMulty(src PluginReader, writers ...PluginWriter) error {
wIndex := 0
modifier := NewHTTPModifier(&Settings.ModifierConfig)
filteredRequests := make(map[string]int64)
filteredRequestsLastCleanTime := time.Now().UnixNano()
filteredCount := 0
for {
nr, er := src.Read(buf)
if nr > 0 && len(buf) > nr{
Debug("Sending", src, ": ", string(buf[0:nr]))
msg, err := src.PluginRead()
if err != nil {
if err == ErrorStopped || err == io.EOF {
return nil
}
return err
}
if Settings.splitOutput {
// Simple round robin
writers[wIndex].Write(buf[0:nr])
if msg != nil && len(msg.Data) > 0 {
if len(msg.Data) > int(Settings.CopyBufferSize) {
msg.Data = msg.Data[:Settings.CopyBufferSize]
}
meta := payloadMeta(msg.Meta)
if len(meta) < 3 {
Debug(2, fmt.Sprintf("[EMITTER] Found malformed record %q from %q", msg.Meta, src))
continue
}
requestID := byteutils.SliceToString(meta[1])
// start a subroutine only when necessary
if Settings.Verbose >= 3 {
Debug(3, "[EMITTER] input: ", byteutils.SliceToString(msg.Meta[:len(msg.Meta)-1]), " from: ", src)
}
if modifier != nil {
Debug(3, "[EMITTER] modifier:", requestID, "from:", src)
if isRequestPayload(msg.Meta) {
msg.Data = modifier.Rewrite(msg.Data)
// If modifier tells to skip request
if len(msg.Data) == 0 {
filteredRequests[requestID] = time.Now().UnixNano()
filteredCount++
continue
}
Debug(3, "[EMITTER] Rewritten input:", requestID, "from:", src)
wIndex++
if wIndex >= len(writers) {
wIndex = 0
}
} else {
for _, dst := range writers {
dst.Write(buf[0:nr])
} else {
if _, ok := filteredRequests[requestID]; ok {
delete(filteredRequests, requestID)
filteredCount--
continue
}
}
}
if Settings.PrettifyHTTP {
msg.Data = prettifyHTTP(msg.Data)
if len(msg.Data) == 0 {
continue
}
}
if Settings.SplitOutput {
if Settings.RecognizeTCPSessions {
if !PRO {
log.Fatal("Detailed TCP sessions work only with PRO license")
}
hasher := fnv.New32a()
hasher.Write(meta[1])
wIndex = int(hasher.Sum32()) % len(writers)
if _, err := writers[wIndex].PluginWrite(msg); err != nil {
return err
}
} else {
// Simple round robin
if _, err := writers[wIndex].PluginWrite(msg); err != nil {
return err
}
wIndex = (wIndex + 1) % len(writers)
}
} else {
for _, dst := range writers {
if _, err := dst.PluginWrite(msg); err != nil {
return err
}
}
}
}
if er == io.EOF {
break
}
if er != nil {
err = er
break
// Run GC on each 1000 request
if filteredCount > 0 && filteredCount%1000 == 0 {
// Clean up filtered requests for which we didn't get a response to filter
now := time.Now().UnixNano()
if now-filteredRequestsLastCleanTime > int64(60*time.Second) {
for k, v := range filteredRequests {
if now-v > int64(60*time.Second) {
delete(filteredRequests, k)
filteredCount--
}
}
filteredRequestsLastCleanTime = time.Now().UnixNano()
}
}
}
return err
}
func memUsage() (uint64, uint64) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
// fmt.Printf("Current memory usage: ")
// // For info on each, see: https://golang.org/pkg/runtime/#MemStats
// fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc))
// fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc))
// fmt.Printf("\tSys = %v MiB", bToMb(m.Sys))
// fmt.Printf("\tNumGC = %v\n", m.NumGC)
// fmt.Printf("Total available memory %v MiB\n", bToMb(memory.TotalMemory()))
// fmt.Printf("Currently using %.2f %% of memory\n", 100*float64(m.Alloc+m.Sys)/float64(memory.TotalMemory()))
return m.Alloc + m.Sys, memory.TotalMemory()
}
func bToMb(b uint64) uint64 {
return b / 1024 / 1024
}
+188 -24
View File
@@ -1,25 +1,36 @@
package main
import (
"io"
"fmt"
"os"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestMain(m *testing.M) {
PRO = true
code := m.Run()
os.Exit(code)
}
func TestEmitter(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
output := NewTestOutput(func(data []byte) {
output := NewTestOutput(func(*Message) {
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
go Start(quit)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
for i := 0; i < 1000; i++ {
wg.Add(1)
@@ -27,34 +38,85 @@ func TestEmitter(t *testing.T) {
}
wg.Wait()
close(quit)
emitter.Close()
}
func TestEmitterRoundRobin(t *testing.T) {
func TestEmitterFiltered(t *testing.T) {
wg := new(sync.WaitGroup)
input := NewTestInput()
input.skipHeader = true
output := NewTestOutput(func(*Message) {
wg.Done()
})
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
methods := HTTPMethods{[]byte("GET")}
Settings.ModifierConfig = HTTPModifierConfig{Methods: methods}
emitter := &Emitter{}
go emitter.Start(plugins, "")
wg.Add(2)
id := uuid()
reqh := payloadHeader(RequestPayload, id, time.Now().UnixNano(), -1)
reqb := append(reqh, []byte("POST / HTTP/1.1\r\nHost: www.w3.org\r\nUser-Agent: Go 1.1 package http\r\nAccept-Encoding: gzip\r\n\r\n")...)
resh := payloadHeader(ResponsePayload, id, time.Now().UnixNano()+1, 1)
respb := append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...)
input.EmitBytes(reqb)
input.EmitBytes(respb)
id = uuid()
reqh = payloadHeader(RequestPayload, id, time.Now().UnixNano(), -1)
reqb = append(reqh, []byte("GET / HTTP/1.1\r\nHost: www.w3.org\r\nUser-Agent: Go 1.1 package http\r\nAccept-Encoding: gzip\r\n\r\n")...)
resh = payloadHeader(ResponsePayload, id, time.Now().UnixNano()+1, 1)
respb = append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...)
input.EmitBytes(reqb)
input.EmitBytes(respb)
wg.Wait()
emitter.Close()
Settings.ModifierConfig = HTTPModifierConfig{}
}
func TestEmitterSplitRoundRobin(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
var counter1, counter2 int32
output1 := NewTestOutput(func(data []byte) {
output1 := NewTestOutput(func(*Message) {
atomic.AddInt32(&counter1, 1)
wg.Done()
})
output2 := NewTestOutput(func(data []byte) {
output2 := NewTestOutput(func(*Message) {
atomic.AddInt32(&counter2, 1)
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output1, output2}
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output1, output2},
}
Settings.splitOutput = true
Settings.SplitOutput = true
go Start(quit)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
for i := 0; i < 1000; i++ {
wg.Add(1)
@@ -63,29 +125,131 @@ func TestEmitterRoundRobin(t *testing.T) {
wg.Wait()
close(quit)
emitter.Close()
if counter1 == 0 || counter2 == 0 || counter1 != counter2 {
t.Errorf("Round robin should split traffic equally: %d vs %d", counter1, counter2)
}
Settings.SplitOutput = false
}
func TestEmitterRoundRobin(t *testing.T) {
wg := new(sync.WaitGroup)
input := NewTestInput()
var counter1, counter2 int32
output1 := NewTestOutput(func(*Message) {
counter1++
wg.Done()
})
output2 := NewTestOutput(func(*Message) {
counter2++
wg.Done()
})
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output1, output2},
}
plugins.All = append(plugins.All, input, output1, output2)
Settings.SplitOutput = true
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
for i := 0; i < 1000; i++ {
wg.Add(1)
input.EmitGET()
}
wg.Wait()
emitter.Close()
if counter1 == 0 || counter2 == 0 {
t.Errorf("Round robin should split traffic equally: %d vs %d", counter1, counter2)
}
Settings.splitOutput = false
Settings.SplitOutput = false
}
func TestEmitterSplitSession(t *testing.T) {
wg := new(sync.WaitGroup)
wg.Add(200)
input := NewTestInput()
input.skipHeader = true
var counter1, counter2 int32
output1 := NewTestOutput(func(msg *Message) {
if payloadID(msg.Meta)[0] == 'a' {
counter1++
}
wg.Done()
})
output2 := NewTestOutput(func(msg *Message) {
if payloadID(msg.Meta)[0] == 'b' {
counter2++
}
wg.Done()
})
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output1, output2},
}
Settings.SplitOutput = true
Settings.RecognizeTCPSessions = true
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
for i := 0; i < 200; i++ {
// Keep session but randomize
id := make([]byte, 20)
if i&1 == 0 { // for recognizeTCPSessions one should be odd and other will be even number
id[0] = 'a'
} else {
id[0] = 'b'
}
input.EmitBytes([]byte(fmt.Sprintf("1 %s 1 1\nGET / HTTP/1.1\r\n\r\n", id[:20])))
}
wg.Wait()
if counter1 != counter2 {
t.Errorf("Round robin should split traffic equally: %d vs %d", counter1, counter2)
}
Settings.SplitOutput = false
Settings.RecognizeTCPSessions = false
emitter.Close()
}
func BenchmarkEmitter(b *testing.B) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
output := NewTestOutput(func(data []byte) {
output := NewTestOutput(func(*Message) {
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
go Start(quit)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
b.ResetTimer()
@@ -95,5 +259,5 @@ func BenchmarkEmitter(b *testing.B) {
}
wg.Wait()
close(quit)
emitter.Close()
}
+53
View File
@@ -0,0 +1,53 @@
(ns echo.core
(:gen-class)
(:require [clojure.string :as cs]
[clojure.java.io :as io])
(:import org.apache.commons.codec.binary.Hex
java.io.BufferedReader
java.io.IOException
java.io.InputStreamReader))
(defn transform-http-msg
"Function that transforms/filters the incoming HTTP messages."
[headers body]
;; do actual transformations here
[headers body])
(defn decode-hex-string
"Decode an Hex-encoded string."
[s]
(String. (Hex/decodeHex (.toCharArray s))))
(defn encode-hex-string
"Encode a string to a hex-encoded string."
[^String s]
(String. (Hex/encodeHex (.getBytes s))))
(defn -main
[& args]
(let [br (BufferedReader. (InputStreamReader. System/in))]
(try
(loop [hex-line (.readLine br)]
(let [decoded-req (decode-hex-string hex-line)
;; empty line separates headers from body
http-request (partition-by empty? (cs/split-lines decoded-req))
headers (first http-request)
;; HTTP messages can contain no body:
body (when (= 3 (count http-request)) (last http-request))
[new-headers new-body] (transform-http-msg headers body)]
(println (encode-hex-string (str (cs/join "\n" headers)
(when body
(str "\n\n"
(cs/join "\n" body)))))))
(when-let [line (.readLine br)]
(recur line)))
(catch IOException e nil))))
+48
View File
@@ -0,0 +1,48 @@
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
class Echo {
public static String decodeHexString(String s) throws DecoderException {
return new String(Hex.decodeHex(s.toCharArray()));
}
public static String encodeHexString(String s) {
return new String(Hex.encodeHex(s.getBytes()));
}
public static String transformHTTPMessage(String req) {
// do actual transformations here
return req;
}
public static void main(String[] args) throws DecoderException {
if(args != null){
for(String arg : args){
System.out.println(arg);
}
}
BufferedReader stdin = new BufferedReader(new InputStreamReader(
System.in));
String line = null;
try {
while ((line = stdin.readLine()) != null) {
String decodedLine = decodeHexString(line);
String transformedLine = transformHTTPMessage(decodedLine);
String encodedLine = encodeHexString(transformedLine);
System.out.println(encodedLine);
}
} catch (IOException e) {
}
}
}
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env node
const readline = require("readline");
const StringDecoder = require("string_decoder").StringDecoder
const rl = readline.createInterface({
input: process.stdin
});
var ignoreIds = new Set();
var ignoreAddresses = "/api";
const decoder = new StringDecoder("utf8");
function convertHexString(hex) {
var bytes = [];
for (var i = 0; i < hex.length - 1; i += 2) {
bytes.push(parseInt(hex.substr(i, 2), 16));
}
return decoder.write(Buffer.from(bytes));
}
function log(output) {
console.error("===================");
console.error(output);
}
function shouldOutputLine(request) {
const components = request.split("\n");
const header = components[0].split(" ");
const type = parseInt(header[0]);
const tag = header[1];
if (type === 3) {
return true;
}
if (type === 1) {
// Check if it's oauth
const endpoint = components[1].split(" ")[1];
if (!endpoint.startsWith(ignoreAddresses)) {
ignoreIds.add(tag);
return false;
}
} else if (type === 2) {
if (ignoreIds.has(tag)) {
ignoreIds.delete(tag);
return false;
}
}
return true;
}
rl.on("line", (input) => {
const str = convertHexString(input);
console.log(input);
if (shouldOutputLine(str)) {
log(str);
}
});
+76
View File
@@ -0,0 +1,76 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import fileinput
import binascii
# Used to find end of the Headers section
EMPTY_LINE = b'\r\n\r\n'
def log(msg):
"""
Logging to STDERR as STDOUT and STDIN used for data transfer
@type msg: str or byte string
@param msg: Message to log to STDERR
"""
try:
msg = str(msg) + '\n'
except:
pass
sys.stderr.write(msg)
sys.stderr.flush()
def find_end_of_headers(byte_data):
"""
Finds where the header portion ends and the content portion begins.
@type byte_data: str or byte string
@param byte_data: Hex decoded req or resp string
"""
return byte_data.index(EMPTY_LINE) + 4
def process_stdin():
"""
Process STDIN and output to STDOUT
"""
for raw_line in fileinput.input():
line = raw_line.rstrip()
# Decode base64 encoded line
decoded = bytes.fromhex(line)
# Split into metadata and payload, the payload is headers + body
(raw_metadata, payload) = decoded.split(b'\n', 1)
# Split into headers and payload
headers_pos = find_end_of_headers(payload)
raw_headers = payload[:headers_pos]
raw_content = payload[headers_pos:]
log('===================================')
request_type_id = int(raw_metadata.split(b' ')[0])
log('Request type: {}'.format({
1: 'Request',
2: 'Original Response',
3: 'Replayed Response'
}[request_type_id]))
log('===================================')
log('Original data:')
log(line)
log('Decoded request:')
log(decoded)
encoded = binascii.hexlify(raw_metadata + b'\n' + raw_headers + raw_content).decode('ascii')
log('Encoded data:')
log(encoded)
sys.stdout.write(encoded + '\n')
if __name__ == '__main__':
process_stdin()
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env ruby
# encoding: utf-8
while data = STDIN.gets # continuously read line from STDIN
next unless data
data = data.chomp # remove end of line symbol
decoded = [data].pack("H*") # decode base64 encoded request
# decoded value is raw HTTP payload, example:
#
# POST /post HTTP/1.1
# Content-Length: 7
# Host: www.w3.org
#
# a=1&b=2"
encoded = decoded.unpack("H*").first # encoding back to base64
# Emit request back
# You can skip this if want to filter out request
STDOUT.puts encoded
end
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
#
# `xxd` utility included into vim-common package
# It allow hex decoding/encoding
#
# This example may broke if you request contains `null` string, you may consider using pipes instead.
# See: https://github.com/buger/gor/issues/309
#
function log {
if [[ ! -v GOR_TEST ]]; then # if we are not testing
# Logging to stderr, because stdout/stdin used for data transfer
>&2 echo "[DEBUG][ECHO] $1"
fi
}
while read line; do
decoded=$(echo -e "$line" | xxd -r -p)
header=$(echo -e "$decoded" | head -n +1)
payload=$(echo -e "$decoded" | tail -n +2)
encoded=$(echo -e "$header\n$payload" | xxd -p | tr -d "\\n")
log ""
log "==================================="
case ${header:0:1} in
"1")
log "Request type: Request"
;;
"2")
log "Request type: Original Response"
;;
"3")
log "Request type: Replayed Response"
;;
*)
log "Unknown request type $header"
esac
echo "$encoded"
log "==================================="
log "Original data: $line"
log "Decoded request: $decoded"
log "Encoded data: $encoded"
done;
+122
View File
@@ -0,0 +1,122 @@
/*
This middleware made for auth system that randomly generate access tokens, which used later for accessing secure content. Since there is no pre-defined token value, naive approach without middleware (or if middleware use only request payloads) will fail, because replayed server have own tokens, not synced with origin. To fix this, our middleware should take in account responses of replayed and origin server, store `originalToken -> replayedToken` aliases and rewrite all requests using this token to use replayed alias. See `middleware_test.go#TestTokenMiddleware` test for examples of using this middleware.
How middleware works:
Original request +--------------+
+-------------+----------STDIN---------->+ |
| Gor input | | Middleware |
+-------------+----------STDIN---------->+ |
Original response +------+---+---+
| ^
+-------------+ Modified request v |
| Gor output +<---------STDOUT-----------------+ |
+-----+-------+ |
| |
| Replayed response |
+------------------STDIN----------------->----+
*/
package main
import (
"bufio"
"bytes"
"encoding/hex"
"fmt"
"os"
"github.com/buger/goreplay/proto"
)
// requestID -> originalToken
var originalTokens map[string][]byte
// originalToken -> replayedToken
var tokenAliases map[string][]byte
func main() {
originalTokens = make(map[string][]byte)
tokenAliases = make(map[string][]byte)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
encoded := scanner.Bytes()
buf := make([]byte, len(encoded)/2)
hex.Decode(buf, encoded)
process(buf)
}
}
func process(buf []byte) {
// First byte indicate payload type, possible values:
// 1 - Request
// 2 - Response
// 3 - ReplayedResponse
payloadType := buf[0]
headerSize := bytes.IndexByte(buf, '\n') + 1
header := buf[:headerSize-1]
// Header contains space separated values of: request type, request id, and request start time (or round-trip time for responses)
meta := bytes.Split(header, []byte(" "))
// For each request you should receive 3 payloads (request, response, replayed response) with same request id
reqID := string(meta[1])
payload := buf[headerSize:]
Debug("Received payload:", string(buf))
switch payloadType {
case '1': // Request
if bytes.Equal(proto.Path(payload), []byte("/token")) {
originalTokens[reqID] = []byte{}
Debug("Found token request:", reqID)
} else {
token, vs, _ := proto.PathParam(payload, []byte("token"))
if vs != -1 { // If there is GET token param
if alias, ok := tokenAliases[string(token)]; ok {
// Rewrite original token to alias
payload = proto.SetPathParam(payload, []byte("token"), alias)
// Copy modified payload to our buffer
buf = append(buf[:headerSize], payload...)
}
}
}
// Emitting data back
os.Stdout.Write(encode(buf))
case '2': // Original response
if _, ok := originalTokens[reqID]; ok {
// Token is inside response body
secureToken := proto.Body(payload)
originalTokens[reqID] = secureToken
Debug("Remember origial token:", string(secureToken))
}
case '3': // Replayed response
if originalToken, ok := originalTokens[reqID]; ok {
delete(originalTokens, reqID)
secureToken := proto.Body(payload)
tokenAliases[string(originalToken)] = secureToken
Debug("Create alias for new token token, was:", string(originalToken), "now:", string(secureToken))
}
}
}
func encode(buf []byte) []byte {
dst := make([]byte, len(buf)*2+1)
hex.Encode(dst, buf)
dst[len(dst)-1] = '\n'
return dst
}
func Debug(args ...interface{}) {
if os.Getenv("GOR_TEST") == "" { // if we are not testing
fmt.Fprint(os.Stderr, "[DEBUG][TOKEN-MOD] ")
fmt.Fprintln(os.Stderr, args...)
}
}
+21
View File
@@ -0,0 +1,21 @@
module github.com/buger/goreplay
go 1.14
require (
github.com/Shopify/sarama v1.26.4
github.com/araddon/gou v0.0.0-20190110011759-c797efecbb61 // indirect
github.com/aws/aws-sdk-go v1.33.2
github.com/bitly/go-hostpool v0.1.0 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/google/gopacket v1.1.18
github.com/klauspost/compress v1.10.10 // indirect
github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b
github.com/pbnjay/memory v0.0.0-20201129165224-b12e5d931931
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.5.1
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd
)
+112
View File
@@ -0,0 +1,112 @@
github.com/Shopify/sarama v1.26.4 h1:+17TxUq/PJEAfZAll0T7XJjSgQWCpaQSoki/x5yN8o8=
github.com/Shopify/sarama v1.26.4/go.mod h1:NbSGBSSndYaIhRcBtY9V0U7AyH+x71bG668AuWys/yU=
github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/araddon/gou v0.0.0-20190110011759-c797efecbb61 h1:Xz25cuW4REGC5W5UtpMU3QItMIImag615HiQcRbxqKQ=
github.com/araddon/gou v0.0.0-20190110011759-c797efecbb61/go.mod h1:ikc1XA58M+Rx7SEbf0bLJCfBkwayZ8T5jBo5FXK8Uz8=
github.com/aws/aws-sdk-go v1.33.2 h1:8TVrnPnSD7I+AmDp66xBUvS3K0J+jH09YXdrkJ34ey0=
github.com/aws/aws-sdk-go v1.33.2/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/bitly/go-hostpool v0.1.0 h1:XKmsF6k5el6xHG3WPJ8U0Ku/ye7njX7W81Ng7O2ioR0=
github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q=
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.7.2 h1:2QxQoC1TS09S7fhCPsrvqYdvP1H5M1P1ih5ABm3BTYk=
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gopacket v1.1.18 h1:lum7VRA9kdlvBi7/v2p7/zcbkduHaCH/SVVyurs7OpY=
github.com/google/gopacket v1.1.18/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b h1:v29yPGHhOqw7VHEnTeQFAth3SsBrmwc8JfuhNY0G34k=
github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b/go.mod h1:5MWrJXKRQyhQdUCF+vu6U5c4nQpg70vW3eHaU0/AYbU=
github.com/pbnjay/memory v0.0.0-20201129165224-b12e5d931931 h1:EeWknjeRU+R3O4ghG7XZCpgSfJNStZyEP8aWyQwJM8s=
github.com/pbnjay/memory v0.0.0-20201129165224-b12e5d931931/go.mod h1:RMU2gJXhratVxBDTFeOdNhd540tG57lt9FIUV0YLvIQ=
github.com/pierrec/lz4 v2.4.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw=
gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM=
gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
gopkg.in/jcmturner/goidentity.v3 v3.0.0 h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI=
gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
gopkg.in/jcmturner/gokrb5.v7 v7.5.0 h1:a9tsXlIDD9SKxotJMK3niV7rPZAJeX2aD/0yg3qlIrg=
gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU=
gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+65 -18
View File
@@ -4,36 +4,57 @@ package main
import (
"flag"
"fmt"
"log"
"net/http"
"net/http/httputil"
_ "net/http/pprof"
"os"
"runtime/debug"
"os/signal"
"runtime"
_ "runtime/debug"
"runtime/pprof"
"syscall"
"time"
)
var (
mode string
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
memprofile = flag.String("memprofile", "", "write memory profile to this file")
)
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rb, _ := httputil.DumpRequest(r, false)
log.Println(string(rb))
next.ServeHTTP(w, r)
})
}
func main() {
// Don't exit on panic
defer func() {
if r := recover(); r != nil {
if _, ok := r.(error); !ok {
fmt.Printf("PANIC: pkg: %v %s \n", r, debug.Stack())
}
if os.Getenv("GOMAXPROCS") == "" {
runtime.GOMAXPROCS(runtime.NumCPU() * 2)
}
args := os.Args[1:]
var plugins *InOutPlugins
if len(args) > 0 && args[0] == "file-server" {
if len(args) != 2 {
log.Fatal("You should specify port and IP (optional) for the file server. Example: `gor file-server :80`")
}
}()
dir, _ := os.Getwd()
fmt.Println("Version:", VERSION)
Debug(0, "Started example file server for current directory on address ", args[1])
flag.Parse()
InitPlugins()
log.Fatal(http.ListenAndServe(args[1], loggingMiddleware(http.FileServer(http.Dir(dir)))))
} else {
flag.Parse()
checkSettings()
plugins = NewPlugins()
}
if len(Plugins.Inputs) == 0 || len(Plugins.Outputs) == 0 {
log.Printf("[PPID %d and PID %d] Version:%s\n", os.Getppid(), os.Getpid(), VERSION)
if len(plugins.Inputs) == 0 || len(plugins.Outputs) == 0 {
log.Fatal("Required at least 1 input and 1 output")
}
@@ -45,7 +66,34 @@ func main() {
profileCPU(*cpuprofile)
}
Start(nil)
if Settings.Pprof != "" {
go func() {
log.Println(http.ListenAndServe(Settings.Pprof, nil))
}()
}
closeCh := make(chan int)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
if Settings.ExitAfter > 0 {
log.Printf("Running gor for a duration of %s\n", Settings.ExitAfter)
time.AfterFunc(Settings.ExitAfter, func() {
log.Printf("gor run timeout %s\n", Settings.ExitAfter)
close(closeCh)
})
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
exit := 0
select {
case <-c:
exit = 1
case <-closeCh:
exit = 0
}
emitter.Close()
os.Exit(exit)
}
func profileCPU(cpuprofile string) {
@@ -56,10 +104,9 @@ func profileCPU(cpuprofile string) {
}
pprof.StartCPUProfile(f)
time.AfterFunc(60*time.Second, func() {
time.AfterFunc(30*time.Second, func() {
pprof.StopCPUProfile()
f.Close()
log.Println("Stop profiling after 60 seconds")
})
}
}
@@ -70,7 +117,7 @@ func profileMEM(memprofile string) {
if err != nil {
log.Fatal(err)
}
time.AfterFunc(60*time.Second, func() {
time.AfterFunc(30*time.Second, func() {
pprof.WriteHeapProfile(f)
f.Close()
})
+18 -21
View File
@@ -1,45 +1,42 @@
package main
import (
"time"
"log"
"runtime"
"strconv"
)
const (
rate = 5
"time"
)
type GorStat struct {
statName string
latest int
mean int
max int
count int
statName string
rateMs int
latest int
mean int
max int
count int
}
func NewGorStat(statName string) (s *GorStat) {
func NewGorStat(statName string, rateMs int) (s *GorStat) {
s = new(GorStat)
s.statName = statName
s.rateMs = rateMs
s.latest = 0
s.mean = 0
s.max = 0
s.count = 0
if Settings.stats {
log.Println(s.statName + ":latest,mean,max,count,count/second")
if Settings.Stats {
go s.reportStats()
}
return
}
func (s *GorStat) Write(latest int) {
if Settings.stats {
if Settings.Stats {
if latest > s.max {
s.max = latest
}
if latest != 0 {
s.mean = (s.mean + latest) / 2
s.mean = ((s.mean * s.count) + latest) / (s.count + 1)
}
s.latest = latest
s.count = s.count + 1
@@ -54,14 +51,14 @@ func (s *GorStat) Reset() {
}
func (s *GorStat) String() string {
return s.statName + ":" + strconv.Itoa(s.latest) + "," + strconv.Itoa(s.mean) + "," + strconv.Itoa(s.max) + "," + strconv.Itoa(s.count) + "," + strconv.Itoa(s.count / rate)
return s.statName + ":" + strconv.Itoa(s.latest) + "," + strconv.Itoa(s.mean) + "," + strconv.Itoa(s.max) + "," + strconv.Itoa(s.count) + "," + strconv.Itoa(s.count/(s.rateMs/1000.0)) + "," + strconv.Itoa(runtime.NumGoroutine())
}
func (s *GorStat) reportStats() {
Debug(0, "\n", s.statName+":latest,mean,max,count,count/second,gcount")
for {
log.Println(s)
s.Reset()
time.Sleep(rate * time.Second)
Debug(0, "\n", s)
s.Reset()
time.Sleep(time.Duration(s.rateMs) * time.Millisecond)
}
}
+46
View File
@@ -0,0 +1,46 @@
require "language/go"
class Gor < Formula
desc "Real-time HTTP traffic replay tool written in Go"
homepage "https://gortool.com"
url "https://github.com/buger/gor/archive/v0.14.0.tar.gz"
sha256 "62260a6f5cabde571b91d5762fba9c47691643df0a58565cbe808854cd064dc8"
head "https://github.com/buger/gor.git"
bottle do
cellar :any_skip_relocation
sha256 "c382403de70a41b7445920a02051f5e82030704aaaae70cfcd4e8f401cc87f6a" => :el_capitan
sha256 "4b76b3785584897800e87967f1af9510208faefe46f57d7bd6f8b40a7133c19b" => :yosemite
sha256 "d186cb1566d33ab8f78215e69934f49dd96becb1c236905b4502d94399ae1974" => :mavericks
end
depends_on "go" => :build
go_resource "github.com/bitly/go-hostpool" do
url "https://github.com/bitly/go-hostpool.git",
:revision => "d0e59c22a56e8dadfed24f74f452cea5a52722d2"
end
go_resource "github.com/buger/elastigo" do
url "https://github.com/buger/elastigo.git",
:revision => "23fcfd9db0d8be2189a98fdab77a4c90fcc3a1e9"
end
go_resource "github.com/google/gopacket" do
url "https://github.com/google/gopacket.git",
:revision => "aa09ced736460d76535444c825932a0742975f7d"
end
def install
ENV["GOPATH"] = buildpath
mkdir_p buildpath/"src/github.com/buger/"
ln_sf buildpath, buildpath/"src/github.com/buger/gor"
Language::Go.stage_deps resources, buildpath/"src"
system "go", "build", "-o", "#{bin}/gor", "-ldflags", "-X main.VERSION \"#{version}\""
end
test do
assert_match version.to_s, shell_output("#{bin}/gor", 1)
end
end
+196
View File
@@ -0,0 +1,196 @@
package main
import (
"bytes"
"encoding/base64"
"hash/fnv"
"strings"
"github.com/buger/goreplay/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.HeaderRewrite) == 0 &&
len(config.HeaderFilters) == 0 &&
len(config.HeaderNegativeFilters) == 0 &&
len(config.HeaderBasicAuthFilters) == 0 &&
len(config.HeaderHashFilters) == 0 &&
len(config.ParamHashFilters) == 0 &&
len(config.Params) == 0 &&
len(config.Headers) == 0 &&
len(config.Methods) == 0 {
return nil
}
return &HTTPModifier{config: config}
}
func (m *HTTPModifier) Rewrite(payload []byte) (response []byte) {
if !proto.HasRequestTitle(payload) {
return payload
}
if len(m.config.Methods) > 0 {
method := proto.Method(payload)
matched := false
for _, m := range m.config.Methods {
if bytes.Equal(method, m) {
matched = true
break
}
}
if !matched {
return
}
}
if len(m.config.Headers) > 0 {
for _, header := range m.config.Headers {
payload = proto.SetHeader(payload, []byte(header.Name), []byte(header.Value))
}
}
if len(m.config.Params) > 0 {
for _, param := range m.config.Params {
payload = proto.SetPathParam(payload, param.Name, param.Value)
}
}
if len(m.config.URLRegexp) > 0 {
path := proto.Path(payload)
matched := false
for _, f := range m.config.URLRegexp {
if f.regexp.Match(path) {
matched = true
break
}
}
if !matched {
return
}
}
if len(m.config.URLNegativeRegexp) > 0 {
path := proto.Path(payload)
for _, f := range m.config.URLNegativeRegexp {
if f.regexp.Match(path) {
return
}
}
}
if len(m.config.HeaderFilters) > 0 {
for _, f := range m.config.HeaderFilters {
value := proto.Header(payload, f.name)
if len(value) == 0 {
return
}
if !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.HeaderBasicAuthFilters) > 0 {
for _, f := range m.config.HeaderBasicAuthFilters {
value := proto.Header(payload, []byte("Authorization"))
if len(value) > 0 {
valueString := string(value)
trimmedBasicAuthEncoded := strings.TrimPrefix(valueString, "Basic ")
if strings.Compare(valueString, trimmedBasicAuthEncoded) != 0 {
decodedAuth, _ := base64.StdEncoding.DecodeString(trimmedBasicAuthEncoded)
if !f.regexp.Match(decodedAuth) {
return
}
}
}
}
}
if len(m.config.HeaderHashFilters) > 0 {
for _, f := range m.config.HeaderHashFilters {
value := proto.Header(payload, f.name)
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
}
}
}
if len(m.config.HeaderRewrite) > 0 {
for _, f := range m.config.HeaderRewrite {
value := proto.Header(payload, f.header)
if len(value) == 0 {
break
}
if f.src.Match(value) {
newValue := f.src.ReplaceAll(value, f.target)
payload = proto.SetHeader(payload, f.header, newValue)
}
}
}
return payload
}
+299
View File
@@ -0,0 +1,299 @@
package main
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
// HTTPModifierConfig holds configuration options for built-in traffic modifier
type HTTPModifierConfig struct {
URLNegativeRegexp HTTPURLRegexp `json:"http-disallow-url"`
URLRegexp HTTPURLRegexp `json:"http-allow-url"`
URLRewrite URLRewriteMap `json:"http-rewrite-url"`
HeaderRewrite HeaderRewriteMap `json:"http-rewrite-header"`
HeaderFilters HTTPHeaderFilters `json:"http-allow-header"`
HeaderNegativeFilters HTTPHeaderFilters `json:"http-disallow-header"`
HeaderBasicAuthFilters HTTPHeaderBasicAuthFilters `json:"http-basic-auth-filter"`
HeaderHashFilters HTTPHashFilters `json:"http-header-limiter"`
ParamHashFilters HTTPHashFilters `json:"http-param-limiter"`
Params HTTPParams `json:"http-set-param"`
Headers HTTPHeaders `json:"http-set-header"`
Methods HTTPMethods `json:"http-allow-method"`
}
//
// 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)
}
// Set method to implement flags.Value
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$)")
}
val := strings.TrimSpace(valArr[1])
r, err := regexp.Compile(val)
if err != nil {
return err
}
*h = append(*h, headerFilter{name: []byte(valArr[0]), regexp: r})
return nil
}
//
// Handling of --http-basic-auth-filter option
//
type basicAuthFilter struct {
regexp *regexp.Regexp
}
// HTTPHeaderBasicAuthFilters holds list of regxp to match basic Auth header values
type HTTPHeaderBasicAuthFilters []basicAuthFilter
func (h *HTTPHeaderBasicAuthFilters) String() string {
return fmt.Sprint(*h)
}
// Set method to implement flags.Value
func (h *HTTPHeaderBasicAuthFilters) Set(value string) error {
r, err := regexp.Compile(value)
if err != nil {
return err
}
*h = append(*h, basicAuthFilter{regexp: r})
return nil
}
//
// Handling of --http-allow-header-hash and --http-allow-param-hash options
//
type hashFilter struct {
name []byte
percent uint32
}
// HTTPHashFilters represents a slice of header hash filters
type HTTPHashFilters []hashFilter
func (h *HTTPHashFilters) String() string {
return fmt.Sprint(*h)
}
// Set method to implement flags.Value
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])}
val := strings.TrimSpace(valArr[1])
if strings.Contains(val, "%") {
p, _ := strconv.ParseInt(val[:len(val)-1], 0, 0)
f.percent = uint32(p)
} else if strings.Contains(val, "/") {
// DEPRECATED format
var num, den uint64
fracArr := strings.Split(val, "/")
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 httpHeader struct {
Name string
Value string
}
// HTTPHeaders is a slice of headers that must appended
type HTTPHeaders []httpHeader
func (h *HTTPHeaders) String() string {
return fmt.Sprint(*h)
}
// Set method to implement flags.Value
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 httpParam struct {
Name []byte
Value []byte
}
// HTTPParams filters for --http-set-param
type HTTPParams []httpParam
func (h *HTTPParams) String() string {
return fmt.Sprint(*h)
}
// Set method to implement flags.Value
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
//
// HTTPMethods holds values for method allowed
type HTTPMethods [][]byte
func (h *HTTPMethods) String() string {
return fmt.Sprint(*h)
}
// Set method to implement flags.Value
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
}
// URLRewriteMap holds regexp and data to modify URL
type URLRewriteMap []urlRewrite
func (r *URLRewriteMap) String() string {
return fmt.Sprint(*r)
}
// Set method to implement flags.Value
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-rewrite-header option
//
type headerRewrite struct {
header []byte
src *regexp.Regexp
target []byte
}
// HeaderRewriteMap holds regexp and data to rewrite headers
type HeaderRewriteMap []headerRewrite
func (r *HeaderRewriteMap) String() string {
return fmt.Sprint(*r)
}
// Set method to implement flags.Value
func (r *HeaderRewriteMap) Set(value string) error {
headerArr := strings.SplitN(value, ":", 2)
if len(headerArr) < 2 {
return errors.New("need both header, regexp and rewrite target, colon-delimited (ex. Header: regexp,target)")
}
header := headerArr[0]
valArr := strings.SplitN(strings.TrimSpace(headerArr[1]), ",", 2)
if len(valArr) < 2 {
return errors.New("need both header, regexp and rewrite target, colon-delimited (ex. Header: regexp,target)")
}
regexp, err := regexp.Compile(valArr[0])
if err != nil {
return err
}
*r = append(*r, headerRewrite{header: []byte(header), src: regexp, target: []byte(valArr[1])})
return nil
}
//
// Handling of --http-allow-url option
//
type urlRegexp struct {
regexp *regexp.Regexp
}
// HTTPURLRegexp a slice of regexp to match URLs
type HTTPURLRegexp []urlRegexp
func (r *HTTPURLRegexp) String() string {
return fmt.Sprint(*r)
}
// Set method to implement flags.Value
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 :")
}
}
+320
View File
@@ -0,0 +1,320 @@
package main
import (
"bytes"
"testing"
"github.com/buger/goreplay/proto"
)
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")
}
filters = HTTPHeaderFilters{}
// Setting filter that not match our header
filters.Set("Host: www*")
modifier = NewHTTPModifier(&HTTPModifierConfig{
HeaderNegativeFilters: filters,
})
if len(modifier.Rewrite(payload)) != 0 {
t.Error("Request should not pass filters")
}
}
func TestHTTPHeaderBasicAuthFilters(t *testing.T) {
filters := HTTPHeaderBasicAuthFilters{}
filters.Set("^customer[0-9].*")
modifier := NewHTTPModifier(&HTTPModifierConfig{
HeaderBasicAuthFilters: filters,
})
//Encoded UserId:Password = customer3:welcome
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nAuthorization: Basic Y3VzdG9tZXIzOndlbGNvbWU=\r\n\r\na=1&b=2")
if len(modifier.Rewrite(payload)) == 0 {
t.Error("Request should pass filters")
}
//customer6:rest@123^TEST
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 88\r\nAuthorization: Basic Y3VzdG9tZXI2OnJlc3RAMTIzXlRFU1Q==\r\n\r\na=1&b=2")
if len(modifier.Rewrite(payload)) == 0 {
t.Error("Request should pass filters")
}
filters = HTTPHeaderBasicAuthFilters{}
// Setting filter that not match our header
filters.Set("^(homer simpson|mickey mouse).*")
modifier = NewHTTPModifier(&HTTPModifierConfig{
HeaderBasicAuthFilters: filters,
})
if len(modifier.Rewrite(payload)) != 0 {
t.Error("Request should not pass filters")
}
//mickey mouse:happy123
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 88\r\nAuthorization: Basic bWlja2V5IG1vdXNlOmhhcHB5MTIz\r\n\r\na=1&b=2")
if len(modifier.Rewrite(payload)) == 0 {
t.Error("Request should pass filters")
}
}
func TestHTTPModifierURLRewrite(t *testing.T) {
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 TestHTTPModifierHeaderRewrite(t *testing.T) {
var header, newHeader []byte
rewrites := HeaderRewriteMap{}
payload := []byte("GET / HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
err := rewrites.Set("Host: (.*).w3.org,$1.beta.w3.org")
if err != nil {
t.Error("Should not error", err)
}
modifier := NewHTTPModifier(&HTTPModifierConfig{
HeaderRewrite: rewrites,
})
header = []byte("www.beta.w3.org")
if newHeader = proto.Header(modifier.Rewrite(payload), []byte("Host")); !bytes.Equal(newHeader, header) {
t.Error("Request header should have been rewritten, wasn't", string(newHeader), string(header))
}
}
func TestHTTPModifierHeaderHashFilters(t *testing.T) {
filters := HTTPHashFilters{}
filters.Set("Header2:1/2")
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))
}
}
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"bytes"
"compress/gzip"
"fmt"
"io/ioutil"
"net/http/httputil"
"strconv"
"github.com/buger/goreplay/proto"
)
func prettifyHTTP(p []byte) []byte {
tEnc := bytes.Equal(proto.Header(p, []byte("Transfer-Encoding")), []byte("chunked"))
cEnc := bytes.Equal(proto.Header(p, []byte("Content-Encoding")), []byte("gzip"))
if !(tEnc || cEnc) {
return p
}
headersPos := proto.MIMEHeadersEndPos(p)
if headersPos < 5 || headersPos > len(p) {
return p
}
headers := p[:headersPos]
content := p[headersPos:]
if tEnc {
buf := bytes.NewReader(content)
r := httputil.NewChunkedReader(buf)
content, _ = ioutil.ReadAll(r)
headers = proto.DeleteHeader(headers, []byte("Transfer-Encoding"))
newLen := strconv.Itoa(len(content))
headers = proto.SetHeader(headers, []byte("Content-Length"), []byte(newLen))
}
if cEnc {
buf := bytes.NewReader(content)
g, err := gzip.NewReader(buf)
if err != nil {
Debug(1, "[Prettifier] GZIP encoding error:", err)
return []byte{}
}
content, err = ioutil.ReadAll(g)
if err != nil {
Debug(1, fmt.Sprintf("[HTTP-PRETTIFIER] %q", err))
return p
}
headers = proto.DeleteHeader(headers, []byte("Content-Encoding"))
newLen := strconv.Itoa(len(content))
headers = proto.SetHeader(headers, []byte("Content-Length"), []byte(newLen))
}
newPayload := append(headers, content...)
return newPayload
}
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"bytes"
"compress/gzip"
"strconv"
"testing"
"github.com/buger/goreplay/proto"
)
func TestHTTPPrettifierGzip(t *testing.T) {
b := bytes.NewBufferString("")
w := gzip.NewWriter(b)
w.Write([]byte("test"))
w.Close()
size := strconv.Itoa(len(b.Bytes()))
payload := []byte("HTTP/1.1 200 OK\r\nContent-Length: " + size + "\r\nContent-Encoding: gzip\r\n\r\n")
payload = append(payload, b.Bytes()...)
newPayload := prettifyHTTP(payload)
if string(newPayload) != "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ntest" {
t.Errorf("Payload not match %q", string(newPayload))
}
}
func TestHTTPPrettifierChunked(t *testing.T) {
payload := []byte("POST / HTTP/1.1\r\nHost: www.w3.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
payload = prettifyHTTP(payload)
if string(proto.Header(payload, []byte("Content-Length"))) != "23" {
t.Errorf("payload should have content length of 23")
}
}
+26 -6
View File
@@ -4,24 +4,33 @@ import (
"time"
)
// DummyInput used for debugging. It generate 1 "GET /"" request per second.
type DummyInput struct {
data chan []byte
quit chan struct{}
}
// NewDummyInput constructor for DummyInput
func NewDummyInput(options string) (di *DummyInput) {
di = new(DummyInput)
di.data = make(chan []byte)
di.quit = make(chan struct{})
go di.emit()
return
}
func (i *DummyInput) Read(data []byte) (int, error) {
buf := <-i.data
copy(data, buf)
return len(buf), nil
// PluginRead reads message from this plugin
func (i *DummyInput) PluginRead() (*Message, error) {
var msg Message
select {
case <-i.quit:
return nil, ErrorStopped
case buf := <-i.data:
msg.Meta, msg.Data = payloadMetaWithBody(buf)
return &msg, nil
}
}
func (i *DummyInput) emit() {
@@ -30,7 +39,12 @@ func (i *DummyInput) emit() {
for {
select {
case <-ticker.C:
i.data <- []byte("GET / HTTP/1.1\r\n\r\n")
uuid := uuid()
reqh := payloadHeader(RequestPayload, uuid, time.Now().UnixNano(), -1)
i.data <- append(reqh, []byte("GET / HTTP/1.1\r\nHost: www.w3.org\r\nUser-Agent: Go 1.1 package http\r\nAccept-Encoding: gzip\r\n\r\n")...)
resh := payloadHeader(ResponsePayload, uuid, time.Now().UnixNano()+1, 1)
i.data <- append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...)
}
}
}
@@ -38,3 +52,9 @@ func (i *DummyInput) emit() {
func (i *DummyInput) String() string {
return "Dummy Input"
}
// Close closes this plugins
func (i *DummyInput) Close() error {
close(i.quit)
return nil
}
+239 -29
View File
@@ -1,66 +1,276 @@
package main
import (
"encoding/gob"
"log"
"bufio"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
)
type FileInput struct {
data chan []byte
path string
decoder *gob.Decoder
type fileInputReader struct {
reader *bufio.Reader
data []byte
file io.ReadCloser
timestamp int64
closed int32 // Value of 0 indicates that the file is still open.
s3 bool
}
func NewFileInput(path string) (i *FileInput) {
func (f *fileInputReader) parseNext() error {
payloadSeparatorAsBytes := []byte(payloadSeparator)
var buffer bytes.Buffer
for {
line, err := f.reader.ReadBytes('\n')
if err != nil {
if err != io.EOF {
Debug(1, err)
} else {
f.Close()
}
return err
}
if bytes.Equal(payloadSeparatorAsBytes[1:], line) {
asBytes := buffer.Bytes()
meta := payloadMeta(asBytes)
f.timestamp, _ = strconv.ParseInt(string(meta[2]), 10, 64)
f.data = asBytes[:len(asBytes)-1]
return nil
}
buffer.Write(line)
}
}
func (f *fileInputReader) ReadPayload() []byte {
defer f.parseNext()
return f.data
}
// Close closes this plugin
func (f *fileInputReader) Close() error {
if atomic.LoadInt32(&f.closed) == 0 {
atomic.StoreInt32(&f.closed, 1)
f.file.Close()
}
return nil
}
func newFileInputReader(path string) *fileInputReader {
var file io.ReadCloser
var err error
if strings.HasPrefix(path, "s3://") {
file = NewS3ReadCloser(path)
} else {
file, err = os.Open(path)
}
if err != nil {
Debug(0, fmt.Sprintf("[INPUT-FILE] err: %q", err))
return nil
}
r := &fileInputReader{file: file, closed: 0}
if strings.HasSuffix(path, ".gz") {
gzReader, err := gzip.NewReader(file)
if err != nil {
Debug(0, fmt.Sprintf("[INPUT-FILE] err: %q", err))
return nil
}
r.reader = bufio.NewReader(gzReader)
} else {
r.reader = bufio.NewReader(file)
}
r.parseNext()
return r
}
// FileInput can read requests generated by FileOutput
type FileInput struct {
mu sync.Mutex
data chan []byte
exit chan bool
path string
readers []*fileInputReader
speedFactor float64
loop bool
}
// NewFileInput constructor for FileInput. Accepts file path as argument.
func NewFileInput(path string, loop bool) (i *FileInput) {
i = new(FileInput)
i.data = make(chan []byte)
i.data = make(chan []byte, 1000)
i.exit = make(chan bool)
i.path = path
i.Init(path)
i.speedFactor = 1
i.loop = loop
if err := i.init(); err != nil {
return
}
go i.emit()
return
}
func (i *FileInput) Init(path string) {
file, err := os.Open(path)
func (i *FileInput) init() (err error) {
defer i.mu.Unlock()
i.mu.Lock()
if err != nil {
log.Fatal(i, "Cannot open file %q. Error: %s", path, err)
var matches []string
if strings.HasPrefix(i.path, "s3://") {
sess := session.Must(session.NewSession(awsConfig()))
svc := s3.New(sess)
bucket, key := parseS3Url(i.path)
params := &s3.ListObjectsInput{
Bucket: aws.String(bucket),
Prefix: aws.String(key),
}
resp, err := svc.ListObjects(params)
if err != nil {
Debug(0, "[INPUT-FILE] Error while retreiving list of files from S3", i.path, err)
return err
}
for _, c := range resp.Contents {
matches = append(matches, "s3://"+bucket+"/"+(*c.Key))
}
} else if matches, err = filepath.Glob(i.path); err != nil{
Debug(0, "[INPUT-FILE] Wrong file pattern", i.path, err)
return
}
i.decoder = gob.NewDecoder(file)
if len(matches) == 0 {
Debug(0, "[INPUT-FILE] No files match pattern: ", i.path)
return errors.New("No matching files")
}
i.readers = make([]*fileInputReader, len(matches))
for idx, p := range matches {
i.readers[idx] = newFileInputReader(p)
}
return nil
}
func (i *FileInput) Read(data []byte) (int, error) {
buf := <-i.data
copy(data, buf)
return len(buf), nil
// PluginRead reads message from this plugin
func (i *FileInput) PluginRead() (*Message, error) {
var msg Message
select {
case <-i.exit:
return nil, ErrorStopped
case buf := <-i.data:
msg.Meta, msg.Data = payloadMetaWithBody(buf)
return &msg, nil
}
}
func (i *FileInput) String() string {
return "File input: " + i.path
}
// Find reader with smallest timestamp e.g next payload in row
func (i *FileInput) nextReader() (next *fileInputReader) {
for _, r := range i.readers {
if r == nil || atomic.LoadInt32(&r.closed) != 0 {
continue
}
if next == nil || r.timestamp < next.timestamp {
next = r
continue
}
}
return
}
func (i *FileInput) emit() {
var lastTime int64
var lastTime int64 = -1
for {
raw := new(RawRequest)
err := i.decoder.Decode(raw)
if err != nil {
select {
case <-i.exit:
return
default:
}
if lastTime != 0 {
time.Sleep(time.Duration(raw.Timestamp - lastTime))
}
lastTime = raw.Timestamp
reader := i.nextReader()
i.data <- raw.Request
if reader == nil {
if i.loop {
i.init()
lastTime = -1
continue
} else {
break
}
}
if lastTime != -1 {
diff := reader.timestamp - lastTime
lastTime = reader.timestamp
if i.speedFactor != 1 {
diff = int64(float64(diff) / i.speedFactor)
}
time.Sleep(time.Duration(diff))
} else {
lastTime = reader.timestamp
}
// Recheck if we have exited since last check.
select {
case <-i.exit:
return
default:
i.data <- reader.ReadPayload()
}
}
Debug(0, fmt.Sprintf("[INPUT-FILE] FileInput: end of file '%s'\n", i.path))
}
// Close closes this plugin
func (i *FileInput) Close() error {
defer i.mu.Unlock()
i.mu.Lock()
close(i.exit)
for _, r := range i.readers {
r.Close()
}
return nil
}
+360
View File
@@ -0,0 +1,360 @@
package main
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"os"
"sync"
"testing"
"time"
)
func TestInputFileWithGET(t *testing.T) {
input := NewTestInput()
rg := NewRequestGenerator([]PluginReader{input}, func() { input.EmitGET() }, 1)
readPayloads := []*Message{}
// Given a capture file with a GET request
expectedCaptureFile := CreateCaptureFile(rg)
defer expectedCaptureFile.TearDown()
// When the request is read from the capture file
err := ReadFromCaptureFile(expectedCaptureFile.file, 1, func(msg *Message) {
readPayloads = append(readPayloads, msg)
})
// The read request should match the original request
if err != nil {
t.Error(err)
} else if !expectedCaptureFile.PayloadsEqual(readPayloads){
t.Error("Request read back from file should match")
}
}
func TestInputFileWithPayloadLargerThan64Kb(t *testing.T) {
input := NewTestInput()
rg := NewRequestGenerator([]PluginReader{input}, func() { input.EmitSizedPOST(64 * 1024) }, 1)
readPayloads := []*Message{}
// Given a capture file with a request over 64Kb
expectedCaptureFile := CreateCaptureFile(rg)
defer expectedCaptureFile.TearDown()
// When the request is read from the capture file
err := ReadFromCaptureFile(expectedCaptureFile.file, 1, func(msg *Message) {
readPayloads = append(readPayloads, msg)
})
// The read request should match the original request
if err != nil {
t.Error(err)
} else if !expectedCaptureFile.PayloadsEqual(readPayloads){
t.Error("Request read back from file should match")
}
}
func TestInputFileWithGETAndPOST(t *testing.T) {
input := NewTestInput()
rg := NewRequestGenerator([]PluginReader{input}, func() {
input.EmitGET()
input.EmitPOST()
}, 2)
readPayloads := []*Message{}
// Given a capture file with a GET request
expectedCaptureFile := CreateCaptureFile(rg)
defer expectedCaptureFile.TearDown()
// When the requests are read from the capture file
err := ReadFromCaptureFile(expectedCaptureFile.file, 2, func(msg *Message) {
readPayloads = append(readPayloads, msg)
})
// The read requests should match the original request
if err != nil {
t.Error(err)
} else if !expectedCaptureFile.PayloadsEqual(readPayloads){
t.Error("Request read back from file should match")
}
}
func TestInputFileMultipleFilesWithRequestsOnly(t *testing.T) {
rnd := rand.Int63()
file1, _ := os.OpenFile(fmt.Sprintf("/tmp/%d_0", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
file1.Write([]byte("1 1 1\ntest1"))
file1.Write([]byte(payloadSeparator))
file1.Write([]byte("1 1 3\ntest2"))
file1.Write([]byte(payloadSeparator))
file1.Close()
file2, _ := os.OpenFile(fmt.Sprintf("/tmp/%d_1", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
file2.Write([]byte("1 1 2\ntest3"))
file2.Write([]byte(payloadSeparator))
file2.Write([]byte("1 1 4\ntest4"))
file2.Write([]byte(payloadSeparator))
file2.Close()
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false)
for i := '1'; i <= '4'; i++ {
msg, _ := input.PluginRead()
if msg.Meta[4] != byte(i) {
t.Error("Should emit requests in right order", string(msg.Meta))
}
}
os.Remove(file1.Name())
os.Remove(file2.Name())
}
func TestInputFileRequestsWithLatency(t *testing.T) {
rnd := rand.Int63()
file, _ := os.OpenFile(fmt.Sprintf("/tmp/%d", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
defer file.Close()
file.Write([]byte("1 1 100000000\nrequest1"))
file.Write([]byte(payloadSeparator))
file.Write([]byte("1 2 150000000\nrequest2"))
file.Write([]byte(payloadSeparator))
file.Write([]byte("1 3 250000000\nrequest3"))
file.Write([]byte(payloadSeparator))
input := NewFileInput(fmt.Sprintf("/tmp/%d", rnd), false)
start := time.Now().UnixNano()
for i := 0; i < 3; i++ {
input.PluginRead()
}
end := time.Now().UnixNano()
var expectedLatency int64 = 300000000 - 100000000
realLatency := end - start
if realLatency > expectedLatency {
t.Errorf("Should emit requests respecting latency. Expected: %v, real: %v", expectedLatency, realLatency)
}
}
func TestInputFileMultipleFilesWithRequestsAndResponses(t *testing.T) {
rnd := rand.Int63()
file1, _ := os.OpenFile(fmt.Sprintf("/tmp/%d_0", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
file1.Write([]byte("1 1 1\nrequest1"))
file1.Write([]byte(payloadSeparator))
file1.Write([]byte("2 1 1\nresponse1"))
file1.Write([]byte(payloadSeparator))
file1.Write([]byte("1 2 3\nrequest2"))
file1.Write([]byte(payloadSeparator))
file1.Write([]byte("2 2 3\nresponse2"))
file1.Write([]byte(payloadSeparator))
file1.Close()
file2, _ := os.OpenFile(fmt.Sprintf("/tmp/%d_1", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
file2.Write([]byte("1 3 2\nrequest3"))
file2.Write([]byte(payloadSeparator))
file2.Write([]byte("2 3 2\nresponse3"))
file2.Write([]byte(payloadSeparator))
file2.Write([]byte("1 4 4\nrequest4"))
file2.Write([]byte(payloadSeparator))
file2.Write([]byte("2 4 4\nresponse4"))
file2.Write([]byte(payloadSeparator))
file2.Close()
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false)
for i := '1'; i <= '4'; i++ {
msg, _ := input.PluginRead()
if msg.Meta[0] != '1' && msg.Meta[4] != byte(i) {
t.Error("Shound emit requests in right order", string(msg.Meta))
}
msg, _ = input.PluginRead()
if msg.Meta[0] != '2' && msg.Meta[4] != byte(i) {
t.Error("Shound emit responses in right order", string(msg.Meta))
}
}
os.Remove(file1.Name())
os.Remove(file2.Name())
}
func TestInputFileLoop(t *testing.T) {
rnd := rand.Int63()
file, _ := os.OpenFile(fmt.Sprintf("/tmp/%d", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
file.Write([]byte("1 1 1\ntest1"))
file.Write([]byte(payloadSeparator))
file.Write([]byte("1 1 2\ntest2"))
file.Write([]byte(payloadSeparator))
file.Close()
input := NewFileInput(fmt.Sprintf("/tmp/%d", rnd), true)
// Even if we have just 2 requests in file, it should indifinitly loop
for i := 0; i < 1000; i++ {
input.PluginRead()
}
input.Close()
os.Remove(file.Name())
}
func TestInputFileCompressed(t *testing.T) {
rnd := rand.Int63()
output := NewFileOutput(fmt.Sprintf("/tmp/%d_0.gz", rnd), &FileOutputConfig{FlushInterval: time.Minute, Append: true})
for i := 0; i < 1000; i++ {
output.PluginWrite(&Message{Meta: []byte("1 1 1\r\n"), Data: []byte("test")})
}
name1 := output.file.Name()
output.Close()
output2 := NewFileOutput(fmt.Sprintf("/tmp/%d_1.gz", rnd), &FileOutputConfig{FlushInterval: time.Minute, Append: true})
for i := 0; i < 1000; i++ {
output2.PluginWrite(&Message{Meta: []byte("1 1 1\r\n"), Data: []byte("test")})
}
name2 := output2.file.Name()
output2.Close()
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false)
for i := 0; i < 2000; i++ {
input.PluginRead()
}
os.Remove(name1)
os.Remove(name2)
}
type CaptureFile struct {
msgs []*Message
file *os.File
}
func NewExpectedCaptureFile(msgs []*Message, file *os.File) *CaptureFile {
ecf := new(CaptureFile)
ecf.file = file
ecf.msgs = msgs
return ecf
}
func (expectedCaptureFile *CaptureFile) TearDown() {
if expectedCaptureFile.file != nil {
os.Remove(expectedCaptureFile.file.Name())
}
}
type RequestGenerator struct {
inputs []PluginReader
emit func()
wg *sync.WaitGroup
}
func NewRequestGenerator(inputs []PluginReader, emit func(), count int) (rg *RequestGenerator) {
rg = new(RequestGenerator)
rg.inputs = inputs
rg.emit = emit
rg.wg = new(sync.WaitGroup)
rg.wg.Add(count)
return
}
func (expectedCaptureFile *CaptureFile) PayloadsEqual(other []*Message) bool {
if len(expectedCaptureFile.msgs) != len(other) {
return false
}
for i, payload := range other {
if !bytes.Equal(expectedCaptureFile.msgs[i].Meta, payload.Meta) {
return false
}
if !bytes.Equal(expectedCaptureFile.msgs[i].Data, payload.Data) {
return false
}
}
return true
}
func CreateCaptureFile(requestGenerator *RequestGenerator) *CaptureFile {
f, err := ioutil.TempFile("", "testmainconf")
if err != nil {
panic(err)
}
readPayloads := []*Message{}
output := NewTestOutput(func(msg *Message) {
readPayloads = append(readPayloads, msg)
requestGenerator.wg.Done()
})
outputFile := NewFileOutput(f.Name(), &FileOutputConfig{FlushInterval: time.Second, Append: true})
plugins := &InOutPlugins{
Inputs: requestGenerator.inputs,
Outputs: []PluginWriter{output, outputFile},
}
for _, input := range requestGenerator.inputs {
plugins.All = append(plugins.All, input)
}
plugins.All = append(plugins.All, output, outputFile)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
requestGenerator.emit()
requestGenerator.wg.Wait()
time.Sleep(100 * time.Millisecond)
emitter.Close()
return NewExpectedCaptureFile(readPayloads, f)
}
func ReadFromCaptureFile(captureFile *os.File, count int, callback writeCallback) (err error) {
wg := new(sync.WaitGroup)
input := NewFileInput(captureFile.Name(), false)
output := NewTestOutput(func(msg *Message) {
callback(msg)
wg.Done()
})
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
wg.Add(count)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
done := make(chan int, 1)
go func() {
wg.Wait()
done <- 1
}()
select {
case <-done:
break
case <-time.After(2 * time.Second):
err = errors.New("Timed out")
}
emitter.Close()
return
}
+81
View File
@@ -0,0 +1,81 @@
package main
import (
"log"
"net"
"net/http"
"net/http/httputil"
"time"
)
// HTTPInput used for sending requests to Gor via http
type HTTPInput struct {
data chan []byte
address string
listener net.Listener
stop chan bool // Channel used only to indicate goroutine should shutdown
}
// NewHTTPInput constructor for HTTPInput. Accepts address with port which it will listen on.
func NewHTTPInput(address string) (i *HTTPInput) {
i = new(HTTPInput)
i.data = make(chan []byte, 1000)
i.stop = make(chan bool)
i.listen(address)
return
}
// PluginRead reads message from this plugin
func (i *HTTPInput) PluginRead() (*Message, error) {
var msg Message
select {
case <-i.stop:
return nil, ErrorStopped
case buf := <-i.data:
msg.Data = buf
msg.Meta = payloadHeader(RequestPayload, uuid(), time.Now().UnixNano(), -1)
return &msg, nil
}
}
// Close closes this plugin
func (i *HTTPInput) Close() error {
close(i.stop)
return nil
}
func (i *HTTPInput) handler(w http.ResponseWriter, r *http.Request) {
r.URL.Scheme = "http"
r.URL.Host = i.address
buf, _ := httputil.DumpRequestOut(r, true)
http.Error(w, http.StatusText(200), 200)
i.data <- buf
}
func (i *HTTPInput) listen(address string) {
var err error
mux := http.NewServeMux()
mux.HandleFunc("/", i.handler)
i.listener, err = net.Listen("tcp", address)
if err != nil {
log.Fatal("HTTP input listener failure:", err)
}
i.address = i.listener.Addr().String()
go func() {
err = http.Serve(i.listener, mux)
if err != nil && err != http.ErrServerClosed {
log.Fatal("HTTP input serve failure ", err)
}
}()
}
func (i *HTTPInput) String() string {
return "HTTP input: " + i.address
}
+80
View File
@@ -0,0 +1,80 @@
package main
import (
"bytes"
"net/http"
"strings"
"sync"
"testing"
"time"
)
func TestHTTPInput(t *testing.T) {
wg := new(sync.WaitGroup)
input := NewHTTPInput("127.0.0.1:0")
time.Sleep(time.Millisecond)
output := NewTestOutput(func(*Message) {
wg.Done()
})
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
address := strings.Replace(input.address, "[::]", "127.0.0.1", -1)
for i := 0; i < 100; i++ {
wg.Add(1)
http.Get("http://" + address + "/")
}
wg.Wait()
emitter.Close()
}
func TestInputHTTPLargePayload(t *testing.T) {
wg := new(sync.WaitGroup)
const n = 10 << 20 // 10MB
var large [n]byte
large[n-1] = '0'
input := NewHTTPInput("127.0.0.1:0")
output := NewTestOutput(func(msg *Message) {
_len := len(msg.Data)
if _len >= n { // considering http body CRLF
t.Errorf("expected body to be >= %d", n)
}
wg.Done()
})
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
emitter := NewEmitter()
defer emitter.Close()
go emitter.Start(plugins, Settings.Middleware)
address := strings.Replace(input.address, "[::]", "127.0.0.1", -1)
var req *http.Request
var err error
req, err = http.NewRequest("POST", "http://"+address, bytes.NewBuffer(large[:]))
if err != nil {
t.Error(err)
return
}
wg.Add(1)
_, err = http.DefaultClient.Do(req)
if err != nil {
t.Error(err)
return
}
wg.Wait()
}
+120
View File
@@ -0,0 +1,120 @@
package main
import (
"encoding/json"
"log"
"strings"
"github.com/Shopify/sarama"
"github.com/Shopify/sarama/mocks"
)
// KafkaInput is used for recieving Kafka messages and
// transforming them into HTTP payloads.
type KafkaInput struct {
config *InputKafkaConfig
consumers []sarama.PartitionConsumer
messages chan *sarama.ConsumerMessage
quit chan struct{}
}
// NewKafkaInput creates instance of kafka consumer client with TLS config
func NewKafkaInput(address string, config *InputKafkaConfig, tlsConfig *KafkaTLSConfig) *KafkaInput {
c := NewKafkaConfig(tlsConfig)
var con sarama.Consumer
if mock, ok := config.consumer.(*mocks.Consumer); ok && mock != nil {
con = config.consumer
} else {
var err error
con, err = sarama.NewConsumer(strings.Split(config.Host, ","), c)
if err != nil {
log.Fatalln("Failed to start Sarama(Kafka) consumer:", err)
}
}
partitions, err := con.Partitions(config.Topic)
if err != nil {
log.Fatalln("Failed to collect Sarama(Kafka) partitions:", err)
}
i := &KafkaInput{
config: config,
consumers: make([]sarama.PartitionConsumer, len(partitions)),
messages: make(chan *sarama.ConsumerMessage, 256),
quit: make(chan struct{}),
}
for index, partition := range partitions {
consumer, err := con.ConsumePartition(config.Topic, partition, sarama.OffsetNewest)
if err != nil {
log.Fatalln("Failed to start Sarama(Kafka) partition consumer:", err)
}
go func(consumer sarama.PartitionConsumer) {
defer consumer.Close()
for message := range consumer.Messages() {
i.messages <- message
}
}(consumer)
go i.ErrorHandler(consumer)
i.consumers[index] = consumer
}
return i
}
// ErrorHandler should receive errors
func (i *KafkaInput) ErrorHandler(consumer sarama.PartitionConsumer) {
for err := range consumer.Errors() {
Debug(1, "Failed to read access log entry:", err)
}
}
// PluginRead a reads message from this plugin
func (i *KafkaInput) PluginRead() (*Message, error) {
var message *sarama.ConsumerMessage
var msg Message
select {
case <-i.quit:
return nil, ErrorStopped
case message = <-i.messages:
}
msg.Data = message.Value
if i.config.UseJSON {
var kafkaMessage KafkaMessage
json.Unmarshal(message.Value, &kafkaMessage)
var err error
msg.Data, err = kafkaMessage.Dump()
if err != nil {
Debug(1, "[INPUT-KAFKA] failed to decode access log entry:", err)
return nil, err
}
}
// does it have meta
if isOriginPayload(msg.Data) {
msg.Meta, msg.Data = payloadMetaWithBody(msg.Data)
}
return &msg, nil
}
func (i *KafkaInput) String() string {
return "Kafka Input: " + i.config.Host + "/" + i.config.Topic
}
// Close closes this plugin
func (i *KafkaInput) Close() error {
close(i.quit)
return nil
}
+60
View File
@@ -0,0 +1,60 @@
package main
import (
"testing"
"github.com/Shopify/sarama"
"github.com/Shopify/sarama/mocks"
)
func TestInputKafkaRAW(t *testing.T) {
consumer := mocks.NewConsumer(t, nil)
defer consumer.Close()
consumer.ExpectConsumePartition("test", 0, mocks.AnyOffset).YieldMessage(&sarama.ConsumerMessage{Value: []byte("1 2 3\nGET / HTTP1.1\r\nHeader: 1\r\n\r\n")})
consumer.SetTopicMetadata(
map[string][]int32{"test": {0}},
)
input := NewKafkaInput("", &InputKafkaConfig{
consumer: consumer,
Topic: "test",
UseJSON: false,
}, nil)
msg, err := input.PluginRead()
if err != nil {
t.Fatal(err)
}
if string(append(msg.Meta, msg.Data...)) != "1 2 3\nGET / HTTP1.1\r\nHeader: 1\r\n\r\n" {
t.Error("Message not properly decoded")
}
}
func TestInputKafkaJSON(t *testing.T) {
consumer := mocks.NewConsumer(t, nil)
defer consumer.Close()
consumer.ExpectConsumePartition("test", 0, mocks.AnyOffset).YieldMessage(&sarama.ConsumerMessage{Value: []byte(`{"Req_URL":"/","Req_Type":"1","Req_ID":"2","Req_Ts":"3","Req_Method":"GET","Req_Headers":{"Header":"1"}}`)})
consumer.SetTopicMetadata(
map[string][]int32{"test": {0}},
)
input := NewKafkaInput("", &InputKafkaConfig{
consumer: consumer,
Topic: "test",
UseJSON: true,
}, nil)
msg, err := input.PluginRead()
if err != nil {
t.Fatal(err)
}
if string(append(msg.Meta, msg.Data...)) != "1 2 3\nGET / HTTP/1.1\r\nHeader: 1\r\n\r\n" {
t.Error("Message not properly decoded")
}
}
+204 -28
View File
@@ -1,53 +1,229 @@
package main
import (
raw "github.com/buger/gor/raw_socket_listener"
"context"
"fmt"
"log"
"net"
"strings"
"strconv"
"sync"
"time"
"github.com/buger/goreplay/capture"
"github.com/buger/goreplay/proto"
"github.com/buger/goreplay/size"
"github.com/buger/goreplay/tcp"
)
type RAWInput struct {
data chan []byte
address string
// TCPProtocol is a number to indicate type of protocol
type TCPProtocol uint8
const (
// ProtocolHTTP ...
ProtocolHTTP TCPProtocol = iota
// ProtocolBinary ...
ProtocolBinary
)
// Set is here so that TCPProtocol can implement flag.Var
func (protocol *TCPProtocol) Set(v string) error {
switch v {
case "", "http":
*protocol = ProtocolHTTP
case "binary":
*protocol = ProtocolBinary
default:
return fmt.Errorf("unsupported protocol %s", v)
}
return nil
}
func NewRAWInput(address string) (i *RAWInput) {
i = new(RAWInput)
i.data = make(chan []byte)
i.address = address
func (protocol *TCPProtocol) String() string {
switch *protocol {
case ProtocolBinary:
return "binary"
case ProtocolHTTP:
return "http"
default:
return ""
}
}
go i.listen(address)
// RAWInputConfig represents configuration that can be applied on raw input
type RAWInputConfig struct {
capture.PcapOptions
Expire time.Duration `json:"input-raw-expire"`
CopyBufferSize size.Size `json:"copy-buffer-size"`
Engine capture.EngineType `json:"input-raw-engine"`
TrackResponse bool `json:"input-raw-track-response"`
Protocol TCPProtocol `json:"input-raw-protocol"`
RealIPHeader string `json:"input-raw-realip-header"`
Stats bool `json:"input-raw-stats"`
quit chan bool // Channel used only to indicate goroutine should shutdown
host string
port uint16
}
// RAWInput used for intercepting traffic for given address
type RAWInput struct {
sync.Mutex
RAWInputConfig
messageStats []tcp.Stats
listener *capture.Listener
message chan *tcp.Message
cancelListener context.CancelFunc
closed bool
}
// NewRAWInput constructor for RAWInput. Accepts raw input config as arguments.
func NewRAWInput(address string, config RAWInputConfig) (i *RAWInput) {
i = new(RAWInput)
i.RAWInputConfig = config
i.message = make(chan *tcp.Message, 10000)
i.quit = make(chan bool)
var host, _port string
var err error
var port int
host, _port, err = net.SplitHostPort(address)
if err != nil {
log.Fatalf("input-raw: error while parsing address: %s", err)
}
if _port != "" {
port, err = strconv.Atoi(_port)
}
if err != nil {
log.Fatalf("parsing port error: %v", err)
}
i.host = host
i.port = uint16(port)
i.listen(address)
return
}
func (i *RAWInput) Read(data []byte) (int, error) {
buf := <-i.data
copy(data, buf)
// PluginRead reads meassage from this plugin
func (i *RAWInput) PluginRead() (*Message, error) {
var msgTCP *tcp.Message
var msg Message
select {
case <-i.quit:
return nil, ErrorStopped
case msgTCP = <-i.message:
msg.Data = msgTCP.Data()
}
var msgType byte = ResponsePayload
if msgTCP.IsRequest {
msgType = RequestPayload
if i.RealIPHeader != "" {
msg.Data = proto.SetHeader(msg.Data, []byte(i.RealIPHeader), []byte(msgTCP.SrcAddr))
}
}
msg.Meta = payloadHeader(msgType, msgTCP.UUID(), msgTCP.Start.UnixNano(), msgTCP.End.UnixNano()-msgTCP.Start.UnixNano())
return len(buf), nil
// to be removed....
if msgTCP.Truncated {
Debug(2, "[INPUT-RAW] message truncated, increase copy-buffer-size")
}
// to be removed...
if msgTCP.TimedOut {
Debug(2, "[INPUT-RAW] message timeout reached, increase input-raw-expire")
}
if i.Stats {
stat := msgTCP.Stats
go i.addStats(stat)
}
msgTCP.Finalize()
msgTCP = nil
return &msg, nil
}
func (i *RAWInput) listen(address string) {
address = strings.Replace(address, "[::]", "127.0.0.1", -1)
host, port, err := net.SplitHostPort(address)
var err error
i.listener, err = capture.NewListener(i.host, i.port, "", i.Engine, i.TrackResponse, Settings.maxAllowedMem)
if err != nil {
log.Fatal("input-raw: error while parsing address", err)
log.Fatal(err)
}
listener := raw.NewListener(host, port)
for {
// Receiving TCPMessage object
m := listener.Receive()
i.data <- m.Bytes()
i.listener.SetPcapOptions(i.PcapOptions)
err = i.listener.Activate()
if err != nil {
log.Fatal(err)
}
parser := tcp.NewMessageParser(i.CopyBufferSize, i.Expire, Debug, i.messageEmitter)
if i.Protocol == ProtocolHTTP {
parser.Start = http1StartHint
parser.End = http1EndHint
}
var ctx context.Context
ctx, i.cancelListener = context.WithCancel(context.Background())
errCh := i.listener.ListenBackground(ctx, parser.PacketHandler)
<-i.listener.Reading
Debug(1, i)
go func() {
<-errCh // the listener closed voluntarily
i.Close()
}()
}
func (i *RAWInput) messageEmitter(m *tcp.Message) {
i.message <- m
}
func (i *RAWInput) String() string {
return "RAW Socket input: " + i.address
return fmt.Sprintf("Intercepting traffic from: %s:%d", i.host, i.port)
}
// GetStats returns the stats so far and reset the stats
func (i *RAWInput) GetStats() []tcp.Stats {
i.Lock()
defer func() {
i.messageStats = []tcp.Stats{}
i.Unlock()
}()
return i.messageStats
}
// Close closes the input raw listener
func (i *RAWInput) Close() error {
i.Lock()
defer i.Unlock()
if i.closed {
return nil
}
i.cancelListener()
close(i.quit)
i.closed = true
return nil
}
func (i *RAWInput) addStats(mStats tcp.Stats) {
i.Lock()
if len(i.messageStats) >= 10000 {
i.messageStats = []tcp.Stats{}
}
i.messageStats = append(i.messageStats, mStats)
i.Unlock()
}
func http1StartHint(pckt *tcp.Packet) (isRequest, isResponse bool) {
if proto.HasRequestTitle(pckt.Payload) {
return true, false
}
if proto.HasResponseTitle(pckt.Payload) {
return false, true
}
// No request or response detected
return false, false
}
func http1EndHint(m *tcp.Message) bool {
if m.MissingChunk() {
return false
}
return proto.HasFullPayload(m, m.PacketData()...)
}
+326 -15
View File
@@ -1,39 +1,350 @@
package main
import (
"io"
"bytes"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"os/exec"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/buger/goreplay/capture"
"github.com/buger/goreplay/proto"
)
func TestRAWInput(t *testing.T) {
const testRawExpire = time.Millisecond * 200
func TestRAWInputIPv4(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
listener := startHTTP(func(req *http.Request) {})
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Error(err)
return
}
origin := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ab"))
}),
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
go origin.Serve(listener)
defer listener.Close()
_, port, _ := net.SplitHostPort(listener.Addr().String())
input := NewRAWInput(listener.Addr().String())
output := NewTestOutput(func(data []byte) {
var respCounter, reqCounter int64
conf := RAWInputConfig{
Engine: capture.EnginePcap,
Expire: 0,
Protocol: ProtocolHTTP,
TrackResponse: true,
RealIPHeader: "X-Real-IP",
}
input := NewRAWInput(listener.Addr().String(), conf)
output := NewTestOutput(func(msg *Message) {
if msg.Meta[0] == '1' {
if len(proto.Header(msg.Data, []byte("X-Real-IP"))) == 0 {
t.Error("Should have X-Real-IP header")
}
reqCounter++
} else {
respCounter++
}
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
address := strings.Replace(listener.Addr().String(), "[::]", "127.0.0.1", -1)
addr := "http://127.0.0.1:" + port
emitter := NewEmitter()
defer emitter.Close()
go emitter.Start(plugins, Settings.Middleware)
for i := 0; i < 1; i++ {
wg.Add(2)
_, err = http.Get(addr)
if err != nil {
t.Error(err)
return
}
}
wg.Wait()
const want = 10
if reqCounter != respCounter && reqCounter != want {
t.Errorf("want %d requests and %d responses, got %d requests and %d responses", want, want, reqCounter, respCounter)
}
}
go Start(quit)
func TestRAWInputNoKeepAlive(t *testing.T) {
wg := new(sync.WaitGroup)
for i := 0; i < 100; i++ {
wg.Add(1)
res, _ := http.Get("http://" + address)
res.Body.Close()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
origin := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ab"))
}),
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
origin.SetKeepAlivesEnabled(false)
go origin.Serve(listener)
defer listener.Close()
_, port, _ := net.SplitHostPort(listener.Addr().String())
conf := RAWInputConfig{
Engine: capture.EnginePcap,
Expire: testRawExpire,
Protocol: ProtocolHTTP,
TrackResponse: true,
}
input := NewRAWInput(":"+port, conf)
var respCounter, reqCounter int64
output := NewTestOutput(func(msg *Message) {
if msg.Meta[0] == '1' {
atomic.AddInt64(&reqCounter, 1)
} else {
atomic.AddInt64(&respCounter, 1)
}
wg.Done()
})
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
addr := "http://127.0.0.1:" + port
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
for i := 0; i < 10; i++ {
// request + response
wg.Add(2)
_, err = http.Get(addr)
if err != nil {
t.Error(err)
return
}
}
wg.Wait()
const want = 10
if reqCounter != respCounter && reqCounter != want {
t.Errorf("want %d requests and %d responses, got %d requests and %d responses", want, want, reqCounter, respCounter)
}
emitter.Close()
}
close(quit)
func TestRAWInputIPv6(t *testing.T) {
wg := new(sync.WaitGroup)
listener, err := net.Listen("tcp", "[::1]:0")
if err != nil {
return
}
origin := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ab"))
}),
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
go origin.Serve(listener)
defer listener.Close()
_, port, _ := net.SplitHostPort(listener.Addr().String())
originAddr := "[::1]:" + port
var respCounter, reqCounter int64
conf := RAWInputConfig{
Engine: capture.EnginePcap,
Protocol: ProtocolHTTP,
TrackResponse: true,
}
input := NewRAWInput(originAddr, conf)
output := NewTestOutput(func(msg *Message) {
if msg.Meta[0] == '1' {
atomic.AddInt64(&reqCounter, 1)
} else {
atomic.AddInt64(&respCounter, 1)
}
wg.Done()
})
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
emitter := NewEmitter()
addr := "http://" + originAddr
go emitter.Start(plugins, Settings.Middleware)
for i := 0; i < 10; i++ {
// request + response
wg.Add(2)
_, err = http.Get(addr)
if err != nil {
t.Error(err)
return
}
}
wg.Wait()
const want = 10
if reqCounter != respCounter && reqCounter != want {
t.Errorf("want %d requests and %d responses, got %d requests and %d responses", want, want, reqCounter, respCounter)
}
emitter.Close()
}
func TestInputRAWChunkedEncoding(t *testing.T) {
wg := new(sync.WaitGroup)
fileContent, _ := ioutil.ReadFile("README.md")
// Origing and Replay server initialization
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
ioutil.ReadAll(r.Body)
wg.Done()
}))
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
conf := RAWInputConfig{
Engine: capture.EnginePcap,
Expire: time.Second,
Protocol: ProtocolHTTP,
TrackResponse: true,
}
input := NewRAWInput(originAddr, conf)
replay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, _ := ioutil.ReadAll(r.Body)
if !bytes.Equal(body, fileContent) {
buf, _ := httputil.DumpRequest(r, true)
t.Error("Wrong POST body:", string(buf))
}
wg.Done()
}))
defer replay.Close()
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{})
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{httpOutput},
}
plugins.All = append(plugins.All, input, httpOutput)
emitter := NewEmitter()
defer emitter.Close()
go emitter.Start(plugins, Settings.Middleware)
wg.Add(2)
curl := exec.Command("curl", "http://"+originAddr, "--header", "Transfer-Encoding: chunked", "--header", "Expect:", "--data-binary", "@README.md")
err := curl.Run()
if err != nil {
t.Error(err)
return
}
wg.Wait()
}
func BenchmarkRAWInputWithReplay(b *testing.B) {
var respCounter, reqCounter, replayCounter uint32
wg := &sync.WaitGroup{}
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
b.Error(err)
return
}
listener0, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
b.Error(err)
return
}
origin := http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ab"))
}),
}
go origin.Serve(listener)
defer origin.Close()
originAddr := listener.Addr().String()
replay := http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddUint32(&replayCounter, 1)
w.Write(nil)
wg.Done()
}),
}
go replay.Serve(listener0)
defer replay.Close()
replayAddr := listener0.Addr().String()
conf := RAWInputConfig{
Engine: capture.EnginePcap,
Expire: testRawExpire,
Protocol: ProtocolHTTP,
TrackResponse: true,
}
input := NewRAWInput(originAddr, conf)
testOutput := NewTestOutput(func(msg *Message) {
if msg.Meta[0] == '1' {
reqCounter++
} else {
respCounter++
}
wg.Done()
})
httpOutput := NewHTTPOutput("http://"+replayAddr, &HTTPOutputConfig{})
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{testOutput, httpOutput},
}
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
addr := "http://" + originAddr
b.ResetTimer()
for i := 0; i < b.N; i++ {
wg.Add(3) // reqCounter + replayCounter + respCounter
resp, err := http.Get(addr)
if err != nil {
wg.Add(-3)
}
resp.Body.Close()
}
wg.Wait()
b.ReportMetric(float64(reqCounter), "requests")
b.ReportMetric(float64(respCounter), "responses")
b.ReportMetric(float64(replayCounter), "replayed")
emitter.Close()
}
+94 -35
View File
@@ -2,79 +2,128 @@ package main
import (
"bufio"
"bytes"
"crypto/tls"
"fmt"
"io"
"log"
"net"
)
// Can be tested using nc tool:
// echo "asdad" | nc 127.0.0.1 27017
//
// TCPInput used for internal communication
type TCPInput struct {
data chan []byte
address string
data chan *Message
listener net.Listener
address string
config *TCPInputConfig
stop chan bool // Channel used only to indicate goroutine should shutdown
}
func NewTCPInput(address string) (i *TCPInput) {
// TCPInputConfig represents configuration of a TCP input plugin
type TCPInputConfig struct {
Secure bool `json:"input-tcp-secure"`
CertificatePath string `json:"input-tcp-certificate"`
KeyPath string `json:"input-tcp-certificate-key"`
}
// NewTCPInput constructor for TCPInput, accepts address with port
func NewTCPInput(address string, config *TCPInputConfig) (i *TCPInput) {
i = new(TCPInput)
i.data = make(chan []byte)
i.data = make(chan *Message, 1000)
i.address = address
i.config = config
i.stop = make(chan bool)
i.listen(address)
return
}
func (i *TCPInput) Read(data []byte) (int, error) {
buf := <-i.data
copy(data, buf)
// PluginRead returns data and details read from plugin
func (i *TCPInput) PluginRead() (msg *Message, err error) {
select {
case <-i.stop:
return nil, ErrorStopped
case msg = <-i.data:
return msg, nil
}
return len(buf), nil
}
// Close closes the plugin
func (i *TCPInput) Close() error {
close(i.stop)
i.listener.Close()
return nil
}
func (i *TCPInput) listen(address string) {
listener, err := net.Listen("tcp", address)
i.listener = listener
if i.config.Secure {
cer, err := tls.LoadX509KeyPair(i.config.CertificatePath, i.config.KeyPath)
if err != nil {
log.Fatalln("error while loading --input-tcp TLS certificate:", err)
}
if err != nil {
log.Fatal("Can't start:", err)
config := &tls.Config{Certificates: []tls.Certificate{cer}}
listener, err := tls.Listen("tcp", address, config)
if err != nil {
log.Fatalln("[INPUT-TCP] failed to start INPUT-TCP listener:", err)
}
i.listener = listener
} else {
listener, err := net.Listen("tcp", address)
if err != nil {
log.Fatalln("failed to start INPUT-TCP listener:", err)
}
i.listener = listener
}
go func() {
for {
conn, err := listener.Accept()
if err != nil {
log.Println("Error while Accept()", err)
conn, err := i.listener.Accept()
if err == nil {
go i.handleConnection(conn)
continue
}
go i.handleConnection(conn)
if isTemporaryNetworkError(err) {
continue
}
if operr, ok := err.(*net.OpError); ok && operr.Err.Error() != "use of closed network connection" {
Debug(0, fmt.Sprintf("[INPUT-TCP] listener closed, err: %q", err))
}
break
}
}()
}
var payloadSeparatorAsBytes = []byte(payloadSeparator)
func (i *TCPInput) handleConnection(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
var buffer bytes.Buffer
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)
}
}
line, err := reader.ReadBytes('\n')
if err != nil {
if isTemporaryNetworkError(err) {
continue
}
if err != io.EOF {
Debug(0, fmt.Sprintf("[INPUT-TCP] connection error: %q", err))
}
break
}
if bytes.Equal(payloadSeparatorAsBytes[1:], line) {
// unread the '\n' before monkeys
buffer.UnreadByte()
var msg Message
msg.Meta, msg.Data = payloadMetaWithBody(buffer.Bytes())
i.data <- &msg
buffer.Reset()
} else {
buffer.Write(line)
}
}
}
@@ -82,3 +131,13 @@ func (i *TCPInput) handleConnection(conn net.Conn) {
func (i *TCPInput) String() string {
return "TCP input: " + i.address
}
func isTemporaryNetworkError(err error) bool {
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
return true
}
if operr, ok := err.(*net.OpError); ok && operr.Temporary() {
return true
}
return false
}
+91 -61
View File
@@ -1,26 +1,38 @@
package main
import (
"io"
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"log"
"math/big"
"net"
"os"
"sync"
"testing"
"time"
)
func TestTCPInput(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTCPInput(":0")
output := NewTestOutput(func(data []byte) {
input := NewTCPInput("127.0.0.1:0", &TCPInputConfig{})
output := NewTestOutput(func(*Message) {
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
go Start(quit)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
tcpAddr, err := net.ResolveTCPAddr("tcp", input.listener.Addr().String())
@@ -29,84 +41,102 @@ func TestTCPInput(t *testing.T) {
}
conn, err := net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
log.Fatal(err)
}
msg := []byte("GET / HTTP/1.1\r\n\r\n")
msg := []byte("1 1 1\nGET / HTTP/1.1\r\n\r\n")
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)
if _, err = conn.Write(msg); err == nil {
_, err = conn.Write(payloadSeparatorAsBytes)
}
if err != nil {
t.Error(err)
return
}
}
wg.Wait()
close(quit)
emitter.Close()
}
func BenchmarkTCPInput(b *testing.B) {
wg := new(sync.WaitGroup)
quit := make(chan int)
func genCertificate(template *x509.Certificate) ([]byte, []byte) {
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
input := NewTCPInput(":0")
output := NewTestOutput(func(data []byte) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit)
template.SerialNumber = serialNumber
template.BasicConstraintsValid = true
template.NotBefore = time.Now()
template.NotAfter = time.Now().Add(time.Hour)
derBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
var certPem, keyPem bytes.Buffer
pem.Encode(&certPem, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
pem.Encode(&keyPem, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
return certPem.Bytes(), keyPem.Bytes()
}
func TestTCPInputSecure(t *testing.T) {
serverCertPem, serverPrivPem := genCertificate(&x509.Certificate{
DNSNames: []string{"localhost"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::")},
})
serverCertPemFile, _ := ioutil.TempFile("", "server.crt")
serverCertPemFile.Write(serverCertPem)
serverCertPemFile.Close()
serverPrivPemFile, _ := ioutil.TempFile("", "server.key")
serverPrivPemFile.Write(serverPrivPem)
serverPrivPemFile.Close()
defer func() {
os.Remove(serverPrivPemFile.Name())
os.Remove(serverCertPemFile.Name())
}()
wg := new(sync.WaitGroup)
input := NewTCPInput("127.0.0.1:0", &TCPInputConfig{
Secure: true,
CertificatePath: serverCertPemFile.Name(),
KeyPath: serverPrivPemFile.Name(),
})
output := NewTestOutput(func(*Message) {
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
go Start(quit)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
tcpAddr, err := net.ResolveTCPAddr("tcp", input.listener.Addr().String())
conf := &tls.Config{
InsecureSkipVerify: true,
}
conn, err := tls.Dial("tcp", input.listener.Addr().String(), conf)
if err != nil {
log.Fatal(err)
t.Fatal(err)
}
defer conn.Close()
var connections []net.Conn
msg := []byte("1 1 1\nGET / HTTP/1.1\r\n\r\n")
// Creating simple pool of workers, same as output_tcp have
dataChan := make(chan []byte, 1000)
for i := 0; i < 10; i++ {
conn, _ := net.DialTCP("tcp", nil, tcpAddr)
connections = append(connections, conn)
go func(conn net.Conn) {
for {
data := <-dataChan
new_buf := make([]byte, len(data) + 2)
data = append(data,[]byte("¶")...)
copy(new_buf, data)
conn.Write(new_buf)
}
}(conn)
}
if err != nil {
log.Fatal(err)
}
msg := []byte("GET / HTTP/1.1\r\n\r\n")
b.ResetTimer()
for i := 0; i < b.N; i++ {
for i := 0; i < 100; i++ {
wg.Add(1)
dataChan <- msg
conn.Write(msg)
conn.Write([]byte(payloadSeparator))
}
wg.Wait()
for _, conn := range connections {
conn.Close()
}
close(quit)
emitter.Close()
}
+118
View File
@@ -0,0 +1,118 @@
package main
import (
"bytes"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"log"
"github.com/Shopify/sarama"
"github.com/buger/goreplay/proto"
)
// InputKafkaConfig should contains required information to
// build producers.
type InputKafkaConfig struct {
producer sarama.AsyncProducer
consumer sarama.Consumer
Host string `json:"input-kafka-host"`
Topic string `json:"input-kafka-topic"`
UseJSON bool `json:"input-kafka-json-format"`
}
// OutputKafkaConfig is the representation of kfka output configuration
type OutputKafkaConfig struct {
producer sarama.AsyncProducer
consumer sarama.Consumer
Host string `json:"output-kafka-host"`
Topic string `json:"output-kafka-topic"`
UseJSON bool `json:"output-kafka-json-format"`
}
// KafkaTLSConfig should contains TLS certificates for connecting to secured Kafka clusters
type KafkaTLSConfig struct {
CACert string `json:"kafka-tls-ca-cert"`
ClientCert string `json:"kafka-tls-client-cert"`
ClientKey string `json:"kafka-tls-client-key"`
}
// KafkaMessage should contains catched request information that should be
// passed as Json to Apache Kafka.
type KafkaMessage struct {
ReqURL string `json:"Req_URL"`
ReqType string `json:"Req_Type"`
ReqID string `json:"Req_ID"`
ReqTs string `json:"Req_Ts"`
ReqMethod string `json:"Req_Method"`
ReqBody string `json:"Req_Body,omitempty"`
ReqHeaders map[string]string `json:"Req_Headers,omitempty"`
}
// NewTLSConfig loads TLS certificates
func NewTLSConfig(clientCertFile, clientKeyFile, caCertFile string) (*tls.Config, error) {
tlsConfig := tls.Config{}
if clientCertFile != "" && clientKeyFile == "" {
return &tlsConfig, errors.New("Missing key of client certificate in kafka")
}
if clientCertFile == "" && clientKeyFile != "" {
return &tlsConfig, errors.New("missing TLS client certificate in kafka")
}
// Load client cert
if (clientCertFile != "") && (clientKeyFile != "") {
cert, err := tls.LoadX509KeyPair(clientCertFile, clientKeyFile)
if err != nil {
return &tlsConfig, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
// Load CA cert
if caCertFile != "" {
caCert, err := ioutil.ReadFile(caCertFile)
if err != nil {
return &tlsConfig, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig.RootCAs = caCertPool
}
return &tlsConfig, nil
}
// NewKafkaConfig returns Kafka config with or without TLS
func NewKafkaConfig(tlsConfig *KafkaTLSConfig) *sarama.Config {
config := sarama.NewConfig()
// Configuration options go here
if tlsConfig != nil && (tlsConfig.ClientCert != "" || tlsConfig.CACert != "") {
config.Net.TLS.Enable = true
tlsConfig, err := NewTLSConfig(tlsConfig.ClientCert, tlsConfig.ClientKey, tlsConfig.CACert)
if err != nil {
log.Fatal(err)
}
config.Net.TLS.Config = tlsConfig
}
return config
}
// Dump returns the given request in its HTTP/1.x wire
// representation.
func (m KafkaMessage) Dump() ([]byte, error) {
var b bytes.Buffer
b.WriteString(fmt.Sprintf("%s %s %s\n", m.ReqType, m.ReqID, m.ReqTs))
b.WriteString(fmt.Sprintf("%s %s HTTP/1.1", m.ReqMethod, m.ReqURL))
b.Write(proto.CRLF)
for key, value := range m.ReqHeaders {
b.WriteString(fmt.Sprintf("%s: %s", key, value))
b.Write(proto.CRLF)
}
b.Write(proto.CRLF)
b.WriteString(m.ReqBody)
return b.Bytes(), nil
}
+79 -12
View File
@@ -3,43 +3,110 @@ package main
import (
"fmt"
"io"
"math/rand"
"strconv"
"strings"
"time"
)
// Limiter is a wrapper for input or output plugin which adds rate limiting
type Limiter struct {
writer io.Writer
limit int
plugin interface{}
limit int
isPercent bool
currentRPS int
currentTime int64
}
func NewLimiter(writer io.Writer, limit int) (l *Limiter) {
l = new(Limiter)
l.limit = limit
l.writer = writer
l.currentTime = time.Now().UnixNano()
func parseLimitOptions(options string) (limit int, isPercent bool) {
if n := strings.Index(options, "%"); n > 0 {
limit, _ = strconv.Atoi(options[:n])
isPercent = true
} else {
limit, _ = strconv.Atoi(options)
isPercent = false
}
return
}
func (l *Limiter) Write(data []byte) (n int, err error) {
// NewLimiter constructor for Limiter, accepts plugin and options
// `options` allow to sprcify relatve or absolute limiting
func NewLimiter(plugin interface{}, options string) PluginReadWriter {
l := new(Limiter)
l.limit, l.isPercent = parseLimitOptions(options)
l.plugin = plugin
l.currentTime = time.Now().UnixNano()
// FileInput have its own rate limiting. Unlike other inputs we not just dropping requests, we can slow down or speed up request emittion.
if fi, ok := l.plugin.(*FileInput); ok && l.isPercent {
fi.speedFactor = float64(l.limit) / float64(100)
}
return l
}
func (l *Limiter) isLimited() bool {
// File input have its own limiting algorithm
if _, ok := l.plugin.(*FileInput); ok && l.isPercent {
return false
}
if l.isPercent {
return l.limit <= rand.Intn(100)
}
if (time.Now().UnixNano() - l.currentTime) > time.Second.Nanoseconds() {
l.currentTime = time.Now().UnixNano()
l.currentRPS = 0
}
if l.currentRPS >= l.limit {
return 0, nil
return true
}
n, err = l.writer.Write(data)
l.currentRPS++
return false
}
// PluginWrite writes message to this plugin
func (l *Limiter) PluginWrite(msg *Message) (n int, err error) {
if l.isLimited() {
return 0, nil
}
if w, ok := l.plugin.(PluginWriter); ok {
return w.PluginWrite(msg)
}
// avoid further writing
return 0, io.ErrClosedPipe
}
// PluginRead reads message from this plugin
func (l *Limiter) PluginRead() (msg *Message, err error) {
if r, ok := l.plugin.(PluginReader); ok {
msg, err = r.PluginRead()
} else {
// avoid further reading
return nil, io.ErrClosedPipe
}
if l.isLimited() {
return nil, nil
}
return
}
func (l *Limiter) String() string {
return fmt.Sprintf("Limiting %s to: %d", l.writer, l.limit)
return fmt.Sprintf("Limiting %s to: %d (isPercent: %v)", l.plugin, l.limit, l.isPercent)
}
// Close closes the resources.
func (l *Limiter) Close() error {
if fi, ok := l.plugin.(io.Closer); ok {
fi.Close()
}
return nil
}
+90 -10
View File
@@ -1,31 +1,111 @@
// +build !race
package main
import (
"io"
"sync"
"testing"
)
func TestLimiter(t *testing.T) {
func TestOutputLimiter(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
output := NewLimiter(NewTestOutput(func(data []byte) {
output := NewLimiter(NewTestOutput(func(*Message) {
wg.Done()
}), 10)
}), "10")
wg.Add(10)
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
go Start(quit)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
for i := 0; i < 100; i++ {
input.EmitGET()
}
wg.Wait()
emitter.Close()
}
func TestInputLimiter(t *testing.T) {
wg := new(sync.WaitGroup)
input := NewLimiter(NewTestInput(), "10")
output := NewTestOutput(func(*Message) {
wg.Done()
})
wg.Add(10)
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
for i := 0; i < 100; i++ {
input.(*Limiter).plugin.(*TestInput).EmitGET()
}
wg.Wait()
emitter.Close()
}
// Should limit all requests
func TestPercentLimiter1(t *testing.T) {
wg := new(sync.WaitGroup)
input := NewTestInput()
output := NewLimiter(NewTestOutput(func(*Message) {
wg.Done()
}), "0%")
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
for i := 0; i < 100; i++ {
input.EmitGET()
}
wg.Wait()
}
// Should not limit at all
func TestPercentLimiter2(t *testing.T) {
wg := new(sync.WaitGroup)
input := NewTestInput()
output := NewLimiter(NewTestOutput(func(*Message) {
wg.Done()
}), "100%")
wg.Add(100)
plugins := &InOutPlugins{
Inputs: []PluginReader{input},
Outputs: []PluginWriter{output},
}
plugins.All = append(plugins.All, input, output)
emitter := NewEmitter()
go emitter.Start(plugins, Settings.Middleware)
for i := 0; i < 100; i++ {
input.EmitGET()
}
wg.Wait()
close(quit)
}
+166
View File
@@ -0,0 +1,166 @@
package main
import (
"bufio"
"context"
"encoding/hex"
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"syscall"
)
// Middleware represents a middleware object
type Middleware struct {
command string
data chan *Message
Stdin io.Writer
Stdout io.Reader
commandCancel context.CancelFunc
stop chan bool // Channel used only to indicate goroutine should shutdown
closed bool
mu sync.RWMutex
}
// NewMiddleware returns new middleware
func NewMiddleware(command string) *Middleware {
m := new(Middleware)
m.command = command
m.data = make(chan *Message, 1000)
m.stop = make(chan bool)
commands := strings.Split(command, " ")
ctx, cancl := context.WithCancel(context.Background())
m.commandCancel = cancl
cmd := exec.CommandContext(ctx, commands[0], commands[1:]...)
m.Stdout, _ = cmd.StdoutPipe()
m.Stdin, _ = cmd.StdinPipe()
cmd.Stderr = os.Stderr
go m.read(m.Stdout)
go func() {
defer m.Close()
var err error
if err = cmd.Start(); err == nil {
err = cmd.Wait()
}
if err != nil {
if e, ok := err.(*exec.ExitError); ok {
status := e.Sys().(syscall.WaitStatus)
if status.Signal() == syscall.SIGKILL /*killed or context canceld */ {
return
}
}
Debug(0, fmt.Sprintf("[MIDDLEWARE] command[%q] error: %q", command, err.Error()))
}
}()
return m
}
// ReadFrom start a worker to read from this plugin
func (m *Middleware) ReadFrom(plugin PluginReader) {
Debug(2, fmt.Sprintf("[MIDDLEWARE] command[%q] Starting reading from %q", m.command, plugin))
go m.copy(m.Stdin, plugin)
}
func (m *Middleware) copy(to io.Writer, from PluginReader) {
var buf, dst []byte
for {
msg, err := from.PluginRead()
if err != nil {
return
}
if msg == nil || len(msg.Data) == 0 {
continue
}
buf = msg.Data
if Settings.PrettifyHTTP {
buf = prettifyHTTP(msg.Data)
}
dstLen := (len(buf)+len(msg.Meta))*2 + 1
// if enough space was previously allocated use it instead
if dstLen > len(dst) {
dst = make([]byte, dstLen)
}
n := hex.Encode(dst, msg.Meta)
n += hex.Encode(dst[n:], buf)
dst[n] = '\n'
n, err = to.Write(dst[:n+1])
if err == nil {
continue
}
if m.isClosed() {
return
}
}
}
func (m *Middleware) read(from io.Reader) {
reader := bufio.NewReader(from)
var line []byte
var e error
for {
if line, e = reader.ReadBytes('\n'); e != nil {
if m.isClosed() {
return
}
continue
}
buf := make([]byte, (len(line)-1)/2)
if _, err := hex.Decode(buf, line[:len(line)-1]); err != nil {
Debug(0, fmt.Sprintf("[MIDDLEWARE] command[%q] failed to decode err: %q", m.command, err))
continue
}
var msg Message
msg.Meta, msg.Data = payloadMetaWithBody(buf)
select {
case <-m.stop:
return
case m.data <- &msg:
}
}
}
// PluginRead reads message from this plugin
func (m *Middleware) PluginRead() (msg *Message, err error) {
select {
case <-m.stop:
return nil, ErrorStopped
case msg = <-m.data:
}
return
}
func (m *Middleware) String() string {
return fmt.Sprintf("Modifying traffic using %q command", m.command)
}
func (m *Middleware) isClosed() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.closed
}
// Close closes this plugin
func (m *Middleware) Close() error {
if m.isClosed() {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
m.commandCancel()
close(m.stop)
m.closed = true
return nil
}
+122
View File
@@ -0,0 +1,122 @@
# GoReplay middleware
GoReplay support protocol for writing middleware in any language, which allows you to implement custom logic like authentification or complex rewriting and filterting. See protocol description here: https://github.com/buger/goreplay/wiki/Middleware, but the basic idea that middleware process receive hex encoded data via STDIN and emits it back via STDOUT. STDERR for loggin inside middleware. Yes, that's simple.
To simplify middleware creation we provide packages for NodeJS and Go (upcoming).
If you want to get access to original and replayed responses, do not forget adding `--output-http-track-response` and `--input-raw-track-response` options.
## NodeJS
Before starting, you should install the package via npm: `npm install goreplay_middleware`.
And initialize middleware the following way:
```javascript
var gor = require("goreplay_middleware");
// `init` will initialize STDIN listener
gor.init();
```
Basic idea is that you write callbacks which respond to `request`, `response`, `replay`, or `message` events, which contain request meta information and actuall http paylod. Depending on your needs you may compare, override or filter incoming requests and responses.
You can respond to the incoming events using `on` function, by providing callbacks:
```javascript
// valid events are `request`, `response` (original response), `replay` (replayed response), and `message` (all events)
gor.on('request', function(data) {
// `data` contains incoming message its meta information.
data
// Raw HTTP payload of `Buffer` type
// Example (hidden character for line endings shown on purpose):
// GET / HTTP/1.1\r\n
// User-Agent: Golang\r\n
// \r\n
data.http
// Meta is an array size of 4, containing:
// 1. request type - 1, 2 or 3 (which maps to `request`, `respose` and `replay`)
// 2. uuid - request unique identifier. Request responses have the same ID as their request.
// 3. timestamp of when request was made (for responses it is time of request start too)
// 4. latency - time difference between request start and finish. For `request` is zero.
data.meta
// Unique request ID. It should be same for `request`, `response` and `replay` events of the same request.
data.ID
// You should return data at the end of function, even if you not changed request, if you do not want to filter it out.
// If you just `return` nothing, request will be filtered
return data
})
```
### Mapping requests and responses
You can provide request ID as additional argument to `on` function, which allow you to map related requests and responses. Below is example of middleware which checks that original and replayed response have same HTTP status code.
```javascript
// Example of very basic way to compare if replayed traffic have no errors
gor.on("request", function(req) {
gor.on("response", req.ID, function(resp) {
gor.on("replay", req.ID, function(repl) {
if (gor.httpStatus(resp.http) != gor.httpStatus(repl.http)) {
// Note that STDERR is used for logging, and it actually will be send to `Gor` STDOUT.
// This trick is used because SDTIN and STDOUT already used for process communication.
// You can write logger that writes to files insead.
console.error(`${gor.httpPath(req.http)} STATUS NOT MATCH: 'Expected ${gor.httpStatus(resp.http)}' got '${gor.httpStatus(repl.http)}'`)
}
return repl;
})
return resp;
})
return req
})
```
This middleware includes `searchResponses` helper which is used to compare value of original response with the replayed response. If authentication system or xsrf protection returns unique tokens in headers or the response, it will be helpful to rewrite your requests based on them. Because tokens are unique, and the value contained in original and replayed responses will be different. So, you need to extract value from both responses, and rewrite requests based on those mappings.
`searchResponses` accepts request id, regexp pattern for searching the compared value (should include capture group), and callback which returns both original and replayed matched value.
Example:
```javascript
// Compare HTTP headers for response and replayed response, and map values
let tokMap = {};
gor.on("request", function(req) {
let tok = gor.httpHeader(req.http, "Auth-Token");
if (tok && tokMap[tok]) {
req.http = gor.setHttpHeader(req.http, "Auth-Token", tokMap[tok])
}
gor.searchResponses(req.ID, "X-Set-Token: (\w+)$", function(respTok, replTok) {
if (respTok && replTok) tokMap[respTok] = replTok;
})
return req;
})
```
### API documentation
Package expose following functions to process raw HTTP payloads:
* `init` - initialize middleware object, start reading from STDIN.
* `httpPath` - URL path of the request: `gor.httpPath(req.http)`
* `httpMethod` - Http method: 'GET', 'POST', etc. `gor.httpMethod(req.http)`.
* `setHttpPath` - update URL path: `req.http = gor.setHttpPath(req.http, newPath)`
* `httpPathParam` - get param from URL path: `gor.httpPathParam(req.http, queryParam)`
* `setHttpPathParam` - set URL param: `req.http = gor.setHttpPathParam(req.http, queryParam, value)`
* `httpStatus` - response status code
* `httpHeaders` - get all headers: `gor.httpHeaders(req.http)`
* `httpHeader` - get HTTP header: `gor.httpHeader(req.http, "Content-Length")`
* `setHttpHeader` - Set HTTP header, returns modified payload: `req.http = gor.setHttpHeader(req.http, "X-Replayed", "1")`
* `httpBody` - get HTTP Body: `gor.httpBody(req.http)`
* `setHttpBody` - Set HTTP Body and ensures that `Content-Length` header have proper value. Returns modified payload: `req.http = gor.setHttpBody(req.http, Buffer.from('hello!'))`.
* `httpBodyParam` - get POST body param: `gor.httpBodyParam(req.http, param)`
* `setHttpBodyParam` - set POST body param: `req.http = gor.setHttpBodyParam(req.http, param, value)`
* `httpCookie` - get HTTP cookie: `gor.httpCookie(req.http, "SESSSION_ID")`
* `setHttpCookie` - set HTTP cookie, returns modified payload: `req.http = gor.setHttpCookie(req.http, "iam", "cuckoo")`
* `deleteHttpCookie` - delete HTTP cookie, returns modified payload: `req.http = gor.deleteHttpCookie(req.http, "iam")`
Also it is totally legit to use standard `Buffer` functions like `indexOf` for processing the HTTP payload. Just do not forget that if you modify the body, update the `Content-Length` header with a new value. And if you modify any of the headers, line endings should be `\r\n`. Rest is up to your imagination.
## Support
Feel free to ask questions here and by sending email to [support@goreplay.org](mailto:support@goreplay.org). Commercial support is available and welcomed 🙈.
+790
View File
@@ -0,0 +1,790 @@
// ======= GoReplay Middleware helper =============
// Created by Leonid Bugaev in 2017
//
// For questions use GitHub or support@goreplay.org
//
// GoReplay: https://github.com/buger/goreplay
// Middleware package: https://github.com/buger/goreplay/middleware
var middleware;
function init() {
var proxy = {
ch: {},
on: function(chan, id, cb) {
if (!cb && id) {
cb = id;
} else if (cb && id) {
chan = chan + "#" + id;
}
if (!proxy.ch[chan]) {
proxy.ch[chan] = [];
}
proxy.ch[chan].push({
created: new Date(),
cb: cb
});
return proxy;
},
emit: function(msg, raw) {
var chanPrefix;
switch(msg.type) {
case "1": chanPrefix = "request"; break;
case "2": chanPrefix = "response"; break;
case "3": chanPrefix = "replay"; break;
}
let resp = msg;
["message", chanPrefix, chanPrefix + "#" + msg.ID].forEach(function(chanID, idx){
if (proxy.ch[chanID]) {
proxy.ch[chanID].forEach(function(ch){
let r = ch.cb(msg);
if (resp) resp = r; // If one of callback decided not to send response back, do not override it in global callbacks
})
// Cleanup Individual message channels to avoid memory leaks
if (idx == 2) {
delete proxy.ch[chanID]
}
}
})
if (resp) {
process.stdout.write(`${resp.rawMeta.toString('hex')}${Buffer.from("\n").toString("hex")}${resp.http.toString('hex')}\n`)
}
return resp
}
}
// Clean up old messaged ID specific channels if they are older then 60s
let gc = function(gcTime){
let now = new Date();
for (k in proxy.ch) {
if (k.indexOf("#") == -1) continue;
proxy.ch[k] = proxy.ch[k].filter(function(ch){
return (now - ch.created) < gcTime
})
if (proxy.ch[k].length == 0) {
delete proxy.ch[k]
}
}
}
proxy.gc = gc
setInterval(function(){
gc(10 * 1000)
}, 1000);
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin
});
rl.on('line', function(line) {
let msg = parseMessage(line)
if (msg) {
proxy.emit(msg, line)
}
});
middleware = proxy;
return proxy;
}
function parseMessage(msg) {
try {
let payload = Buffer.from(msg, "hex");
let metaPos = payload.indexOf("\n");
let meta = payload.slice(0, metaPos);
let metaArr = meta.toString("ascii").split(" ");
let pType = metaArr[0];
let pID = metaArr[1];
let raw = payload.slice(metaPos + 1, payload.length);
return {
type: pType,
ID: pID,
rawMeta: meta,
meta: metaArr,
http: raw
}
} catch(e) {
fail(`Error while parsing incoming request: ${msg}`)
}
}
// Used to compare values from original and replayed responses
// Accepts request id, regexp pattern for searching the compared value (should include capture group), and callback which returns both original and replayed matched value.
// Example:
//
// // Compare HTTP headers for response and replayed response, and map values
// let tokMap = {};
//
// gor.on("request", function(req) {
// let tok = gor.httpHeader(req.http, "Auth-Token");
// if (tok && tokMap[tok]) {
// req.http = gor.setHttpHeader(req.http, "Auth-Token", tokMap[tok])
// }
//
// gor.searchResponses(req.ID, "X-Set-Token: (\w+)$", function(respTok, replTok) {
// tokMap[respTok] = replTok;
// })
//
// return req;
// })
//
function searchResponses(id, searchPattern, callback) {
let re = new RegExp(searchPattern);
// Using regexp require converting buffer to string
// Before converting to string we can use initial `Buffer.indexOf` check
let indexPattern = searchPattern.split("(")[0];
if (!indexPattern) {
console.error("Search regexp should include capture group, pointing to the value: `prefix-(.*)`")
return
}
middleware.on("response", id, function(resp){
if (resp.http.indexOf(indexPattern) == -1) {
callback()
return resp
}
let respMatch = resp.http.toString('utf-8').match(re);
if (!respMatch) {
callback()
return resp
}
middleware.on("replay", id, function(repl) {
if (repl.http.indexOf(indexPattern) == -1) {
callback(respMatch[1]);
return repl;
}
let replMatch = repl.http.toString('utf-8').match(re);
if (!replMatch) {
callback(respMatch[1]);
return repl;
}
callback(respMatch[1], replMatch[1]);
return repl;
})
return resp;
})
}
// =========== HTTP parsing =================
// Example HTTP payload record (including hidden characters):
//
// POST / HTTP/1.1\r\n
// User-Agent: Node\r\n
// Content-Length: 5\r\n
// \r\n
// hello
function httpMethod(payload) {
var pEnd = payload.indexOf(' ');
return payload.slice(0, pEnd).toString("ascii");
}
function httpPath(payload) {
var pStart = payload.indexOf(' ') + 1;
var pEnd = payload.indexOf(' ', pStart);
return payload.slice(pStart, pEnd).toString("ascii");
}
function setHttpPath(payload, newPath) {
var pStart = payload.indexOf(' ') + 1;
var pEnd = payload.indexOf(' ', pStart);
return Buffer.concat([payload.slice(0, pStart), Buffer.from(newPath), payload.slice(pEnd, payload.length)])
}
function httpPathParam(payload, name) {
let path = httpPath(payload);
let re = new RegExp(name + "=([^&$]+)");
let match = path.match(re);
if (match) return decodeURI(match[1]);
}
function setHttpPathParam(payload, name, value) {
let path = httpPath(payload);
let re = new RegExp(name + "=([^&$]+)");
let newPath = path.replace(re, name + "=" + encodeURI(value));
// If we should add new param instead
if (newPath == path) {
if (newPath.indexOf("?") == -1) {
newPath += "?"
} else {
newPath += "&"
}
newPath += name + "=" + encodeURI(value);
}
return setHttpPath(payload, newPath)
}
// HTTP response have status code in same position as `path` for requests
function httpStatus(payload) {
return httpPath(payload);
}
function setHttpStatus(payload, newStatus) {
return setHttpPath(payload, newStatus);
}
function httpHeaders(payload) {
var httpHeaderString = payload.slice(0,payload.indexOf("\r\n\r\n") + 4).toString().split("\n").slice(1);
var headers = {};
for (var item in httpHeaderString) {
var parts = httpHeaderString[item].split(":");
if (parts.length > 1) {
headers[parts[0]] = parts.slice(1).join(":").trim();
}
}
return headers;
}
function httpHeader(payload, name) {
var currentLine = 0;
var i = 0;
var header = { start: -1, end: -1, valueStart: -1 }
var nameBuf = Buffer.from(name);
var nameBufLower = Buffer.from(name.toLowerCase());
while(c = payload[i]) {
if (c == 13) { // new line "\n"
currentLine++;
i++
header.end = i
if (currentLine > 0 && header.start > 0 && header.valueStart > 0) {
if (nameBuf.compare(payload, header.start, header.valueStart - 1) == 0 ||
nameBufLower.compare(payload, header.start, header.valueStart - 1) == 0) { // ensure that headers are not case sensitive
header.value = payload.slice(header.valueStart, header.end - 1).toString("utf-8").trim();
header.name = payload.slice(header.start, header.valueStart - 1).toString("utf-8");
return header
}
}
header.start = -1
header.valueStart = -1
continue;
} else if (c == 10) { // "\r"
i++
continue;
} else if (c == 58) { // ":" Header/value separator symbol
if (header.valueStart == -1) {
header.valueStart = i + 1;
i++
continue;
}
}
if (header.start == -1) header.start = i;
i++
}
return
}
function setHttpHeader(payload, name, value) {
let header = httpHeader(payload, name);
if (!header) {
let headerStart = payload.indexOf(13) + 1;
return Buffer.concat([payload.slice(0, headerStart + 1), Buffer.from(name + ": " + value + "\r\n"), payload.slice(headerStart + 1, payload.length)])
} else {
return Buffer.concat([payload.slice(0, header.valueStart), Buffer.from(" " + value + "\r\n"), payload.slice(header.end + 1, payload.length)])
}
}
function deleteHttpHeader(payload, name) {
let header = httpHeader(payload, name);
if (header) {
return Buffer.concat([payload.slice(0, header.start), payload.slice(header.end+1, payload.length)])
}
return payload
}
function httpBody(payload) {
let bodyIndex = payload.indexOf("\r\n\r\n");
if (-1 != bodyIndex){
return payload.slice(bodyIndex + 4, payload.length);
} else {
return null;
}
}
function setHttpBody(payload, newBody) {
let p = setHttpHeader(payload, "Content-Length", newBody.length)
let headerEnd = p.indexOf("\r\n\r\n") + 4;
return Buffer.concat([p.slice(0, headerEnd), newBody])
}
function httpBodyParam(payload, name) {
let body = httpBody(payload);
let re = new RegExp(name + "=([^&$]+)");
if (body.indexOf(name + "=") != -1) {
let param = body.toString('utf-8').match(re);
if (param) {
return decodeURI(param[1]);
}
}
}
function setHttpBodyParam(payload, name, value) {
let body = httpBody(payload);
let re = new RegExp(name + "=([^&$]+)");
let newBody = body.toString('utf-8');
if (newBody.indexOf(name + "=") != -1 ) {
newBody = newBody.replace(re, name + "=" + encodeURI(value));
} else {
if (newBody.indexOf("=") != -1) {
newBody += "&";
}
newBody += name + "=" + value;
}
return setHttpBody(payload, Buffer.from(newBody));
}
function setHttpCookie(payload, name, value) {
let h = httpHeader(payload, "Cookie");
let cookie = h ? h.value : "";
let cookies = cookie.split("; ").filter(function(v){ return v.indexOf(name + "=") != 0 })
cookies.push(name + "=" + value)
return setHttpHeader(payload, "Cookie", cookies.join("; "))
}
function deleteHttpCookie(payload, name) {
let h = httpHeader(payload, "Cookie");
let cookie = h ? h.value : "";
let cookies = cookie.split("; ").filter(function(v){ return v.indexOf(name + "=") != 0 })
return setHttpHeader(payload, "Cookie", cookies.join("; "))
}
function httpCookie(payload, name) {
let h = httpHeader(payload, "Cookie");
let cookie = h ? h.value : "";
let value;
let cookies = cookie.split("; ").forEach(function(v){
if (v.indexOf(name + "=") == 0) {
value = v.substr(name.length + 1);
}
})
return value;
}
module.exports = {
init: init,
on: function(){ return middleware.on.apply(this, arguments) },
parseMessage: parseMessage,
searchResponses: searchResponses,
httpPath: httpPath,
httpMethod: httpMethod,
setHttpPath: setHttpPath,
httpPathParam: httpPathParam,
setHttpPathParam: setHttpPathParam,
httpStatus: httpStatus,
setHttpStatus: setHttpStatus,
httpHeader: httpHeader,
setHttpHeader: setHttpHeader,
deleteHttpHeader: deleteHttpHeader,
httpBody: httpBody,
setHttpBody: setHttpBody,
httpBodyParam: httpBodyParam,
setHttpBodyParam: setHttpBodyParam,
httpCookie: httpCookie,
setHttpCookie: setHttpCookie,
deleteHttpCookie: deleteHttpCookie,
test: testRunner,
benchmark: testBenchmark,
httpHeaders: httpHeaders
}
// =========== Tests ==============
function testRunner(){
["init", "filter", "parseMessage", "httpMethod", "httpPath", "setHttpHeader", "deleteHttpHeader", "httpPathParam", "httpHeader", "httpBody", "setHttpBody", "httpBodyParam", "httpCookie", "setHttpCookie", "deleteHttpCookie", "httpHeaders"].forEach(function(t){
console.log(`====== Start ${t} =======`)
eval(`TEST_${t}()`)
console.log(`====== End ${t} =======`)
})
}
function testBenchmark(){
const child_process = require('child_process');
let gor = init();
gor.on("message", function(){
});
gor.on("request", function(){
});
for (var i = 0; i<256; i++) {
let req = parseMessage(Buffer.from("1 2 3\nGET / HTTP/1.1\r\n\r\n").toString('hex'));
req.ID = +Date.now()
gor.emit(req);
gor.on("request", req.ID+"", function(){
gor.on("response", req.ID+"", function(){
})
})
if ( i % 3 == 0 ) {
let resp = parseMessage(Buffer.from("2 2 3\nHTTP/1.1 200 OK\r\n\r\n").toString('hex'));
resp.ID = req.ID
gor.emit(resp);
}
}
child_process.execSync("sleep 0.01");
gor.gc(1)
fail(JSON.stringify(gor.ch))
}
// Just print in red color
function fail(message) {
console.error("\x1b[31m[MIDDLEWARE] %s\x1b[0m", message)
}
function log(message) {
console.error(message)
}
function TEST_init() {
const child_process = require('child_process');
let received = 0;
let gor = init();
gor.on("message", function(){
received++; // should be called 3 times for for every request
});
gor.on("request", function(){
received++; // should be called 1 time only for request
});
gor.on("response", "2", function(){
received++; // should be called 1 time only for specific response
})
if (Object.keys(gor.ch).length != 3) {
return fail("Should create 3 channels");
}
let req = parseMessage(Buffer.from("1 2 3\nGET / HTTP/1.1\r\n\r\n").toString('hex'));
let resp = parseMessage(Buffer.from("2 2 3\nHTTP/1.1 200 OK\r\n\r\n").toString('hex'));
let resp2 = parseMessage(Buffer.from("2 3 3\nHTTP/1.1 200 OK\r\n\r\n").toString('hex'));
gor.emit(req);
gor.emit(resp);
gor.emit(resp2);
child_process.execSync("sleep 0.01");
if (received != 5) {
fail(`Should receive 5 messages: ${received}`);
}
}
function TEST_filter() {
const child_process = require('child_process');
let gor = init();
gor.on("request", function(req){
if (httpPath(req.http) != "/filter") {
return req
}
});
gor.on("request", function(req){
return req
});
let reqPass = parseMessage(Buffer.from("1 2 3\nGET / HTTP/1.1\r\n\r\n").toString('hex'));
let reqFilter = parseMessage(Buffer.from("1 2 3\nGET /filter HTTP/1.1\r\n\r\n").toString('hex'));
if (!gor.emit(reqPass)) {
return fail("Should not filter request")
}
if (gor.emit(reqFilter)) {
return fail("Should filter request even if one middleware rejected it")
}
}
function TEST_parseMessage() {
const exampleMessage = Buffer.from("1 2 3\nGET / HTTP/1.1\r\n\r\n").toString('hex')
let msg = parseMessage(exampleMessage)
let expected = { type: '1', ID: '2', meta: ["1", "2", "3"], http: Buffer.from("GET / HTTP/1.1\r\n\r\n") }
Object.keys(expected).forEach(function(k){
if (msg[k].toString() != expected[k].toString()) {
fail(`${k}: '${expected[k]}' != '${msg[k]}'`)
}
})
}
function TEST_httpPath() {
const examplePayload = "GET /test HTTP/1.1\r\n\r\n";
let payload = Buffer.from(examplePayload);
let path = httpPath(payload);
if (path != "/test") {
return fail(`Path '${patj}' != '/test'`)
}
let newPayload = setHttpPath(payload, '/')
if (newPayload.toString() != "GET / HTTP/1.1\r\n\r\n") {
return fail(`Malformed payload '${newPayload}'`)
}
newPayload = setHttpPath(payload, '/bigger')
if (newPayload.toString() != "GET /bigger HTTP/1.1\r\n\r\n") {
return fail(`Malformed payload '${newPayload}'`)
}
}
function TEST_httpMethod() {
const examplePayload = "GET /test HTTP/1.1\r\n\r\n";
let payload = Buffer.from(examplePayload);
let method = httpMethod(payload);
if (method != "GET") {
return fail(`Path '${method}' != 'GET'`)
}
}
function TEST_httpPathParam() {
let p = Buffer.from("GET / HTTP/1.1\r\n\r\n");
if (httpPathParam(p, "test")) {
return fail("Should not found param")
}
p = setHttpPathParam(p, "test", "123");
if (httpPath(p) != "/?test=123") {
return fail("Should set first param: " + httpPath(p));
}
if (httpPathParam(p, "test") != "123") {
return fail("Should get first param: " + httpPathParam(p, "test"));
}
p = setHttpPathParam(p, "qwer", "ty");
if (httpPath(p) != "/?test=123&qwer=ty") {
return fail("Should set second param: " + httpPath(p));
}
p = setHttpPathParam(p, "test", "4321");
if (httpPath(p) != "/?test=4321&qwer=ty") {
return fail("Should update first param: " + httpPath(p));
}
if (httpPathParam(p, "test") != "4321") {
return fail("Should update first param: " + httpPath(p));
}
}
function TEST_httpBodyParam() {
let p = Buffer.from("POST / HTTP/1.1\r\n\r\n");
if (httpBodyParam(p, "test")) {
return fail("Should not found param")
}
p = setHttpBodyParam(p, "test", "123");
if (httpBody(p).toString() != "test=123") {
return fail("Should set first param: " + httpBody(p).toString());
}
if (httpBodyParam(p, "test") != "123") {
return fail("Should get first param: " + httpBodyParam(p, "test"));
}
p = setHttpBodyParam(p, "qwer", "ty");
if (httpBody(p).toString() != "test=123&qwer=ty") {
return fail("Should set second param: " + httpBody(p).toString());
}
p = setHttpBodyParam(p, "test", "4321");
if (httpBody(p).toString() != "test=4321&qwer=ty") {
return fail("Should update first param: " + httpBody(p).toString());
}
if (httpBodyParam(p, "test") != "4321") {
return fail("Should update first param: " + httpBody(p).toString());
}
}
function TEST_httpHeader() {
const examplePayload = "GET / HTTP/1.1\r\nHost: localhost:3000\r\nUser-Agent: Node\r\nContent-Length:5\r\n\r\nhello";
let expected = {"Host": "localhost:3000", "User-Agent": "Node", "Content-Length": "5"}
Object.keys(expected).forEach(function(name){
let payload = Buffer.from(examplePayload);
let header = httpHeader(payload, name);
if (!header) {
fail(`Header not found. Was looking for: ${name}`)
}
if (header && header.value != expected[name]) {
fail(`${name}: '${expected[name]}' != '${header.value}'`)
}
})
}
function TEST_setHttpHeader() {
const examplePayload = "GET / HTTP/1.1\r\nUser-Agent: Node\r\nContent-Length: 5\r\n\r\nhello";
// Modify existing header
["", "1", "Long test header"].forEach(function(ua){
let expected = `GET / HTTP/1.1\r\nUser-Agent: ${ua}\r\nContent-Length: 5\r\n\r\nhello`;
let p = Buffer.from(examplePayload);
p = setHttpHeader(p, "User-Agent", ua);
if (p != expected) {
console.error(`setHeader failed, expected User-Agent value: ${ua}.\n${p}`)
}
})
// Adding new header
let expected = `GET / HTTP/1.1\r\nX-Test: test\r\nUser-Agent: Node\r\nContent-Length: 5\r\n\r\nhello`;
let p = Buffer.from(examplePayload);
p = setHttpHeader(p, "X-Test", "test");
if (p != expected) {
console.error(`setHeader failed, expected new header 'X-Test' header: ${p}`)
}
}
function TEST_deleteHttpHeader() {
const examplePayload = "GET / HTTP/1.1\r\nUser-Agent: Node\r\nContent-Length: 5\r\n\r\nhello";
// Adding new header
let expected = `GET / HTTP/1.1\r\nContent-Length: 5\r\n\r\nhello`;
let p = Buffer.from(examplePayload);
p = deleteHttpHeader(p, "User-Agent", "test");
if (p != expected) {
console.error(`setHeader failed, expected delete header 'User-Agent' header: ${p}`)
}
}
function TEST_httpBody() {
const examplePayload = "GET / HTTP/1.1\r\nUser-Agent: Node\r\nContent-Length: 5\r\n\r\nhello";
let body = httpBody(Buffer.from(examplePayload));
if (body != "hello") {
fail(`'${body}' != 'hello'`)
}
const exampleInvalidPayload = "Invalid HTTP Response by Network issue";
let invalidBody = httpBody(Buffer.from(exampleInvalidPayload));
if (invalidBody != null) {
fail(`'${invalidBody}' != 'null'`)
}
}
function TEST_setHttpBody() {
const examplePayload = "GET / HTTP/1.1\r\nUser-Agent: Node\r\nContent-Length: 5\r\n\r\nhello";
let p = setHttpBody(Buffer.from(examplePayload), Buffer.from("hello, world!"));
if (p != "GET / HTTP/1.1\r\nUser-Agent: Node\r\nContent-Length: 13\r\n\r\nhello, world!") {
fail(`Wrong body: '${p}'`)
}
}
function TEST_httpCookie() {
const examplePayload = "GET / HTTP/1.1\r\nCookie: a=b; test=zxc\r\n\r\n";
let c = httpCookie(Buffer.from(examplePayload), "test");
if (c != "zxc") {
return fail(`Should get cookie: ${c}`);
}
c = httpCookie(Buffer.from(examplePayload), "nope");
if (c != null) {
return fail(`Should not find cookie: ${c}`);
}
}
function TEST_setHttpCookie() {
const examplePayload = "GET / HTTP/1.1\r\nCookie: a=b; test=zxc\r\n\r\n";
let p = setHttpCookie(Buffer.from(examplePayload), "test", "1");
if (p != "GET / HTTP/1.1\r\nCookie: a=b; test=1\r\n\r\n") {
return fail(`Should update cookie: ${p}`)
}
p = setHttpCookie(Buffer.from(examplePayload), "new", "one");
if (p != "GET / HTTP/1.1\r\nCookie: a=b; test=zxc; new=one\r\n\r\n") {
return fail(`Should add new cookie: ${p}`)
}
}
function TEST_deleteHttpCookie() {
const examplePayload = "GET / HTTP/1.1\r\nCookie: a=b; test=zxc\r\n\r\n";
let p = deleteHttpCookie(Buffer.from(examplePayload), "a");
if (p != "GET / HTTP/1.1\r\nCookie: test=zxc\r\n\r\n") {
return fail(`Should delete cookie: ${p}`)
}
}
function TEST_httpHeaders() {
const examplePayload = "GET / HTTP/1.1\r\nHost: localhost:3000\r\nUser-Agent: Node\r\nContent-Length:5\r\n\r\nhello";
let expectedHeaders = {"Host": "localhost:3000", "User-Agent": "Node", "Content-Length": "5"}
let payload = Buffer.from(examplePayload);
let headers = httpHeaders(payload);
["Host", "User-Agent", "Content-Length"].forEach(function(header){
let actual = headers[header];
let expected = expectedHeaders[header];
if (!actual) {
fail(`${header} Header was not found`);
}
if (actual != expected) {
fail(`${header} Header not Equal to Expected: ${expected} was ${actual}`);
}
})
}
+16
View File
@@ -0,0 +1,16 @@
{
"name": "goreplay_middleware",
"version": "1.0.0",
"description": "Package for writing middleware for GoReplay https://goreplay.org",
"main": "middleware.js",
"scripts": {
"test": "node -e \"var gor = require('./middleware.js'); gor.test(); process.exit()\"",
"benchmark": "node -e \"var gor = require('./middleware.js'); gor.benchmark(); process.exit()\""
},
"keywords": [
"middleware",
"goreplay"
],
"author": "Leonid Bugaev",
"license": "LGPL-3.0"
}

Some files were not shown because too many files have changed in this diff Show More