Compare commits

...
724 Commits
Author SHA1 Message Date
Leonid Bugaev 46b6064a5e Merge branch 'master' into pr/308 2016-06-27 16:18:43 +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
Leonid Bugaev 468d94610f Merge branch 'DamianZaremba-url-rewrite2' 2014-10-09 12:29:17 +04:00
Leonid Bugaev d0266303d1 Merge #119 2014-10-09 12:29:02 +04:00
Leonid Bugaev 82ed628d45 Merge pull request #112 from buger/rollback-elastic-search
Rollback Elasticsearch
2014-10-09 11:53:59 +04:00
Leonid Bugaev 97dd1520f1 Update README.md 2014-10-09 11:53:30 +04:00
Leonid Bugaev 972a80356d Add go get call to travisci 2014-10-09 11:44:27 +04:00
Leonid Bugaev 91a0282755 Update go version 2014-10-09 11:43:51 +04:00
Leonid Bugaev 2c03d3ee28 Run go get for docker image, and fix make file 2014-10-09 11:41:03 +04:00
Leonid Bugaev b2f90789d2 Instead of godep just clone elasticgo repo 2014-10-09 11:37:00 +04:00
Leonid Bugaev e68332b897 Update README.md 2014-10-08 21:10:34 +04:00
Leonid Bugaev cf0dfecac2 Merge pull request #116 from joekiller/scale_http_output_squash
Scale http output squash
2014-10-08 20:48:58 +04:00
Joseph Lawson 4f9c983bc7 Dynamic scaling by default 2014-10-05 20:03:50 -04:00
Joseph Lawson 988c6abdad Dynamic workers toggle. Separate stats toggles. README and FAQs
* Created separate toggles for output-http-stats and output-tcp-stats
  * Created HTTPWorkers option (defaults to 10) to allow larger number of HTTPWorkers to be spawned.
  * Refactored output_http to allow dynamic http worker scaling when httpworkers equals -1
  * Updated README.me to include updates and some common FAQs.
2014-10-05 15:53:50 -04:00
Damian Zaremba 864723fe47 First go at a basic test 2014-09-28 13:23:47 +01:00
Damian Zaremba 2c2d5439bf Add in --output-http-rewrite-url option 2014-09-28 01:15:18 +01:00
Leonid Bugaev c04b2528b8 Merge pull request #115 from Blank101/filefix
fixed file input sleep timer
2014-09-25 10:34:08 +04:00
Joseph Lawson 58944fd42d calling this version 0.9.0 2014-09-24 14:57:49 -04:00
Joseph Lawson 6e125a7b7b allow dynamic http workers with a signal channel for full queue 2014-09-24 14:31:16 -04:00
Sam MacPherson 1a7cf2bbab fixed file input sleep timer 2014-09-23 16:54:26 -04:00
Leonid Bugaev e022fdff98 Update travisci config 2014-09-04 23:41:05 +04:00
Leonid Bugaev c0dc1b3a53 Rollback elastic search, use godep for dependencies 2014-09-04 23:35:56 +04:00
Leonid Bugaev 674d6036d1 Revert "Remove elastic search functionality"
This reverts commit cd27667246.
2014-09-04 22:48:08 +04:00
Leonid Bugaev 6f6c6aba95 Fix readme 2014-09-04 22:43:10 +04:00
Leonid Bugaev 2a6e9f7a92 Merge pull request #111 from buger/docker-dev
Add Docker development environment
2014-09-04 22:35:50 +04:00
Leonid Bugaev 007e0f3019 Add Docker development environment 2014-09-04 22:32:58 +04:00
Leonid Bugaev 115a37e0e9 Update README.md 2014-08-21 16:39:45 +04:00
Leonid Bugaev b2c87ff2a4 Add TomTom to companies using Gor 2014-08-21 16:38:34 +04:00
Leonid Bugaev 3a1efccbca Update README.md 2014-08-20 21:11:26 +04:00
Leonid Bugaev a2873116a3 Update README.md 2014-08-20 21:10:58 +04:00
Leonid Bugaev ccb309170f Merge pull request #108 from matzhouse/master
gor issue 74
2014-08-12 12:44:59 +04:00
Mat Evans defc017ade Add a testable SetHeader function with associated tests
It seemed easier to pull this bit of functionality out and be able to test it on it's own than rely on the whole http tests
2014-08-07 10:22:04 +01:00
Mat Evans 9e2f7feec7 gor issue 74
explicitly set the Host on the request when using the --output-http-header option
2014-07-31 11:40:40 +01:00
Leonid Bugaev c56bbd98ce Merge pull request #107 from buger/readme_host_header
Remove example of injecting Host header
2014-07-30 14:29:13 +04:00
Dan Carley 787a906bb8 Remove --output-http-header typo from README
The ` -header "` was extraneous.
2014-07-30 07:50:23 +01:00
Dan Carley d608d18057 Remove example of injecting Host header
This doesn't work per the discussion in #74. So we shouldn't document it.
2014-07-30 07:49:03 +01:00
Leonid Bugaev cd27667246 Remove elastic search functionality 2014-07-22 22:50:04 +04:00
Leonid Bugaev bae1a84f40 Update README.md 2014-07-22 22:33:55 +04:00
Leonid Bugaev 057e642b87 Updated build instructions 2014-06-26 14:26:07 +04:00
Leonid Bugaev c94cc88608 Merge pull request #99 from mantasmatelis/filter-regex-and-header
Allow filtering by regex of URL or specific header, consistent filtering by hash of header
2014-06-19 18:48:42 +04:00
Mantas Matelis 731a3745a4 Add tests 2014-06-18 12:24:53 -04:00
Mantas Matelis c4307b750a Documentation and test fixes 2014-06-12 11:43:04 -04:00
Mantas Matelis 9d341738de Fix readme spacing 2014-06-11 15:46:39 -04:00
Mantas Matelis c8d623580a Update readme 2014-06-11 15:42:36 -04:00
Mantas Matelis 2e29e3ab9e Regex on string, not byte array 2014-05-27 17:26:28 -04:00
Mantas Matelis bf786e5922 minor changes 2014-05-27 15:18:24 -04:00
Mantas Matelis a9a08860b4 less, not greater 2014-05-27 14:58:49 -04:00
Mantas Matelis c6c021d65f Further fmting 2014-05-27 14:51:11 -04:00
Mantas Matelis 49da801b33 go fmt personal files 2014-05-27 14:50:21 -04:00
Mantas Matelis a4a4c605a5 Remove log statements 2014-05-27 14:45:17 -04:00
Mantas Matelis 11d9531829 Add ability to filter output_http by headers/regex
Can filter output_http by percentage of requests, constistent with a specific header value (FNV32-a hash-based)
Can filter output_http to those with header matching a specific regexp
Can filter output_http to those whose URL matches a specfic regexp
2014-05-27 14:23:51 -04:00
Leonid Bugaev a6af811c50 Merge pull request #95 from joekiller/queue_stats_output
Queue stats output
2014-04-24 14:04:26 +06:00
Joseph Lawson 7bf6ca4c87 adding count stat 2014-04-23 16:25:22 -04:00
Joseph Lawson 31aef9ee1d adding count stat 2014-04-23 16:22:27 -04:00
Joseph Lawson 41fe11c12f added output_tcp queue stats 2014-04-23 16:06:29 -04:00
Joseph Lawson 4720070996 finished stat for output_http 2014-04-23 14:05:42 -04:00
Joseph Lawson dbfea7ceac as many workers are their are channel 2014-04-23 14:02:33 -04:00
Joseph Lawson c49c19428d as many workers are their are channel 2014-04-23 12:58:41 -04:00
Joseph Lawson 7bf2015e90 fixing stat output 2014-04-23 12:52:01 -04:00
Joseph Lawson db14154eb9 fix typo 2014-04-23 12:48:18 -04:00
Joseph Lawson b52d562dad tweaking stat output 2014-04-23 12:47:16 -04:00
Joseph Lawson 9461217a0e write stat help when init 2014-04-23 12:43:15 -04:00
Joseph Lawson 40d835e143 update rate types 2014-04-23 12:23:15 -04:00
Joseph Lawson c97f02ff4c updated stat printer 2014-04-23 12:22:47 -04:00
Joseph Lawson 62254b6265 updated stat printer 2014-04-23 12:20:43 -04:00
Joseph Lawson d5535ee7f3 fixing pointers 2014-04-23 11:50:23 -04:00
Joseph Lawson 959ac29661 fixing pointers 2014-04-23 11:48:49 -04:00
Joseph Lawson b5c2d7747c fixing pointers 2014-04-23 11:47:06 -04:00
Joseph Lawson 610552be88 fixing stat errors 2014-04-23 11:45:39 -04:00
Joseph Lawson c250c3e30c add gor_stat 2014-04-23 11:34:51 -04:00
Leonid Bugaev ce5ee74d74 Merge pull request #94 from joekiller/sort_tcp_packets
Sort tcp packets
2014-04-23 20:19:43 +06:00
Joseph Lawson c4d0eb0ae1 revert accidental channel call 2014-04-22 19:58:39 -04:00
Joseph Lawson 840ca61de9 update input raw import 2014-04-22 16:22:30 -04:00
Joseph Lawson 5cb3562796 Merge branch 'can_not' into sort_tcp_packets 2014-04-22 16:21:02 -04:00
Joseph Lawson 384b3c5614 Merge branch 'message_sort_fix' into realistic_message_timeout 2014-04-22 16:07:35 -04:00
Joseph Lawson e97827d600 capping at 2 second maximum 2014-04-22 16:05:00 -04:00
Joseph Lawson 3eed7c2546 send message closed notification if channel closes. 2014-04-22 16:02:40 -04:00
Joseph Lawson fa8f612a4f 20 second timeout on a message 2014-04-22 14:01:34 -04:00
Joseph Lawson b00ea39d3e apache maximum timeout 2014-04-22 13:52:17 -04:00
Joseph Lawson 06b4892b33 switch raw socket listener 2014-04-22 13:42:05 -04:00
Joseph Lawson 06b08105bf 20 second message timeout 2014-04-22 13:37:36 -04:00
Joseph Lawson 8ccc0249ca typo fix. 2014-04-22 11:46:19 -04:00
Joseph Lawson e9bb6ef8f3 added sort by Seq for packets, updated Bytes to return data based on sorted t.packets 2014-04-22 11:06:16 -04:00
Joseph Lawson adf7ed6b84 return packets in Seq order 2014-04-22 10:45:05 -04:00
Joseph Lawson 0dc315b343 set m[k] from packets based on the Seq index 2014-04-21 15:43:06 -04:00
Joseph Lawson 48ae0e6812 wrap through the range of packets 2014-04-21 15:38:48 -04:00
Joseph Lawson 0d398dbf3d convert to proper int 2014-04-21 15:36:05 -04:00
Joseph Lawson e373073742 switch raw socket listener to local 2014-04-21 15:29:57 -04:00
Joseph Lawson 296034ef00 make message sort by packet's index 2014-04-21 15:29:21 -04:00
Joseph Lawson a786c78d26 Merge remote-tracking branch 'upstream/master' 2014-04-21 15:27:13 -04:00
Leonid Bugaev ed68b27153 Merge pull request #91 from MandaBrown/the-tiniest-typo-fix
Fixes a tiny typo in the README.
2014-04-14 10:20:29 +06:00
Mandy Brown e6fe38cfda Fixes a tiny typo in the README. 2014-04-13 21:45:26 -04:00
Joseph Lawson 441f0ade04 Merge remote-tracking branch 'upstream/master'
Conflicts:
	emitter.go
2014-04-02 14:50:17 -04:00
Leonid Bugaev 8d8c10f3ea Update .travis.yml 2014-04-02 17:24:04 +06:00
Leonid Bugaev fc52decebf Merge pull request #70 from buger/catch_redirect_errors
Catch RedirectNotAllowed correctly
2014-04-02 14:55:41 +06:00
Leonid Bugaev f5d74e84f6 Merge pull request #88 from joekiller/prebuf
Fix TCP Issues
2014-04-02 14:52:39 +06:00
Joseph Lawson 983eb7c6a8 revert buf 2014-04-01 15:42:11 -04:00
Joseph Lawson 8edbb7fb08 new buffer each loop 2014-04-01 15:40:30 -04:00
Joseph Lawson 89f1b50b8d new_buf_len needs to be > 0 as we already account for the control character. 2014-03-31 15:42:00 -04:00
Joseph Lawson 9b0153f45f prevent crash on quits 2014-03-31 11:43:33 -04:00
Joseph Lawson 832bff26d1 update input_tcp_test to reflect new way of sending messages 2014-03-31 10:48:13 -04:00
Joseph Lawson 3d3cb1dbf5 fixed a reader bug 2014-03-29 18:52:42 -04:00
Joseph Lawson ca952b7a8b overflow fix 2014-03-29 18:42:11 -04:00
Joseph Lawson 872a4dcae9 Update output_tcp_test.go 2014-03-29 18:38:41 -04:00
Joseph Lawson b70290274e Update output_tcp_test.go 2014-03-29 18:33:19 -04:00
Joseph Lawson 787d92d94a attempt to fix test 2014-03-29 18:23:14 -04:00
Joseph Lawson ae5f7e48b0 revert timeout change 2014-03-29 00:36:40 -04:00
Joseph Lawson 29deb996bd copy data buffer 2014-03-28 23:41:02 -04:00
Joseph Lawson 02fe5a976f Merge branch 'tcp_input_fix' of github.com:joekiller/gor into tcp_input_fix 2014-03-28 23:40:37 -04:00
Joseph Lawson c8e39b8f47 correct delimiter append 2014-03-28 23:40:28 -04:00
Joseph Lawson c5150cd911 fix append 2014-03-28 23:36:38 -04:00
Joseph Lawson 7a657e273e Merge branch 'tcp_input_fix' of github.com:joekiller/gor into tcp_input_fix
Conflicts:
	output_tcp.go
2014-03-28 23:35:55 -04:00
Joseph Lawson cc2dd53de8 write full message to new array for async buffer 2014-03-28 23:31:30 -04:00
Joseph Lawson cad94ead29 write full message to new array for async buffer 2014-03-28 23:21:47 -04:00
Joseph Lawson 28f7078dbb make tcpinput use readbytes and create new slice prior to copying to the buffer. 2014-03-28 23:09:49 -04:00
Joseph Lawson 841bba116a revert overflow check 2014-03-28 22:59:05 -04:00
Joseph Lawson 3a3ae5b283 revert overflow check 2014-03-28 22:57:39 -04:00
Joseph Lawson 3299955888 Merge remote-tracking branch 'upstream/master' 2014-03-25 11:27:46 -04:00
Leonid Bugaev 3aa04783f9 Fix tests 2014-03-22 17:11:21 +05:00
Leonid Bugaev 5a7c817f0b Fixed buffer overflow 2014-03-22 17:58:31 +06:00
Leonid Bugaev b800896b04 Incoming ip should be included into message ID 2014-03-19 19:33:24 +06:00
Joseph Lawson e7a50ac5d4 increasing MSG_EXPIRE
Attempting to decrease unexpected EOF errors on replay instance.
2014-03-17 13:22:02 -04:00
Leonid Bugaev 0fb1d83284 Simplify directory structure 2014-03-16 22:24:27 +06:00
Leonid Bugaev 1784516771 Bump version and add Makefile 2014-03-16 22:05:13 +06:00
Leonid Bugaev 9fa5ef74fb Quick fix for #61 2014-03-16 22:03:09 +06:00
Leonid Bugaev d67bea5e10 Fixed #82 2014-03-16 22:01:23 +06:00
Leonid Bugaev 263d1347a0 Update version 2014-01-29 21:29:46 +06:00
Leonid Bugaev 19008c2a2a Merge pull request #55 from buger/tcp_pool
Tcp pool
2014-01-29 07:26:51 -08:00
Leonid Bugaev 49cdbbe41b Removed unused package 2014-01-29 21:13:57 +06:00
Leonid Bugaev 59a2b7ca1d GOMAXPROCS should be set manually 2014-01-29 21:13:24 +06:00
Leonid Bugaev 9a32742653 Fixed tests 2014-01-29 21:10:35 +06:00
Leonid Bugaev 38b171d357 Merge branch 'master' into tcp_pool
Conflicts:
	output_http.go
	output_http_test.go
2014-01-29 14:57:47 +00:00
Dan Carley 90261550c4 Catch RedirectNotAllowed correctly
The error `RedirectNotAllowed` is wrapped by `url.Error`, causing it to not
currently be caught/ignored correctly for responses that issue redirects:
```
2014/01/06 12:17:17 Request error: Get https://example.com/foo: Redirects not allowed
```

It seems that two type assertions/conditions are required to unravel the
exact error. I can't see any way to write a test for this functionality --
the call to `defer()` or `log.Println()`.
2014-01-06 17:07:48 +00:00
Leonid Bugaev 7814bd8bc5 Merge pull request #66 from alphagov/replay_get_methods_only
Filter HTTP methods on output
2013-12-23 02:29:18 -08:00
Leonid Bugaev 40cd01630b Merge pull request #67 from mtrojanowski/elastigo-indexer-fix
Changed `Indexor` to `Indexer` to comply with the newest ver of elastigo.
2013-12-18 06:11:46 -08:00
mtrojanowski 19eadf49b9 Changed Indexor to Indexer to comply with newest ver of elastigo. 2013-12-18 14:55:51 +01:00
Leonid Bugaev 227e95467a Merge pull request #63 from robyoung/check-packet-size
Check packet has data via buffer size not PSH
2013-12-17 10:00:48 -08:00
Dan Carley 14d9ca6d54 Filter HTTP methods on output
Add a new CLI flag to specify which HTTP methods should be replayed by
HTTPOutput. If specified, any requests not matching will be dropped.

This can be useful for replaying requests against stateful environments.
e.g. You may not want to reproduce POST requests against an application if
it results in additional calls to the outside world.

Some variations I considered:

- Filtering on input instead of output. However we don't currently do any
  request parsing on input, and doing so would likely have an impact on
  performance.

- A request filtering plugin. We might want to revisit this if we add
  anymore options to HTTPOutput. It could also be used to modify requests in
  an existing file capture. Although because all plugins expect byte slices
  we'd potentially have to parse requests more than once.

I don't think this warrants a separate test for HTTPOutput yet. But, again,
if we add anymore then we should split them out, and DRY up if possible.
Because we don't want to overload the scenarios covered by that one test.
2013-12-17 17:51:35 +00:00
Leonid Bugaev b9c97eabaf Merge pull request #65 from alphagov/62-misbehaving_http_output_test
[Fixes #62] HTTPOutput take copy of request byte slice
2013-12-17 05:37:58 -08:00
Dan Carley d9c54d31ad [#62] HTTPOutput take copy of request byte slice
At the time `CopyMulty()` calls `HTTPOutput.Write(data)` the contents of the
byte slice `data` is correct/consistent. However by the time the goroutine
for `HTTPOutput.sendRequest(data)` is scheduled, the contents of `data` has
changed, which in the case of our tests results in two things happening:

- The same request gets repeated many times.
- A request with the length of `EmitGET()` is made but with the larger
  contents of `EmitPOST()`, causing it to be truncated and
  `HTTPOutput.ParseRequest()` fails.

As I understand it, this is because the slice header of `data` is passed by
value into the goroutine, but the contents referred to by that header are
pointers which subsequently get overwritten.

By taking a `copy()` of the request data into a new buffer variable and
passing that to the goroutine, we can ensure that it doesn't get modified
in-flight.
2013-12-17 10:47:49 +00:00
Dan Carley 796f192e71 [#62] Reverse order of GET/POST in HTTP out test
Swap the order of calls to `EmitGET()` and `EmitPOST()` for HTTP output
tests in order to surface the problems:

- GET requests appear to be repeated. For three iterations, 5 GETs and only
  1 POST request can be observed at `StartHTTP()`.
- POST requests are truncated somewhere along the line and cannot be parsed
  by `HTTPOutput.ParseRequest()`. Resulting in:

      Can not parse request POST /pub/WWW/ HTT malformed HTTP version "HTT"

There is an underlying problem with the test that it only checks the request
count, not the type/content of "good" requests.
2013-12-17 10:36:22 +00:00
Rob Young eb6d0cddd9 Check packet has data via buffer size not PSH
The PSH flag tells the receiver to flush it's buffer. While this will
never be set for packets that have no data it can be, and often is, not
set for packets that do. For example, when a message spans more than one
segment the earlier segments often do not have the PSH flag set.

This alternative solution checks that the buffer is larger than the TCP
header.
2013-12-02 21:22:27 +00:00
Leonid Bugaev f82dade127 Merge pull request #60 from waffleio/master
waffle.io Badge
2013-11-22 03:49:54 -08:00
waffle.io 5f35f41637 add waffle.io badge 2013-11-22 03:48:55 -08:00
Leonid Bugaev 26ddfcd3cd Update README.md 2013-11-18 14:34:08 +01:00
Leonid Bugaev ac3f255d38 Merge pull request #58 from tambet/master
Read requests from file
2013-11-15 14:36:09 -08:00
Tambet Masik 68ba87c19e Read requests from file 2013-11-15 22:36:29 +02:00
Leonid Bugaev fd9f6cd056 Connections pool for output_tcp 2013-11-02 21:45:49 +01:00
Leonid Bugaev 9bba1296bd Persistent connection support for input_tcp 2013-11-02 21:11:17 +01:00
Leonid Bugaev f7b3adc8cd Add delay to http server for proper testing 2013-11-02 19:46:46 +01:00
Leonid Bugaev dd806640f9 Remove debugging 2013-11-02 17:33:36 +01:00
Leonid Bugaev 824c51248c Use workers and increase GOMAXPROCS
Gives about 2x increase.
2013-11-02 17:26:59 +01:00
Leonid Bugaev f8a8444b27 Add basic benchmakrs 2013-11-02 16:53:59 +01:00
Leonid Bugaev c811dc017e Update README.md 2013-10-31 13:36:55 +01:00
Leonid Bugaev c2424b2b8f Update README.md 2013-10-31 13:36:33 +01:00
Leonid Bugaev ccf6723743 Merge pull request #46 from buger/module_architecture
Merge listener and replay functionality
2013-10-31 04:46:56 -07:00
Leonid Bugaev fa9c9c3387 Merge branch 'master' into module_architecture 2013-10-31 11:44:16 +00:00
Leonid Bugaev 8620e4a467 Updated README 2013-10-31 12:43:42 +01:00
Leonid Bugaev 457e92ed11 Updated Procfile 2013-10-31 12:43:07 +01:00
Leonid Bugaev 9da6f908fd Updated CHANGELOG 2013-10-31 12:39:41 +01:00
Leonid Bugaev 4406794a28 Rename dirrectory with runnable file 2013-10-31 12:15:06 +01:00
Leonid Bugaev 92c8dd19f4 Updated documentation 2013-10-31 12:11:56 +01:00
Leonid Bugaev 860189b1c5 Update ELASTICSEARCH.md 2013-10-31 10:40:54 +01:00
Leonid Bugaev 0f27a45fbb Update README.md 2013-10-31 10:38:06 +01:00
Leonid Bugaev 8ffdd10988 Added ElasticSearch statistics 2013-10-30 15:40:44 +01:00
Leonid Bugaev 6a136b4686 Add splitOutput option if have multiple outputs 2013-10-30 14:53:16 +01:00
Leonid Bugaev af3a7a7541 Addded limiting support to output_tcp 2013-10-30 14:35:40 +01:00
Leonid Bugaev 602448accd Added custome headers 2013-10-30 14:29:32 +01:00
Leonid Bugaev 8b9c72ef9e Don't panic if replay server is not responding 2013-10-30 12:48:07 +01:00
Leonid Bugaev 95d3676d83 Add delay between request for input_file 2013-10-29 20:49:59 +01:00
Leonid Bugaev 72952fddd2 Added input_file plugin 2013-10-29 18:53:35 +01:00
Leonid Bugaev faa3cb7307 Add limiter 2013-10-29 15:50:56 +01:00
Leonid Bugaev ce76453fa6 Added tests for all plugins 2013-10-29 11:42:15 +01:00
Leonid Bugaev 9dddab58d3 Fiix: Profiling never start 2013-10-29 08:07:27 +01:00
Leonid Bugaev c42a42928b Require at least 1 input and 1 output 2013-10-29 08:06:32 +01:00
Leonid Bugaev 2a4efa91af Initial commit 2013-10-27 18:49:00 +00:00
Leonid Bugaev 2fd36f03f1 Merge pull request #48 from alphagov/binary_file_format
[#47 #36] Serialize requests to file using gob
2013-10-25 07:28:14 -07:00
Leonid Bugaev 3b890c6d7f Merge pull request #51 from alphagov/document_basic_auth_injection
[#31] Document BasicAuth injection
2013-10-24 07:25:04 -07:00
Dan Carley bc4fabd0da [#31] Document BasicAuth injection
Has been asked about previously. `http.Client.Do` does this for us.
2013-10-24 13:02:14 +01:00
Dan Carley 24a240c819 [README] Stylise GOV.UK link
Use `https://www` to spare a redirect. And capped-up-cos-we-mean-business:
http://www.theregister.co.uk/2012/02/01/gov_uk_single_domain_release/
2013-10-24 12:42:47 +01:00
Dan Carley 2f049dddbd Rename ParsedRequest to RawRequest
Since it contains a raw byte slice of the request data rather than a
`http.Request` object.
2013-10-23 21:07:35 +01:00
Dan Carley e7c1d86bfa Move ParsedRequest{} to gor/utils sub-package
This de-dupes the type definition used in both `listener` and `replay`.
2013-10-23 17:17:32 +01:00
Dan Carley 714307890e [#47 #36] Serialize requests to file using gob
To replace the serial plaintext representation of request and timestamp.
This fixes a bug whereby we were unable to delimit POST requests with bodies
because they didn't end in `\r\n\r\n`, frequently resulting in:
```
--- FAIL: TestSavingRequestToFileAndReplayThem (2.80 seconds)
        integration_test.go:346: Timeout error
```

Now we don't need to play guesswork with `Content-Length` headers or
introduce a custom delimiter. As a bonus, the code required is also slightly
simpler.

It is no longer possible to manipulate the `.gor` file by hand; but it does
make it trivial to write a tool that can parse and modify.

There are some `go fmt` indent changes rolled into `replay_file_parser`.
2013-10-22 21:54:24 +01:00
Leonid Bugaev 69df96e55d Merge pull request #43 from alphagov/replay_header_injection
Allow injection of additional headers in replay
2013-10-21 05:42:10 -07:00
Dan Carley 9c6802f5e8 Integration test for header injection
Re-using one of the existing integration tests. If we were to refactor some
of the logic to send and receieve a single request then it would make sense
to split this, and the cookie validation, out to separate test cases.
2013-10-21 13:33:27 +01:00
Dan Carley d54da99667 Unit test the parsing of -header arguments
Per the features described in the preceding commit.
2013-10-21 13:31:15 +01:00
Dan Carley f4af3a9bd4 Allow injection of additional headers in replay
Similar to curl(1) and siege(1):

- Can be specified multiple times.
- Will overwrite headers of the same key name in the original request.
- An argument without a value (`Foo:`) will be set as empty.
- Leading and trailing whitespace will be stripped from the key and value,
  otherwise it's difficult to determine where the key or value begin/end.
2013-10-21 13:30:52 +01:00
Leonid Bugaev 1f9c9a6154 Merge pull request #44 from alphagov/run_all_tests
Run all tests + fix failing ES test
2013-10-20 13:25:55 -07:00
Dan Carley 9e175434d6 Fix failing Elasticsearch test
Use the local instance of `ReplaySettings` rather than `Settings` which is
initialised as an empty struct and then populated by flag. Since we're not
calling flag, this remains empty, and `esp.Init()` is passed an empty
string.

For the same reason the call to `settings.Parse()` is actually redundant.
`Parse()` could be changed to use `r.ElastiSearchURI`, but it results in the
plugin begin setup/registered twice, because it doesn't give us access to
the host/port/index properties.
2013-10-20 21:18:25 +01:00
Dan Carley 9a126103fc Ensure that TravisCI tests all subpackages
Previously this was only running the integration tests in the root
directory. `go test` needs to be given wildcard in order to pickup tests in
sub directories/packages.
2013-10-20 20:53:09 +01:00
Leonid Bugaev 9a978422b2 Update README.md 2013-10-19 20:35:47 +02:00
Leonid Bugaev e02c145ac6 Do not count redirects as errors 2013-10-19 10:53:36 +02:00
Leonid Bugaev 7d482d47de Update README.md 2013-10-18 14:15:08 +02:00
Leonid Bugaev f7cf05c257 Merge pull request #41 from alphagov/readme_file_based_timings
Document time delay in file-based replay
2013-10-18 03:16:58 -07:00
Leonid Bugaev 4af1230d83 Merge pull request #40 from alphagov/rename_reply_to_replay
Rename instances of "reply" to "replay"
2013-10-18 03:16:39 -07:00
Dan Carley 18597d9e7a Rename instances of "reply" to "replay"
This looks to have been a typo. Limited to file-based playback only. The
word "replay" makes more sense here, and is consistent with TCP playback.
2013-10-18 11:01:37 +01:00
Dan Carley fe02bfa8c5 Document time delay in file-based replay
This is an awesome feature that I've been looking for, for sometime. But I
had to dig through the code to verify that it did such. It should be called
out in the README instead!
2013-10-18 10:57:28 +01:00
Sławosz 48d4406893 Merge pull request #39 from alphagov/garbled_file_parsing
Fix garbled request input from file
2013-10-18 01:48:25 -07:00
Dan Carley f07c7f5eaf Merge branch 'master' into garbled_file_parsing 2013-10-18 09:35:08 +01:00
Leonid Bugaev db4b2c26b0 Fixed #36. Replaying requests to mutiple hosts 2013-10-17 20:55:18 +02:00
Dan Carley 8ecda0fdb5 Fix garbled request input from file
Buffer a copy of `scanner.Bytes()` when appending to a slice of requests.
Otherwise the data may be overwritten in subsequent loops. This results in
garbled requests, without truncated output and sometimes random characters.
This may not be apparent when a small number of requests are used.

Ref: http://golang.org/pkg/bufio/#Scanner.Bytes
```
Bytes returns the most recent token generated by a call to Scan. The
underlying array may point to data that will be overwritten by a subsequent
call to Scan. It does no allocation.
```

This fixes the tests errors like:
```
2013/10/17 15:26:28 Starting file reply2013/10/17 15:26:28 Can not parse request =b&c=d
1382023587787798594
POST /test HTTP/1.1
Host: localhost:50030
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.9.1 Safari/534.34
Content-Length: 7
Accept: */*
Accept-Language: en-GB,*
Cookie: test=value
Referer: http://localhost/test
X-Forwarded-For: 1.1.1.1, 2.2.2.2, 3.3.3.3
X-Fo malformed HTTP request "=b&c=d"
2013/10/17 15:26:28 Can't parse request...:malformed HTTP request "=b&c=d"
```
2013-10-17 16:51:38 +01:00
Dan Carley 06befe3b4b Integration test for garbled requests from file
Increase the number of iterations and size requests written to file in order
to trigger a bug when parsing requests back in.

This test will currently fail with: `Can't parse request...`
2013-10-17 16:40:58 +01:00
Leonid Bugaev 8c41c01d4b Update README.md 2013-10-13 19:53:40 +02:00
Leonid Bugaev 4e41151963 Update README.md 2013-10-13 19:51:07 +02:00
Leonid Bugaev 18ec957e62 Merge pull request #37 from gottwald/es-docs
Added elasticsearch documentation
2013-10-13 10:48:07 -07:00
Ingo Gottwald 857a9b4835 Updated README.md linked to ELASTICSEARCH.md 2013-10-13 17:34:34 +02:00
Ingo Gottwald 12ae1455f8 Added elasticsearch documentation 2013-10-13 17:21:30 +02:00
Leonid Bugaev da2478ca3c Fix Reply From File test 2013-10-12 14:16:14 +02:00
Leonid Bugaev c32deec006 Tests: Added second host for replaying 2013-10-12 09:52:27 +02:00
Leonid Bugaev f5a8274fcd One more fix 2013-10-12 09:44:57 +02:00
Leonid Bugaev d1708ff1f3 Fixed test 2013-10-12 09:42:25 +02:00
Leonid Bugaev fd642bded1 Improve debugging 2013-10-10 11:33:31 +02:00
Leonid Bugaev 40203d2519 Turn on verbose mode for tests 2013-10-10 11:27:48 +02:00
Leonid Bugaev fd27e13e85 Turn on verbose mode for go test 2013-10-10 11:26:34 +02:00
Leonid Bugaev d46e587c08 Added tests for POST requests 2013-10-10 11:09:50 +02:00
Leonid Bugaev fa2dedceb3 Merge pull request #35 from buger/add_procfile
Added Procfile to simplify debuging
2013-10-10 00:04:46 -07:00
Leonid Bugaev 983e2a0e79 Merge pull request #33 from buger/elasticsearch_refactoring
ElasticSearch refactoring and updated docs
2013-10-09 23:56:28 -07:00
Leonid Bugaev 42173e4687 Some minor changes 2013-10-10 08:47:19 +02:00
Leonid Bugaev 8757373a7a Fix typo 2013-10-10 08:44:38 +02:00
Leonid Bugaev b6e7b18803 Added Procfile to simplify debuging 2013-10-10 08:33:45 +02:00
Leonid Bugaev e21f9b024d Fix replay. It should not exit if single request is not parsed.
Fix #34
2013-10-09 20:32:24 +02:00
Leonid Bugaev 20901ec6ae Some refactoring and updated docs 2013-10-09 12:58:17 +02:00
Leonid Bugaev 327ee32395 Fix same settings parse bug on replay side
Added basic settings tests
2013-10-09 09:19:57 +02:00
Leonid Bugaev e32b985df2 Fixed settings parse bug
Fix #32
2013-10-09 08:56:03 +02:00
Leonid Bugaev 737ea9fc68 Merge branch 'elasticsearch' of https://github.com/gottwald/gor into gottwald-elasticsearch
Conflicts:
	replay/replay.go
	replay/request_factory.go
2013-10-08 22:20:06 +02:00
Leonid Bugaev 509b27c476 Merge pull request #25 from slawosz/save-request-to-reply-later
Save request to file for later replay
2013-10-08 13:12:42 -07:00
Ingo Gottwald 94199fcb25 Fixed elasicsearch plugin
Don't try to export failed requests
2013-10-08 20:32:35 +02:00
Sławosz bba9637eed remove comment 2013-10-08 19:54:37 +02:00
Sławosz 685c998e8e remove MessageLogger 2013-10-08 19:47:30 +02:00
Sławosz 8adeda9a98 finish replay, when all requests got replayed 2013-10-08 19:41:24 +02:00
Sławosz 9077656bdf Information about replaying in README 2013-10-08 12:59:41 +02:00
Sławosz 830ae237c9 better messages on listener start 2013-10-08 12:54:54 +02:00
Ingo Gottwald 88dd2602df More data for elasticsearch
- Removed extension based mime analysis
- Added more data fields for elasticsearch
- ESResponse is now a ESRequestResponse struct
(holds combined data from requests and responses)
2013-10-06 14:36:16 +02:00
Ingo Gottwald 27582841fe Added elasticsearch response analyze plugin 2013-10-05 23:35:49 +02:00
Ingo Gottwald b53218e729 Added timing information to HTTP responses 2013-10-05 23:09:49 +02:00
Ingo Gottwald 54e9de5088 Implemented support for Response Analyze Plugins 2013-10-05 23:07:54 +02:00
Sławosz Sławiński 5782d486f9 Parse request with timestamp properly 2013-10-04 04:56:48 +00:00
Sławosz Sławiński 8b66a99665 Save request timestamp
WIP
2013-10-03 21:44:26 +00:00
Sławosz 30547f2a41 Embed log.Logger in MessageLogger 2013-10-02 20:07:19 +02:00
Sławosz 140267b6a6 Merge branch 'master' into save-request-to-reply-later
Conflicts:
	listener/raw_tcp_listener.go
	replay/replay.go
	replay/settings.go
2013-10-02 19:47:28 +02:00
Sławosz e4807b7abb Merge branch 'master' of https://github.com/buger/gor 2013-10-02 19:36:05 +02:00
Sławosz 20ea55e6e7 remove debug statement 2013-10-02 19:34:54 +02:00
Sławosz 47b498d4d9 fix request reading 2013-10-02 19:27:52 +02:00
Leonid Bugaev 4305b72608 Merge pull request #24 from buger/refactoring
Refactoring and speed improvements
2013-09-30 03:18:33 -07:00
Leonid Bugaev 578d785c27 Fixed previous merge
oops :)
2013-09-30 11:32:26 +02:00
Leonid Bugaev 6946fc6036 Merge branch 'master' into refactoring
Conflicts:
	listener/raw_tcp_listener.go
	listener/tcp_message.go
	listener/tcp_packet.go
2013-09-30 11:26:35 +02:00
Leonid Bugaev 8535872b10 Merge pull request #23 from fern4lvarez/review
General review
2013-09-30 01:22:18 -07:00
Fernando Álvarez a1f9cc501b General review:
- Fix typos
- Follow some Go good practices
- Better documentation, more consistent godoc.org
- Refactoring
2013-09-30 10:00:30 +02:00
Leonid Bugaev f07586a12e isIncomingDataPacket should be struct function 2013-09-30 09:26:07 +02:00
Leonid Bugaev fa684d4645 Used maps instead of slices for storing messages 2013-09-30 09:24:47 +02:00
Leonid Bugaev c3330cb4be Use close(channel) instead of creating new channel 2013-09-30 09:17:32 +02:00
Leonid Bugaev 3cb98aaf94 Use less buffer 2013-09-30 09:12:22 +02:00
Leonid Bugaev cbc4204e50 Some minor changes 2013-09-30 09:02:27 +02:00
Sławosz 207eea1ba7 file replay partialy working 2013-09-29 14:36:43 +02:00
Leonid Bugaev de93abeab8 Added RawTCPListener test 2013-09-29 13:39:55 +02:00
Leonid Bugaev 9908e8d4d9 Update README.md 2013-09-27 23:04:13 +02:00
Sławosz e7b2866ca9 remove nasty debug 2013-09-23 19:44:57 +02:00
Sławosz b562a6b62d test is not working 2013-09-23 19:43:04 +02:00
Sławosz 0ac36b4fbf Merge branch 'master' into save-request-to-reply-later 2013-09-23 19:38:25 +02:00
Sławosz 16ba3d1514 Merge branch 'master' of https://github.com/buger/gor 2013-09-23 19:36:34 +02:00
Sławosz 5b6b8a53c1 better file formating 2013-09-23 19:36:23 +02:00
Sławosz 1a35a2c651 faulty listener test 2013-09-23 19:34:53 +02:00
Leonid Bugaev 5a4c27dc9e Update README.md 2013-09-15 13:12:45 +04:00
Leonid Bugaev e3d1fb4e72 Update CHANGELOG.md 2013-09-15 12:59:48 +04:00
Leonid Bugaev 0233ef7efb Update CHANGELOG.md 2013-09-15 12:59:25 +04:00
Leonid Bugaev 4a68bea49c Fixed tests 2013-09-15 12:58:29 +04:00
Leonid Bugaev fd47cc1144 Update version 2013-09-15 12:49:41 +04:00
Leonid Bugaev 26455e4e52 Updated tests debugging 2013-09-15 12:47:10 +04:00
Leonid Bugaev 5aca51e5b1 Update README.md 2013-09-15 12:39:24 +04:00
Leonid Bugaev af45f8246e Update README.md 2013-09-15 12:38:36 +04:00
Leonid Bugaev 5bae7c5de5 Don't exit on panic 2013-09-15 10:59:15 +04:00
Sławosz 335c86c6e9 log messages 2013-08-28 18:43:36 +02:00
Sławosz f1eedc78a1 debugging and improving 2013-08-28 18:40:32 +02:00
Sławosz cfb65c06ac some temporary debug 2013-08-28 18:39:01 +02:00
Sławosz 02abcfdf74 message logger 2013-08-28 18:38:08 +02:00
Sławosz 1e08ec3953 gofmt 2013-08-28 18:11:31 +02:00
Sławosz db369cfc89 gofmt 2013-08-28 18:10:07 +02:00
Sławosz 54a3d73a98 better name 2013-08-21 18:29:21 +02:00
Sławosz ae792afa5e gofmt test 2013-08-20 18:40:27 +02:00
Sławosz 700523e0c3 proper test for reader writing request to file 2013-08-20 18:38:38 +02:00
Sławosz 95841d597e wrap request buffer around reader 2013-08-20 09:05:41 +02:00
Sławosz Sławiński 5ccebd008d test for saving listened requests to file wip 2013-08-19 20:08:38 +00:00
Sławosz Sławiński e5cb5d959d add flags for file in replay 2013-08-19 20:07:47 +00:00
Sławosz Sławiński 4f7b201ef1 use empty string as default value 2013-08-19 19:24:30 +00:00
Sławosz 6bb24697b2 add flags for file to listener 2013-08-19 19:00:08 +02:00
Sławosz 719fd471f3 failing integration spec 2013-08-19 18:53:28 +02:00
Leonid Bugaev af2d47817b Add error logging for http server 2013-08-16 12:53:16 +04:00
Sławosz Sławiński 2c34f291fb ignore vim files 2013-08-09 21:07:23 +00:00
Leonid Bugaev 041c20ef38 Add concurrency settings to test 2013-08-09 19:25:46 +04:00
Leonid Bugaev d425a7c9a8 Merge pull request #19 from buger/listener_limit
Fixed listener
2013-08-07 06:06:37 -07:00
Leonid Bugaev 528980c87e Fixed listener 2013-08-07 17:05:06 +04:00
Leonid Bugaev c899791d4c Merge pull request #18 from buger/listener_limit
Listener limit
2013-08-06 23:31:57 -07:00
Leonid Bugaev b6cce83b1b Update README.md 2013-08-05 20:59:37 +04:00
Leonid Bugaev 09175eade0 Updated README 2013-08-05 20:51:23 +04:00
Leonid Bugaev 3c84a0c0d9 Remove debugging 2013-08-05 20:48:01 +04:00
Leonid Bugaev 77751af316 Added listener limit option 2013-08-05 20:42:16 +04:00
Leonid Bugaev 00d3103451 Add rate-limit tests 2013-08-05 19:03:06 +04:00
Leonid Bugaev e42c3a96e2 Make tests more modular 2013-08-05 18:42:37 +04:00
Leonid Bugaev a9ca7ea7ff Make tests more configurable 2013-08-05 18:18:39 +04:00
Leonid Bugaev 40a48863fb Refactored tests 2013-08-05 18:05:40 +04:00
Leonid Bugaev 97f2bb7740 Disable redirects
Fix #15
2013-08-05 12:38:31 +04:00
Leonid Bugaev 6ac803454b Fix #15 2013-08-02 20:39:00 +04:00
Leonid Bugaev ec106e4b4e Update README.md 2013-08-02 20:24:11 +04:00
Leonid Bugaev 1f9a1ac3db Update .travis.yml 2013-08-02 20:22:50 +04:00
Leonid Bugaev 4c9cf3fa6a Update .travis.yml 2013-08-02 20:18:53 +04:00
Leonid Bugaev a89f1c75c8 Update .travis.yml 2013-08-02 19:51:16 +04:00
Leonid Bugaev 0bb7e9f7b8 Update .travis.yml 2013-08-02 19:48:57 +04:00
Leonid Bugaev 3b68ae27a6 Update .travis.yml 2013-08-02 19:45:48 +04:00
Leonid Bugaev 476da0f1e6 Update .travis.yml 2013-08-02 19:41:43 +04:00
Leonid Bugaev f6038b26b7 Update .travis.yml 2013-08-02 19:40:11 +04:00
Leonid Bugaev 668e33c128 Update .travis.yml 2013-08-02 19:37:32 +04:00
Leonid Bugaev 4066c24960 Specify go version 2013-08-02 19:32:02 +04:00
Leonid Bugaev 1686c701bf Update .travis.yml 2013-08-02 19:28:37 +04:00
Leonid Bugaev 8fac086d02 Create .travis.yml 2013-08-02 19:25:10 +04:00
Leonid Bugaev 1cc94bc7ac Added some tests 2013-08-02 19:07:39 +04:00
Leonid Bugaev d231d64abb Merge pull request #13 from bobthecow/patch-1
s/coma/comma/g
2013-07-17 21:30:28 -07:00
Justin Hileman 9a3c3f1e6d s/coma/comma/g 2013-07-17 12:26:27 -07:00
Leonid Bugaev c6fa4b8332 Create LICENSE.txt 2013-07-16 17:41:36 +06:00
Leonid Bugaev 85b54627ae Removed unused debugging. Refactored variable names. 2013-06-24 12:23:46 +06:00
Leonid Bugaev 97a5e99c6b Updated version 2013-06-22 20:36:16 +06:00
Leonid Bugaev 9490c743c3 Update README.md 2013-06-22 20:29:34 +06:00
Leonid Bugaev bc65507c8d Merge pull request #10 from buger/use_tcp
Used TCP instead of UDP
2013-06-22 07:26:36 -07:00
Leonid Bugaev 203f282bad Fixed buffer sharing 2013-06-22 19:12:58 +06:00
Leonid Bugaev 88cb352353 Removed unused debugging 2013-06-22 19:12:25 +06:00
Leonid Bugaev 2ba6dacb64 Remove binary 2013-06-22 18:27:57 +06:00
Leonid Bugaev 8defc2062e Updated README 2013-06-22 18:27:33 +06:00
Leonid Bugaev 76e5981f49 Proper conn closing and settings refactoring 2013-06-22 18:16:23 +06:00
Leonid Bugaev 5910409379 Used TCP instead of UDP 2013-06-22 17:24:10 +06:00
Leonid Bugaev 83f16946c7 Fixed docs 2013-06-20 20:16:25 +06:00
Leonid Bugaev 28702cd503 Updated docs 2013-06-20 18:43:31 +06:00
Leonid Bugaev 35f2f71199 Fixed replaying (forgot to add Query to url) 2013-06-19 21:27:05 +06:00
Leonid Bugaev 15f23270df Fixed replaying to multiple addreses 2013-06-19 17:07:48 +06:00
Leonid Bugaev 6ba587e82e Don't block socket reading 2013-06-19 13:34:01 +06:00
Leonid Bugaev cad13f4233 Refactor concurrency to use more channels 2013-06-18 20:32:47 +06:00
Leonid Bugaev fcadc57696 Optimize packet parsing 2013-06-18 17:21:35 +06:00
Leonid Bugaev 28bbb9abf9 Fixed settings and small typo. 2013-06-18 16:20:54 +06:00
Leonid Bugaev 2709bba3c2 Add setting to specify IP to bind 2013-06-18 16:06:46 +06:00
Leonid Bugaev 2e13a7930b More optimizations. Don't acclocat unneded memory. 2013-06-16 17:25:21 +06:00
Leonid Bugaev c62ce5513f Speed optimizations. v0.3.1 2013-06-16 13:45:38 +06:00
Leonid Bugaev 4bcb3e1ca6 Added building from source section
fixed #7
2013-06-14 13:02:54 +06:00
73 changed files with 8643 additions and 852 deletions
+3
View File
@@ -0,0 +1,3 @@
*.tar.gz
gor
gor.test
+17
View File
@@ -0,0 +1,17 @@
*.swp
*.gor
*.out
*.bin
*.gz
*.class
*.test
.idea
*.iml
gor
*.mprof
+6
View File
@@ -0,0 +1,6 @@
language: go
go: 1.6
script: sudo -E bash -c "source /etc/profile && eval '$(gimme 1.6)' && export GOPATH=$HOME/gopath:$GOPATH && go get && GORACE='halt_on_error=1' go test ./... -v -timeout 120s -race"
before_install:
- sudo apt-get install libpcap-dev -y
-5
View File
@@ -1,5 +0,0 @@
v0.3 - 10 Jun 2013
* Use RAW_SOCKETS instead of tcpdump
* Own TCP stack
* All HTTP request types support
* Simplified request parsing
+95
View File
@@ -0,0 +1,95 @@
END-USER LICENSE AGREEMENT
------------------------------------------------------------------------------
IMPORTANT: THIS SOFTWARE END-USER LICENSE AGREEMENT ("EULA") IS A LEGAL AGREEMENT (“Agreement”) BETWEEN YOU (THE CUSTOMER, EITHER AS AN INDIVIDUAL OR, IF PURCHASED OR OTHERWISE ACQUIRED BY OR FOR AN ENTITY, AS AN ENTITY) AND REPLAY SOFTWARE. READ IT CAREFULLY BEFORE COMPLETING THE INSTALLATION PROCESS AND USING GOR PRO AND RELATED SOFTWARE COMPONENTS (“SOFTWARE”). IT PROVIDES A LICENSE TO USE THE SOFTWARE AND CONTAINS WARRANTY INFORMATION AND LIABILITY DISCLAIMERS. BY INSTALLING AND USING THE SOFTWARE, YOU ARE CONFIRMING YOUR ACCEPTANCE OF THE SOFTWARE AND AGREEING TO BECOME BOUND BY THE TERMS OF THIS AGREEMENT.
------------------------------------------------------------------------------
In order to use the Software under this Agreement, you must receive a “Source URL” at the time of purchase, in accordance with the scope of use and other terms specified for each type of Software and as set forth in this Section 1 of this Agreement.
1. License Grant
1.1 General Use. This Agreement grants you a non-exclusive, non-transferable, limited license to the use rights for the Software, without the right to grant sublicenses, subject to the terms and conditions in this Agreement. The Software is licensed, not sold.
1.2 Unlimited Organization License. If you purchased an Organization License (included with the Gor Pro Software), you may install the Software on an unlimited number of Hosts. “Host” means any physical or virtual machine which is controlled by you. You may also run an unlimited number of Workers. “Worker” means a thread within a Gor server process which executes jobs. You may concurrently run the software on an unlimited number of Hosts, with each host running an unlimited number of Workers.
1.3 Limited Organization License. If you purchased an Organization License (included with the Gor Enterprise Software), you may install the Software on an unlimited number of Hosts. “Host” means any physical or virtual machine which is controlled by you. The aggregate number of Workers run by the hosts must not exceed the maximum number of Workers authorized at the time of purchase. “Worker” means a running Gor instance which intercept or replay traffic. In order to run additional Workers, you must purchase an additional allowance from Replay Software.
1.4 Appliance License. If you purchased an Appliance License, you may distribute the Software in any applications, frameworks, or elements (collectively referred to as an “Application” or “Applications”) that you develop using the Software in accordance with this EULA, provided that such distribution does not violate the restrictions set forth in section 3 of this EULA. You must not remove, obscure or interfere with any copyright, acknowledgment, attribution, trademark, warning or disclaimer statement affixed to, incorporated in or otherwise applied in connection with the Software. You are required to ensure that the Software is not reused by or with any applications other than those with which you distribute it as permitted herein. For example, if You install the Software on a customer’s server, that customer is not permitted to use the Software independently of your Application. You must inform Replay Software of your knowledge of any infringing use of the Software by any of your customers. You are liable for compliance by those third parties with the terms and conditions of this EULA. You will not owe Replay Software any royalties for your distribution of the Software in accordance with this EULA.
1.5 Archive Copies. You are entitled to make a reasonable amount of copies of the Software for archival purposes. Each copy must reproduce all copyright and other proprietary rights notices on or in the Software Product.
1.6 Electronic Delivery. All Software and license documentation shall be delivered by electronic means unless otherwise specified on the applicable invoice or at the time of purchase. Software shall be deemed delivered when it is made available for download by you (“Delivery”).
2. Modifications. Replay Software shall provide you with source code so that you can create Modifications of the original software. “Modification” means: (a) any addition to or deletion from the contents of a file included in the original Software or previous Modifications created by You, or (b) any new file that contains any part of the original Software or previous Modifications. While you retain all rights to any original work authored by you as part of the Modifications, We continue to own all copyright and other intellectual property rights in the Software.
3. Restricted Uses.
3.1 You shall not (and shall not allow any third party to): (a) decompile, disassemble, or otherwise reverse engineer the Software or attempt to reconstruct or discover any source code, underlying ideas, algorithms, file formats or programming interfaces of the Software by any means whatsoever (except and only to the extent that applicable law prohibits or restricts reverse engineering restrictions); (b) distribute, sell, sublicense, rent, lease or use the Software for time sharing, hosting, service provider or like purposes, except as expressly permitted under this Agreement; (c) redistribute the Software or Modifications other than by including the Software or a portion thereof within your own product, which must have substantially different functionality than the Software or Modifications and must not allow any third party to use the Software or Modifications, or any portions thereof, for software development or application development purposes; (d) redistribute the Software as part of a product, "appliance" or "virtual server"; (e) redistribute the Software on any server which is not directly under your control; (f) remove any product identification, proprietary, copyright or other notices contained in the Software; (g) modify any part of the Software, create a derivative work of any part of the Software (except as permitted in Section 4), or incorporate the Software, except to the extent expressly authorized in writing by Replay Software; (h) publicly disseminate performance information or analysis (including, without limitation, benchmarks) from any source relating to the Software; (i) utilize any equipment, device, software, or other means designed to circumvent or remove any form of Source URL or copy protection used by Replay Software in connection with the Software, or use the Software together with any authorization code, Source URL, serial number, or other copy protection device not supplied by Replay Software; (j) use the Software to develop a product which is competitive with any Replay Software product offerings; or (k) use unauthorized Source URLS or keycode(s) or distribute or publish Source URLs or keycode(s), except as may be expressly permitted by Replay Software in writing. If your unique Source URL is ever published, Replay Software reserves the right to terminate your access without notice.
3.2 UNDER NO CIRCUMSTANCES MAY YOU USE THE SOFTWARE AS PART OF A PRODUCT OR SERVICE THAT PROVIDES SIMILAR FUNCTIONALITY TO THE SOFTWARE ITSELF.
The Open Source version of the Software (“LGPL Version”) is licensed
under the terms of the GNU Lesser General Public License versions 3.0
(“LGPL”) and not under this EULA.
4. Ownership. Notwithstanding anything to the contrary contained herein, except for the limited license rights expressly provided herein, Replay Software and its suppliers have and will retain all rights, title and interest (including, without limitation, all patent, copyright, trademark, trade secret and other intellectual property rights) in and to the Software and all copies, modifications and derivative works thereof (including any changes which incorporate any of your ideas, feedback or suggestions). You acknowledge that you are obtaining only a limited license right to the Software, and that irrespective of any use of the words “purchase”, “sale” or like terms hereunder no ownership rights are being conveyed to you under this Agreement or otherwise.
5. Fees and Payment. The Software license fees will be due and payable in full as set forth in the applicable invoice or at the time of purchase. If the Software does not function properly within two weeks of purchase, please contact us within those two weeks for a refund. You shall be responsible for all taxes, withholdings, duties and levies arising from the order (excluding taxes based on the net income of Replay Software).
6. Support, Maintenance and Services. Subject to the terms and conditions of this Agreement, as set forth in your invoice, and as set forth on the Gor Pro support page (https://github.com/buger/gor/wiki/Pro-Support), support and maintenance services may be included with the purchase of your license subscription.
7. Term of Agreement.
7.1 Term. This Agreement is effective as of the Delivery of the Software and expires at such time as all license and service subscriptions hereunder have expired in accordance with their own terms (the “Term”). For clarification, the term of your license under this Agreement may be perpetual, limited for Evaluation Version, or designated as a fixed-term license in the Invoice, and shall be specified at your time of purchase. Either party may terminate this Agreement (including all related Invoices) if the other party: (a) fails to cure any material breach of this Agreement within thirty (30) days after written notice of such breach, provided that Replay Software may terminate this Agreement immediately upon any breach of Section 3 or if you exceed any other restrictions contained in Section 1, unless otherwise specified in this agreement; (b) ceases operation without a successor; or (c) seeks protection under any bankruptcy, receivership, trust deed, creditors arrangement, composition or comparable proceeding, or if any such proceeding is instituted against such party (and not dismissed within sixty (60) days)). Termination is not an exclusive remedy and the exercise by either party of any remedy under this Agreement will be without prejudice to any other remedies it may have under this Agreement, by law, or otherwise.
7.2 Termination. Upon any termination of this Agreement, you shall cease any and all use of any Software and destroy all copies thereof.
7.3 Expiration of License. Upon the expiration of any term under this Agreement, (a) all Software updates and services pursuant to the license shall cease, (b) you may only continue to run existing installations of the Software, (c) you may not install the Software on any additional Hosts, and (d) any new installation of the Software shall require the purchase of a new license subscription from Replay Software.
8. Disclaimer of Warranties. The Software is provided "as is," with all faults, defects and errors, and without warranty of any kind. Replay Software does not warrant that the Software will be free of bugs, errors, viruses or other defects, and Replay Software shall have no liability of any kind for the use of or inability to use the Software, the Software content or any associated service, and you acknowledge that it is not technically practicable for Replay Software to do so.
To the maximum extent permitted by applicable law, Replay Software disclaims all warranties, express, implied, arising by law or otherwise, regarding the Software, the Software content and their respective performance or suitability for your intended use, including without limitation any implied warranty of merchantability, fitness for a particular purpose.
9. Limitation of Liability.
In no event will Replay Software be liable for any direct, indirect, consequential, incidental, special, exemplary, or punitive damages or liabilities whatsoever arising from or relating to the Software, the Software content or this Agreement, whether based on contract, tort (including negligence), strict liability or other theory, even if Replay Software has been advised of the possibility of such damages.
In no event will Replay Software' liability exceed the Software license price as indicated in the invoice. The existence of more than one claim will not enlarge or extend this limit.
10. Remedies. Your exclusive remedy and Replay Software’ entire liability for breach of this Agreement shall be limited, at Replay Software’ sole and exclusive discretion, to (a) replacement of any defective software or documentation; or (b) refund of the license fee paid to Replay Software, payable in accordance with Replay Software' refund policy.
11. Acknowledgements.
11.1 Consent to the Use of Data. You agree that Replay Software and its affiliates may collect and use technical information gathered as part of the product support services. Replay Software may use this information solely to improve products and services and will not disclose this information in a form that personally identifies you.
11.2 Verification. We or a certified auditor acting on our behalf, may, upon its reasonable request and at its expense, audit you with respect to the use of the Software. Such audit may be conducted by mail, electronic means or through an in-person visit to your place of business. Any such in-person audit shall be conducted during regular business hours at your facilities and shall not unreasonably interfere with your business activities. We shall not remove, copy, or redistribute any electronic material during the course of an audit. If an audit reveals that you are using the Software in a way that is in material violation of the terms of the EULA, then you shall pay our reasonable costs of conducting the audit. In the case of a material violation, you agree to pay Us any amounts owing that are attributable to the unauthorized use. In the alternative, We reserve the right, at our sole option, to terminate the licenses for the Software.
11.3 Government End Users. If the Software and related documentation are supplied to or purchased by or on behalf of the United States Government, then the Software is deemed to be "commercial software" as that term is used in the Federal Acquisition Regulation system. Rights of the United States shall not exceed the minimum rights set forth in FAR 52.227-19 for "restricted computer software". All other terms and conditions of this Agreement apply.
12. Third Party Software. Examples included in Software may provide links to third party libraries or code (collectively “Third Party Software”) to implement various functions. Third Party Software does not comprise part of the Software. In some cases, access to Third Party Software may be included along with the Software delivery as a convenience for demonstration purposes. Such source code and libraries may be included in the “…/examples” source tree delivered with the Software and do not comprise the Software. Licensee acknowledges (1) that some part of Third Party Software may require additional licensing of copyright and patents from the owners of such, and (2) that distribution of any of the Software referencing or including any portion of a Third Party Software may require appropriate licensing from such third parties.
13. Miscellaneous
13.1 Entire Agreement. This Agreement sets forth our entire agreement with respect to the Software and the subject matter hereof and supersedes all prior and contemporaneous understandings and agreements whether written or oral.
13.2 Amendment. Replay Software reserves the right, in its sole discretion, to amend this Agreement from time. Amendments to this Agreement can be located at: https://github.com/buger/gor/blob/master/COMM-LICENSE.
13.3 Assignment. You may not assign this Agreement or any of its rights under this Agreement without the prior written consent of Replay Software and any attempted assignment without such consent shall be void.
13.4 Export Compliance. You agree to comply with all applicable laws and regulations, including laws, regulations, orders or other restrictions on export, re-export or redistribution of software.
13.5 Indemnification. You agree to defend, indemnify, and hold harmless Replay Software from and against any lawsuits, claims, losses, damages, fines and expenses (including attorneys' fees and costs) arising out of your use of the Software or breach of this Agreement.
13.6 Governing Law. This Agreement is governed by the laws of the State of Oregon and the United States without regard to conflicts of laws provisions thereof, and without regard to the United Nations Convention on the International Sale of Goods or the Uniform Computer Information Transactions Act, as currently enacted by any jurisdiction or as may be codified or amended from time to time by any jurisdiction. The jurisdiction and venue for actions related to the subject matter hereof shall be the state of Oregon and United States federal courts located in Portland, Oregon, and both parties hereby submit to the personal jurisdiction of such courts.
13.7 Attorneys’ Fees and Costs. The prevailing party in any action to enforce this Agreement will be entitled to recover its attorneys’ fees and costs in connection with such action.
13.8 Severability. If any provision of this Agreement is held by a court of competent jurisdiction to be invalid, illegal, or unenforceable, the remainder of this Agreement will remain in full force and effect.
13.9 Waiver. Failure or neglect by either party to enforce at any time any of the provisions of this licence Agreement shall not be construed or deemed to be a waiver of that party's rights under this Agreement.
13.10 Headings. The headings of sections and paragraphs of this Agreement are for convenience of reference only and are not intended to restrict, affect or be of any weight in the interpretation or construction of the provisions of such sections or paragraphs.
14. Contact Information. If you have any questions about this EULA, or if you want to contact Replay Software for any reason, please direct correspondence to info@gortool.com
+22
View File
@@ -0,0 +1,22 @@
FROM golang:latest
RUN apt-get update && apt-get install ruby vim-common -y
# Install Java for middleware testing
RUN echo "deb http://ppa.launchpad.net/webupd8team/java/ubuntu trusty main" | tee /etc/apt/sources.list.d/webupd8team-java.list
RUN echo "deb-src http://ppa.launchpad.net/webupd8team/java/ubuntu trusty main" | tee -a /etc/apt/sources.list.d/webupd8team-java.list
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys EEA14886
RUN apt-get update -y
RUN echo oracle-java7-installer shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections
RUN apt-get install oracle-java8-installer -y
RUN apt-get install flex bison -y
RUN wget http://www.tcpdump.org/release/libpcap-1.7.4.tar.gz && tar xzf libpcap-1.7.4.tar.gz && cd libpcap-1.7.4 && ./configure && make install
RUN go get github.com/google/gopacket
RUN go get -u github.com/golang/lint/golint
WORKDIR /go/src/github.com/buger/gor/
ADD . /go/src/github.com/buger/gor/
RUN javac -cp /tmp/commons-io-2.4/commons-io-2.4.jar ./examples/middleware/echo.java
RUN go get
+65
View File
@@ -0,0 +1,65 @@
gor & elasticsearch
===================
Prerequisites
-------------
- elasticsearch
- kibana (Get it here: http://www.elasticsearch.org/overview/kibana/)
- gor
elasticsearch
-------------
The default elasticsearch configuration is just fine for most workloads. You won't need clustering, sharding or something like that.
In this example we're installing it on our gor replay server which gives us the elasticsearch listener on _http://localhost:9200_
kibana
------
Kibana (elasticsearch analytics web-ui) is just as simple.
Download it, extract it and serve it via a simple webserver.
(Could be nginx or apache)
You could also use a shell, ```cd``` into the kibana directory and start a little quick and dirty python webserver with:
```
python -m SimpleHTTPServer 8000
```
In this example we're also choosing the gor replay server as our kibana host. If you choose a different server you'll have to point kibana to your elasticsearch host.
gor
---
Start your gor replay server with elasticsearch option:
```
./gor --input-raw :8000 --output-http http://staging.com --output-http-elasticsearch localhost:9200/gor
```
(You don't have to create the index upfront. That will be done for you automatically)
Now visit your kibana url, load the predefined dashboard from the gist https://gist.github.com/gottwald/b2c875037f24719a9616 and watch the data rush in.
Troubleshooting
---------------
The replay process may complain about __too many open files__.
That's because your typical linux shell has a small open files soft limit at 1024.
You can easily raise that when you do this before starting your _gor replay_ process:
```
ulimit -n 64000
```
Please be aware, this is not a permanent setting. It's just valid for the following jobs you start from that shell.
We reached the 1024 limit in our tests with a ubuntu box replaying about 9000 requests per minute. (We had very slow responses there, should be way more with fast responses)
+7
View File
@@ -0,0 +1,7 @@
Gor is an Open Source project licensed under the terms of
the LGPLv3 license. Please see <http://www.gnu.org/licenses/lgpl-3.0.html>
for license text.
Gor Pro has a commercial-friendly license allowing private forks
and modifications of Gor. Please see http://gortool.com/#pro for
more detail. You can find the commercial license terms in COMM-LICENSE.
+86
View File
@@ -0,0 +1,86 @@
SOURCE = emitter.go gor.go gor_stat.go input_dummy.go input_file.go input_raw.go input_tcp.go limiter.go output_dummy.go output_file.go input_http.go output_http.go output_tcp.go plugins.go settings.go test_input.go elasticsearch.go http_modifier.go http_modifier_settings.go http_client.go middleware.go protocol.go output_file_settings.go
SOURCE_PATH = /go/src/github.com/buger/gor/
PORT = 8000
FADDR = :8000
RUN = docker run -v `pwd`:$(SOURCE_PATH) -p 0.0.0.0:$(PORT):$(PORT) -t -i gor
BENCHMARK = BenchmarkRAWInput
TEST = TestRawListenerBench
VERSION = DEV-$(shell date +%s)
LDFLAGS = -ldflags "-X main.VERSION=$(VERSION) -extldflags \"-static\""
MAC_LDFLAGS = -ldflags "-X main.VERSION=$(VERSION)"
FADDR = ":8000"
release: release-x64 release-mac
release-x64:
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i gor go build $(LDFLAGS) && tar -czf gor_$(VERSION)_x64.tar.gz gor && rm gor
release-x86:
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=386 -i gor go build $(LDFLAGS) && tar -czf gor_$(VERSION)_x86.tar.gz gor && rm gor
release-mac:
go build $(MAC_LDFLAGS) && tar -czf gor_$(VERSION)_mac.tar.gz gor && rm gor
build:
docker build -t gor .
profile:
go build && ./gor --output-http="http://localhost:9000" --input-dummy 0 --input-raw :9000 --input-http :9000 --memprofile=./mem.out --cpuprofile=./cpu.out --stats --output-http-stats --output-http-timeout 100ms
lint:
$(RUN) golint $(PKG)
race:
$(RUN) go test ./... $(ARGS) -v -race -timeout 15s
test:
$(RUN) go test ./. -timeout 60s $(LDFLAGS) $(ARGS) -v
test_all:
$(RUN) go test ./... -timeout 60s $(LDFLAGS) $(ARGS) -v
testone:
$(RUN) go test ./... -timeout 4s $(LDFLAGS) -run $(TEST) $(ARGS) -v
cover:
$(RUN) go test $(ARGS) -race -v -timeout 15s -coverprofile=coverage.out
go tool cover -html=coverage.out
fmt:
$(RUN) gofmt -w -s ./..
vet:
$(RUN) go vet
bench:
$(RUN) go test $(LDFLAGS) -v -run NOT_EXISTING -bench $(BENCHMARK) -benchtime 5s
profile_test:
$(RUN) go test $(LDFLAGS) -run $(TEST) ./raw_socket_listener/. $(ARGS) -memprofile mem.mprof -cpuprofile cpu.out
$(RUN) go test $(LDFLAGS) -run $(TEST) ./raw_socket_listener/. $(ARGS) -c
# Used mainly for debugging, because docker container do not have access to parent machine ports
run:
$(RUN) go run $(LDFLAGS) $(SOURCE) --input-dummy=0 --output-http="http://localhost:9000" --input-raw-track-response --input-raw 127.0.0.1:9000 --input-http 127.0.0.1:9000 --verbose --debug --middleware "./examples/middleware/echo.sh" --output-file requests.gor
run-2:
sudo -E go run $(SOURCE) --input-dummy="" --output-tcp localhost:27001 --verbose --debug
run-3:
sudo -E go run $(SOURCE) --input-tcp :27001 --output-stdout
run-arg:
sudo -E go run $(SOURCE) $(ARGS)
file-server:
go run $(SOURCE) file-server $(FADDR)
record:
$(RUN) go run $(SOURCE) --input-dummy=0 --output-file=requests.gor --verbose --debug
replay:
$(RUN) go run $(SOURCE) --input-file=requests.bin --output-tcp=:9000 --verbose -h
bash:
$(RUN) /bin/bash
+4
View File
@@ -0,0 +1,4 @@
web: python -m SimpleHTTPServer 8000
replayed_web: python -m SimpleHTTPServer 8001
listener: sudo -E go run ./bin/gor.go --input-raw :8000 --output-tcp :8002 --verbose
replay: go run ./bin/gor.go --input-tcp :8002 --output-http localhost:8001 --verbose
+73 -88
View File
@@ -1,88 +1,73 @@
## About
Gor is a simple http traffic replication tool written in Go.
Its main goal is to replay traffic from production servers to staging and dev environments.
Now you can test your code on real user sessions in an automated and repeatable fashion.
**No more falling down in production!**
Gor consists of 2 parts: listener and replay servers.
The listener server catches http traffic from a given port in real-time
and sends it to the replay server via UDP.
The replay server forwards traffic to a given address.
![Diagram](http://i.imgur.com/zZCFPCY.png)
## Basic example
```bash
# Run on servers where you want to catch traffic. You can run it on each `web` machine.
sudo gor listen -p 80 -r replay.server.local:28020
# Replay server (replay.server.local).
gor replay -f http://staging.server
```
## Advanced use
### Rate limiting
The replay server supports rate limiting. It can be useful if you want
forward only part of production traffic and not overload your staging
environment. You can specify your desired requests per second using the
"|" operator after the server address:
```
# staging.server will not get more than 10 requests per second
gor replay -f "http://staging.server|10"
```
### Forward to multiple addresses
You can forward traffic to multiple endpoints. Just separate the addresses by coma.
```
gor replay -f "http://staging.server|10,http://dev.server|5"
```
## Additional help
```
$ gor listen -h
Usage of ./bin/gor-linux:
-i="any": By default it try to listen on all network interfaces.To get list of interfaces run `ifconfig`
-p=80: Specify the http server port whose traffic you want to capture
-r="localhost:28020": Address of replay server.
```
```
$ gor replay -h
Usage of ./bin/gor-linux:
-f="http://localhost:8080": http address to forward traffic.
You can limit requests per second by adding `|#{num}` after address.
If you have multiple addresses with different limits. For example: http://staging.example.com|100,http://dev.example.com|10
-ip="0.0.0.0": ip addresses to listen on
-p=28020: specify port number
```
## Pre-build binaries
[Download binaries (linux 32/64, darwin)](https://drive.google.com/folderview?id=0B46uay48NwcfWFowc1E4a1BISVU&usp=sharing)
## FAQ
### Why does the `gor listener` requires sudo or root access?
Listener works by sniffing traffic from a given port. It's accessible
only by using sudo or root access.
### Do you support all http request types?
Yes. ~~Right now it supports only "GET" requests.~~
## TODO
Use buffering for request throttling instead of simple rate limiting.
Better stats
Optimize for load testing cases
[![GitHub release](https://img.shields.io/github/release/buger/gor.svg?maxAge=3600)](https://github.com/buger/gor/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)
![Go Replay](http://i.imgur.com/ZG2ki5n.png)
## About
Gor is an open-source tool for capturing and replaying live HTTP traffic into a test environment in order to continuously test your system with real data. It can be used to increase confidence in code deployments, configuration changes and infrastructure changes.
Now you can test your code on real user sessions in an automated and repeatable fashion.
**No more falling down in production!**
Here is basic workflow: The listener server catches http traffic and sends it to the replay server or saves to file. The replay server forwards traffic to a given address.
![Diagram](http://i.imgur.com/9mqj2SK.png)
## Installation
Download latest binary from https://github.com/buger/gor/releases or [compile by yourself](https://github.com/buger/gor/wiki/Compilation).
## Getting started
The most basic setup will be `sudo ./gor --input-raw :8000 --output-stdout` which acts like tcpdump.
If you already have test environment you can start replaying: `sudo ./gor --input-raw :8000 --output-http http://staging.env`.
See the our wiki and especially [Getting started](https://github.com/buger/gor/wiki/Getting-Started) wiki page for more info.
## Want to Upgrade?
I also sell Gor Pro, extensions to Gor which provide more features, a commercial-friendly license and allow you to support high quality open source development all at the same time. Please see the Gor [homepage](https://gortool.com/) for more detail.
Subscribe to the [quarterly newsletter](https://tinyletter.com/gor) to stay informed about the latest features and changes to Gor and its bigger siblings.
## Problems?
If you have a problem, please review the [FAQ](https://github.com/buger/gor/wiki/FAQ) and [Troubleshooting](https://github.com/buger/gor/wiki/Troubleshooting) wiki pages. Searching the [issues](https://github.com/buger/gor/issues) for your problem is also a good idea.
All bug-reports and suggestions should go though Github Issues or our [Google Group](https://groups.google.com/forum/#!forum/gor-users) (you can just send email to gor-users@googlegroups.com).
If you have a private question feel free to send email to support@gortool.com.
Useful resources:
* Product documentation is in the [wiki](http://github.com/buger/gor/wiki).
* Release announcements are made to the [@buger](http://twitter.com/buger) Twitter account and our [newsleter](https://tinyletter.com/gor)
If you need commercial support read more about Pro and Enterprise versions at our site [https://gortool.com/](https://gortool.com/)
## Contributing
1. Fork it
2. Create your feature branch (git checkout -b my-new-feature)
3. Commit your changes (git commit -am 'Added some feature')
4. Push to the branch (git push origin my-new-feature)
5. Create new Pull Request
## Companies using Gor
* [GOV.UK](https://www.gov.uk) - UK Government Digital Service
* [theguardian.com](http://theguardian.com) - Most popular online newspaper in the UK
* [TomTom](http://www.tomtom.com/) - Global leader in navigation, traffic and map products, GPS Sport Watches and fleet management solutions.
* [3SCALE](http://www.3scale.net/) - API infrastructure to manage your APIs for internal or external users
* [Optionlab](http://www.opinionlab.com) - Optimize customer experience and drive engagement across multiple channels
* [TubeMogul](http://tubemogul.com) - Software for Brand Advertising
* [Videology](http://www.videologygroup.com/) - Video advertising platform
* [ForeksMobile](http://foreksmobile.com/) - One of the leading financial application development company in Turkey
* [Granify](http://granify.com) - AI backed SaaS solution that enables online retailers to maximise their sales
* And many more!
If you are using Gor we are happy add you to the list and share your story, just write to: hello@gortool.com
## Author
Leonid Bugaev, [@buger](https://twitter.com/buger), https://leonsbox.com
+40
View File
@@ -0,0 +1,40 @@
// Package byteutils probvides helpers for working with byte slices
package byteutils
// Cut elements from slice for a given range
func Cut(a []byte, from, to int) []byte {
copy(a[from:], a[to:])
a = a[:len(a)-to+from]
return a
}
// Insert new slice at specified position
func Insert(a []byte, i int, b []byte) []byte {
a = append(a, make([]byte, len(b))...)
copy(a[i+len(b):], a[i:])
copy(a[i:i+len(b)], b)
return a
}
// Replace function unlike bytes.Replace allows you to specify range
func Replace(a []byte, from, to int, new []byte) []byte {
lenDiff := len(new) - (to - from)
if lenDiff > 0 {
// Extend if new segment bigger
a = append(a, make([]byte, lenDiff)...)
copy(a[to+lenDiff:], a[to:])
copy(a[from:from+len(new)], new)
return a
} else if lenDiff < 0 {
copy(a[from:], new)
copy(a[from+len(new):], a[to:])
return a[:len(a)+lenDiff]
} else { // same size
copy(a[from:], new)
return a
}
}
+32
View File
@@ -0,0 +1,32 @@
package byteutils
import (
"bytes"
"testing"
)
func TestCut(t *testing.T) {
if !bytes.Equal(Cut([]byte("123456"), 2, 4), []byte("1256")) {
t.Error("Should properly cut")
}
}
func TestInsert(t *testing.T) {
if !bytes.Equal(Insert([]byte("123456"), 2, []byte("abcd")), []byte("12abcd3456")) {
t.Error("Should insert into middle of slice")
}
}
func TestReplace(t *testing.T) {
if !bytes.Equal(Replace([]byte("123456"), 2, 4, []byte("ab")), []byte("12ab56")) {
t.Error("Should replace when same length")
}
if !bytes.Equal(Replace([]byte("123456"), 2, 4, []byte("abcd")), []byte("12abcd56")) {
t.Error("Should replace when replacement length bigger")
}
if !bytes.Equal(Replace([]byte("123456"), 2, 5, []byte("ab")), []byte("12ab6")) {
t.Error("Should replace when replacement length bigger")
}
}
+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"
+153
View File
@@ -0,0 +1,153 @@
package main
import (
"encoding/json"
"github.com/buger/elastigo/api"
"github.com/buger/elastigo/core"
"github.com/buger/gor/proto"
"log"
"regexp"
"time"
)
type ESUriErorr struct{}
func (e *ESUriErorr) Error() string {
return "Wrong ElasticSearch URL format. Expected to be: host:port/index_name"
}
type ESPlugin struct {
Active bool
ApiPort string
Host string
Index string
indexor *core.BulkIndexer
done chan bool
}
type ESRequestResponse struct {
ReqUrl []byte `json:"Req_URL"`
ReqMethod []byte `json:"Req_Method"`
ReqUserAgent []byte `json:"Req_User-Agent"`
ReqAcceptLanguage []byte `json:"Req_Accept-Language,omitempty"`
ReqAccept []byte `json:"Req_Accept,omitempty"`
ReqAcceptEncoding []byte `json:"Req_Accept-Encoding,omitempty"`
ReqIfModifiedSince []byte `json:"Req_If-Modified-Since,omitempty"`
ReqConnection []byte `json:"Req_Connection,omitempty"`
ReqCookies []byte `json:"Req_Cookies,omitempty"`
RespStatus []byte `json:"Resp_Status"`
RespStatusCode []byte `json:"Resp_Status-Code"`
RespProto []byte `json:"Resp_Proto,omitempty"`
RespContentLength []byte `json:"Resp_Content-Length,omitempty"`
RespContentType []byte `json:"Resp_Content-Type,omitempty"`
RespTransferEncoding []byte `json:"Resp_Transfer-Encoding,omitempty"`
RespContentEncoding []byte `json:"Resp_Content-Encoding,omitempty"`
RespExpires []byte `json:"Resp_Expires,omitempty"`
RespCacheControl []byte `json:"Resp_Cache-Control,omitempty"`
RespVary []byte `json:"Resp_Vary,omitempty"`
RespSetCookie []byte `json:"Resp_Set-Cookie,omitempty"`
Rtt int64 `json:"RTT"`
Timestamp time.Time
}
// Parse ElasticSearch URI
//
// Proper format is: host:port/index_name
func parseURI(URI string) (err error, host string, port string, index string) {
rURI := regexp.MustCompile("(.+):([0-9]+)/(.+)")
match := rURI.FindAllStringSubmatch(URI, -1)
if len(match) == 0 {
err = new(ESUriErorr)
} else {
host = match[0][1]
port = match[0][2]
index = match[0][3]
}
return
}
func (p *ESPlugin) Init(URI string) {
var err error
err, p.Host, p.ApiPort, p.Index = parseURI(URI)
if err != nil {
log.Fatal("Can't initialize ElasticSearch plugin.", err)
}
api.Domain = p.Host
api.Port = p.ApiPort
p.indexor = core.NewBulkIndexerErrors(50, 60)
p.done = make(chan bool)
p.indexor.Run(p.done)
// Only start the ErrorHandler goroutine when in verbose mode
// no need to burn ressources otherwise
// go p.ErrorHandler()
log.Println("Initialized Elasticsearch Plugin")
return
}
func (p *ESPlugin) IndexerShutdown() {
p.done <- true
return
}
func (p *ESPlugin) ErrorHandler() {
for {
errBuf := <-p.indexor.ErrorChannel
log.Println(errBuf.Err)
}
}
func (p *ESPlugin) RttDurationToMs(d time.Duration) int64 {
sec := d / time.Second
nsec := d % time.Second
fl := float64(sec) + float64(nsec)*1e-6
return int64(fl)
}
func (p *ESPlugin) ResponseAnalyze(req, resp []byte, start, stop time.Time) {
if len(resp) == 0 {
// nil http response - skipped elasticsearch export for this request
return
}
t := time.Now()
rtt := p.RttDurationToMs(stop.Sub(start))
esResp := ESRequestResponse{
ReqUrl: proto.Path(req),
ReqMethod: proto.Method(req),
ReqUserAgent: proto.Header(req, []byte("User-Agent")),
ReqAcceptLanguage: proto.Header(req, []byte("Accept-Language")),
ReqAccept: proto.Header(req, []byte("Accept")),
ReqAcceptEncoding: proto.Header(req, []byte("Accept-Encoding")),
ReqIfModifiedSince: proto.Header(req, []byte("If-Modified-Since")),
ReqConnection: proto.Header(req, []byte("Connection")),
ReqCookies: proto.Header(req, []byte("Cookie")),
RespStatus: proto.Status(resp),
RespStatusCode: proto.Status(resp),
RespProto: proto.Method(resp),
RespContentLength: proto.Header(resp, []byte("Content-Length")),
RespContentType: proto.Header(resp, []byte("Content-Type")),
RespTransferEncoding: proto.Header(resp, []byte("Transfer-Encoding")),
RespContentEncoding: proto.Header(resp, []byte("Content-Encoding")),
RespExpires: proto.Header(resp, []byte("Expires")),
RespCacheControl: proto.Header(resp, []byte("Cache-Control")),
RespVary: proto.Header(resp, []byte("Vary")),
RespSetCookie: proto.Header(resp, []byte("Set-Cookie")),
Rtt: rtt,
Timestamp: t,
}
j, err := json.Marshal(&esResp)
if err != nil {
log.Println(err)
} else {
p.indexor.Index(p.Index, "RequestResponse", "", "", &t, j, true)
}
return
}
+114
View File
@@ -0,0 +1,114 @@
package main
import (
"bytes"
"io"
"time"
)
// Start initialize loop for sending data from inputs to outputs
func Start(stop chan int) {
if Settings.middleware != "" {
middleware := NewMiddleware(Settings.middleware)
for _, in := range Plugins.Inputs {
middleware.ReadFrom(in)
}
// We going only to read responses, so using same ReadFrom method
for _, out := range Plugins.Outputs {
if r, ok := out.(io.Reader); ok {
middleware.ReadFrom(r)
}
}
go CopyMulty(middleware, Plugins.Outputs...)
} else {
for _, in := range Plugins.Inputs {
go CopyMulty(in, Plugins.Outputs...)
}
}
for {
select {
case <-stop:
pluginMu.Lock()
for _, p := range Plugins.All {
if cp, ok := p.(io.Closer); ok {
cp.Close()
}
}
pluginMu.Unlock()
return
case <-time.After(100 * time.Millisecond):
}
}
}
// CopyMulty copies from 1 reader to multiple writers
func CopyMulty(src io.Reader, writers ...io.Writer) (err error) {
buf := make([]byte, 5*1024*1024)
wIndex := 0
modifier := NewHTTPModifier(&Settings.modifierConfig)
for {
nr, er := src.Read(buf)
if nr > 0 && len(buf) > nr {
payload := buf[:nr]
_maxN := nr
if nr > 500 {
_maxN = 500
}
if Settings.debug {
Debug("[EMITTER] input:", string(payload[0:_maxN]), nr, "from:", src)
}
if modifier != nil && isRequestPayload(payload) {
headSize := bytes.IndexByte(payload, '\n') + 1
body := payload[headSize:]
originalBodyLen := len(body)
body = modifier.Rewrite(body)
// If modifier tells to skip request
if len(body) == 0 {
continue
}
if originalBodyLen != len(body) {
payload = append(payload[:headSize], body...)
}
if Settings.debug {
Debug("[EMITTER] Rewrittern input:", len(payload), "First 500 bytes:", string(payload[0:_maxN]))
}
}
if Settings.splitOutput {
// Simple round robin
writers[wIndex].Write(payload)
wIndex++
if wIndex >= len(writers) {
wIndex = 0
}
} else {
for _, dst := range writers {
dst.Write(payload)
}
}
}
if er == io.EOF {
break
}
if er != nil {
err = er
break
}
}
return err
}
+99
View File
@@ -0,0 +1,99 @@
package main
import (
"io"
"sync"
"sync/atomic"
"testing"
)
func TestEmitter(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
output := NewTestOutput(func(data []byte) {
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
for i := 0; i < 1000; i++ {
wg.Add(1)
input.EmitGET()
}
wg.Wait()
close(quit)
}
func TestEmitterRoundRobin(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
var counter1, counter2 int32
output1 := NewTestOutput(func(data []byte) {
atomic.AddInt32(&counter1, 1)
wg.Done()
})
output2 := NewTestOutput(func(data []byte) {
atomic.AddInt32(&counter2, 1)
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output1, output2}
Settings.splitOutput = true
go Start(quit)
for i := 0; i < 1000; i++ {
wg.Add(1)
input.EmitGET()
}
wg.Wait()
close(quit)
if counter1 == 0 || counter2 == 0 {
t.Errorf("Round robin should split traffic equally: %d vs %d", counter1, counter2)
}
Settings.splitOutput = false
}
func BenchmarkEmitter(b *testing.B) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
output := NewTestOutput(func(data []byte) {
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
b.ResetTimer()
for i := 0; i < b.N; i++ {
wg.Add(1)
input.EmitGET()
}
wg.Wait()
close(quit)
}
+27
View File
@@ -0,0 +1,27 @@
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class echo {
public static void main(String[] args) {
if(args != null){
for(String arg : args){
System.out.println(arg);
}
}
BufferedReader stdin = new BufferedReader(new InputStreamReader(
System.in));
String line = null;
try {
while ((line = stdin.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
}
}
}
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env ruby
# encoding: utf-8
while data = STDIN.gets # continiously read line from STDIN
next unless data
data = data.chomp # remove end of line symbol
decoded = [data].pack("H*") # decode base64 encoded request
# dedoded value is raw HTTP payload, example:
#
# POST /post HTTP/1.1
# Content-Length: 7
# Host: www.w3.org
#
# a=1&b=2"
encoded = decoded.unpack("H*").first # encoding back to base64
# Emit request back
# You can skip this if want to filter out request
STDOUT.puts encoded
end
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
#
# `xxd` utility included into vim-common package
# It allow hex decoding/encoding
#
# This example may broke if you request contains `null` string, you may consider using pipes instead.
# See: https://github.com/buger/gor/issues/309
#
function log {
# Logging to stderr, because stdout/stdin used for data transfer
>&2 echo "[DEBUG][ECHO] $1"
}
while read line; do
decoded=$(echo -e "$line" | xxd -r -p)
header=$(echo -e "$decoded" | head -n +1)
payload=$(echo -e "$decoded" | tail -n +2)
encoded=$(echo -e "$header\n$payload" | xxd -p | tr -d "\\n")
log ""
log "==================================="
case ${header:0:1} in
"1")
log "Request type: Request"
;;
"2")
log "Request type: Original Response"
;;
"3")
log "Request type: Replayed Response"
;;
*)
log "Unknown request type $header"
esac
echo "$encoded"
log "==================================="
log "Original data: $line"
log "Decoded request: $decoded"
log "Encoded data: $encoded"
done;
+119
View File
@@ -0,0 +1,119 @@
/*
This middleware made for auth system that randomly generate access tokens, which used later for accessing secure content. Since there is no pre-defined token value, naive approach without middleware (or if middleware use only request payloads) will fail, because replayed server have own tokens, not synced with origin. To fix this, our middleware should take in account responses of replayed and origin server, store `originalToken -> replayedToken` aliases and rewrite all requests using this token to use replayed alias. See `middleware_test.go#TestTokenMiddleware` test for examples of using this middleware.
How middleware works:
Original request +--------------+
+-------------+----------STDIN---------->+ |
| Gor input | | Middleware |
+-------------+----------STDIN---------->+ |
Original response +------+---+---+
| ^
+-------------+ Modified request v |
| Gor output +<---------STDOUT-----------------+ |
+-----+-------+ |
| |
| Replayed response |
+------------------STDIN----------------->----+
*/
package main
import (
"bufio"
"bytes"
"encoding/hex"
"fmt"
"github.com/buger/gor/proto"
"os"
)
// requestID -> originalToken
var originalTokens map[string][]byte
// originalToken -> replayedToken
var tokenAliases map[string][]byte
func main() {
originalTokens = make(map[string][]byte)
tokenAliases = make(map[string][]byte)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
encoded := scanner.Bytes()
buf := make([]byte, len(encoded)/2)
hex.Decode(buf, encoded)
process(buf)
}
}
func process(buf []byte) {
// First byte indicate payload type, possible values:
// 1 - Request
// 2 - Response
// 3 - ReplayedResponse
payloadType := buf[0]
headerSize := bytes.IndexByte(buf, '\n') + 1
header := buf[:headerSize-1]
// Header contains space separated values of: request type, request id, and request start time (or round-trip time for responses)
meta := bytes.Split(header, []byte(" "))
// For each request you should receive 3 payloads (request, response, replayed response) with same request id
reqID := string(meta[1])
payload := buf[headerSize:]
Debug("Received payload:", string(buf))
switch payloadType {
case '1': // Request
if bytes.Equal(proto.Path(payload), []byte("/token")) {
originalTokens[reqID] = []byte{}
Debug("Found token request:", reqID)
} else {
token, vs, _ := proto.PathParam(payload, []byte("token"))
if vs != -1 { // If there is GET token param
if alias, ok := tokenAliases[string(token)]; ok {
// Rewrite original token to alias
payload = proto.SetPathParam(payload, []byte("token"), alias)
// Copy modified payload to our buffer
buf = append(buf[:headerSize], payload...)
}
}
}
// Emitting data back
os.Stdout.Write(encode(buf))
case '2': // Original response
if _, ok := originalTokens[reqID]; ok {
// Token is inside response body
secureToken := proto.Body(payload)
originalTokens[reqID] = secureToken
Debug("Remember origial token:", string(secureToken))
}
case '3': // Replayed response
if originalToken, ok := originalTokens[reqID]; ok {
delete(originalTokens, reqID)
secureToken := proto.Body(payload)
tokenAliases[string(originalToken)] = secureToken
Debug("Create alias for new token token, was:", string(originalToken), "now:", string(secureToken))
}
}
}
func encode(buf []byte) []byte {
dst := make([]byte, len(buf)*2+1)
hex.Encode(dst, buf)
dst[len(dst)-1] = '\n'
return dst
}
func Debug(args ...interface{}) {
fmt.Fprint(os.Stderr, "[DEBUG][TOKEN-MOD] ")
fmt.Fprintln(os.Stderr, args...)
}
+99 -24
View File
@@ -1,46 +1,121 @@
// Gor is simple http traffic replication tool written in Go. Its main goal to replay traffic from production servers to staging and dev environments.
// Now you can test your code on real user sessions in an automated and repeatable fashion.
//
// Gor consists of 2 parts: listener and replay servers.
// Listener catch http traffic from given port in real-time and send it to replay server via UDP. Replay server forwards traffic to given address.
package main
import (
"flag"
"fmt"
"github.com/buger/gor/listener"
"github.com/buger/gor/replay"
"io"
"log"
"net/http"
"net/http/httputil"
"os"
"os/signal"
"runtime"
_ "runtime/debug"
"runtime/pprof"
"syscall"
"time"
)
const (
VERSION = "0.3"
var (
mode string
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
memprofile = flag.String("memprofile", "", "write memory profile to this file")
)
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rb, _ := httputil.DumpRequest(r, false)
log.Println(string(rb))
next.ServeHTTP(w, r)
})
}
func main() {
// // Don't exit on panic
// defer func() {
// if r := recover(); r != nil {
// fmt.Printf("PANIC: pkg: %v %s \n", r, debug.Stack())
// }
// }()
// If not set via env cariable
if len(os.Getenv("GOMAXPROCS")) == 0 {
runtime.GOMAXPROCS(runtime.NumCPU() * 2)
}
args := os.Args[1:]
if len(args) > 0 && args[0] == "file-server" {
if len(args) != 2 {
log.Fatal("You should specify port and IP (optional) for the file server. Example: `gor file-server :80`")
}
dir, _ := os.Getwd()
log.Println("Started example file server for current dirrectory on address ", args[1])
log.Fatal(http.ListenAndServe(args[1], loggingMiddleware(http.FileServer(http.Dir(dir)))))
} else {
flag.Parse()
InitPlugins()
}
fmt.Println("Version:", VERSION)
mode := "unknown"
if len(os.Args) > 1 {
mode = os.Args[1]
if len(Plugins.Inputs) == 0 || len(Plugins.Outputs) == 0 {
log.Fatal("Required at least 1 input and 1 output")
}
if mode != "listen" && mode != "replay" {
fmt.Println("Usage: \n\tgor listen -h\n\tgor replay -h")
return
if *memprofile != "" {
profileMEM(*memprofile)
}
// Remove mode attr
os.Args = append(os.Args[:1], os.Args[2:]...)
flag.Parse()
switch mode {
case "listen":
listener.Run()
case "replay":
replay.Run()
if *cpuprofile != "" {
profileCPU(*cpuprofile)
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
for _, p := range Plugins.All {
if cp, ok := p.(io.Closer); ok {
cp.Close()
}
}
os.Exit(1)
}()
Start(nil)
}
func profileCPU(cpuprofile string) {
if cpuprofile != "" {
f, err := os.Create(cpuprofile)
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
time.AfterFunc(30*time.Second, func() {
pprof.StopCPUProfile()
f.Close()
log.Println("Stop profiling after 30 seconds")
})
}
}
func profileMEM(memprofile string) {
if memprofile != "" {
f, err := os.Create(memprofile)
if err != nil {
log.Fatal(err)
}
time.AfterFunc(30*time.Second, func() {
pprof.WriteHeapProfile(f)
f.Close()
})
}
}
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"log"
"runtime"
"strconv"
"time"
)
const (
rate = 5
)
type GorStat struct {
statName string
latest int
mean int
max int
count int
}
func NewGorStat(statName string) (s *GorStat) {
s = new(GorStat)
s.statName = statName
s.latest = 0
s.mean = 0
s.max = 0
s.count = 0
if Settings.stats {
log.Println(s.statName + ":latest,mean,max,count,count/second,gcount")
go s.reportStats()
}
return
}
func (s *GorStat) Write(latest int) {
if Settings.stats {
if latest > s.max {
s.max = latest
}
if latest != 0 {
s.mean = (s.mean + latest) / 2
}
s.latest = latest
s.count = s.count + 1
}
}
func (s *GorStat) Reset() {
s.latest = 0
s.max = 0
s.mean = 0
s.count = 0
}
func (s *GorStat) String() string {
return s.statName + ":" + strconv.Itoa(s.latest) + "," + strconv.Itoa(s.mean) + "," + strconv.Itoa(s.max) + "," + strconv.Itoa(s.count) + "," + strconv.Itoa(s.count/rate) + "," + strconv.Itoa(runtime.NumGoroutine())
}
func (s *GorStat) reportStats() {
for {
log.Println(s)
s.Reset()
time.Sleep(rate * time.Second)
}
}
+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
+357
View File
@@ -0,0 +1,357 @@
package main
import (
"bytes"
"crypto/tls"
"io"
"log"
"net"
"net/url"
"runtime/debug"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/buger/gor/proto"
)
var httpMu sync.Mutex
const (
readChunkSize = 64 * 1024
maxResponseSize = 1073741824
)
var chunkedSuffix = []byte("0\r\n\r\n")
var defaultPorts = map[string]string{
"http": "80",
"https": "443",
}
type HTTPClientConfig struct {
FollowRedirects int
Debug bool
OriginalHost bool
ConnectionTimeout time.Duration
Timeout time.Duration
ResponseBufferSize int
}
type HTTPClient struct {
baseURL string
scheme string
host string
conn net.Conn
respBuf []byte
config *HTTPClientConfig
redirectsCount int
}
func NewHTTPClient(baseURL string, config *HTTPClientConfig) *HTTPClient {
if !strings.HasPrefix(baseURL, "http") {
baseURL = "http://" + baseURL
}
u, _ := url.Parse(baseURL)
if !strings.Contains(u.Host, ":") {
if u.Scheme != "http" {
u.Host += ":" + defaultPorts[u.Scheme]
}
}
if config.Timeout.Nanoseconds() == 0 {
config.Timeout = 5 * time.Second
}
config.ConnectionTimeout = config.Timeout
if config.ResponseBufferSize == 0 {
config.ResponseBufferSize = 100 * 1024 // 100kb
}
client := new(HTTPClient)
client.baseURL = u.String()
client.host = u.Host
client.scheme = u.Scheme
client.respBuf = make([]byte, config.ResponseBufferSize)
client.config = config
return client
}
func (c *HTTPClient) Connect() (err error) {
c.Disconnect()
if !strings.Contains(c.host, ":") {
c.conn, err = net.DialTimeout("tcp", c.host+":80", c.config.ConnectionTimeout)
} else {
c.conn, err = net.DialTimeout("tcp", c.host, c.config.ConnectionTimeout)
}
if c.scheme == "https" {
tlsConn := tls.Client(c.conn, &tls.Config{InsecureSkipVerify: true})
if err = tlsConn.Handshake(); err != nil {
return
}
c.conn = tlsConn
}
return
}
func (c *HTTPClient) Disconnect() {
if c.conn != nil {
c.conn.Close()
c.conn = nil
Debug("[HTTP] Disconnected: ", c.baseURL)
}
}
func (c *HTTPClient) isAlive() bool {
one := make([]byte, 1)
// Ready 1 byte from socket without timeout to check if it not closed
c.conn.SetReadDeadline(time.Now().Add(time.Millisecond))
_, err := c.conn.Read(one)
if err == nil {
return true
} else if err == io.EOF {
if c.config.Debug {
Debug("[HTTPClient] connection closed, reconnecting")
}
return false
} else if err == syscall.EPIPE {
Debug("Detected broken pipe.", err)
return false
}
return true
}
func (c *HTTPClient) Send(data []byte) (response []byte, err error) {
// Don't exit on panic
defer func() {
if r := recover(); r != nil {
Debug("[HTTPClient]", r, string(data))
if _, ok := r.(error); !ok {
log.Println("[HTTPClient] Failed to send request: ", string(data))
log.Println("PANIC: pkg:", r, debug.Stack())
}
}
}()
if c.conn == nil || !c.isAlive() {
Debug("[HTTPClient] Connecting:", c.baseURL)
if err = c.Connect(); err != nil {
log.Println("[HTTPClient] Connection error:", err)
response = errorPayload(HTTP_CONNECTION_ERROR)
return
}
}
timeout := time.Now().Add(c.config.Timeout)
c.conn.SetWriteDeadline(timeout)
if !c.config.OriginalHost {
data = proto.SetHost(data, []byte(c.baseURL), []byte(c.host))
}
if c.config.Debug {
Debug("[HTTPClient] Sending:", string(data))
}
if _, err = c.conn.Write(data); err != nil {
Debug("[HTTPClient] Write error:", err, c.baseURL)
response = errorPayload(HTTP_TIMEOUT)
return
}
var readBytes, n int
var currentChunk []byte
timeout = time.Now().Add(c.config.Timeout)
chunked := false
contentLength := -1
currentContentLength := 0
chunks := 0
for {
c.conn.SetReadDeadline(timeout)
if readBytes < len(c.respBuf) {
n, err = c.conn.Read(c.respBuf[readBytes:])
readBytes += n
chunks++
if err != nil {
if err == io.EOF {
err = nil
}
break
}
// First chunk
if chunked || contentLength != -1 {
currentContentLength += n
} else {
// If headers are finished
if bytes.Contains(c.respBuf[:readBytes], proto.EmptyLine) {
if bytes.Equal(proto.Header(c.respBuf, []byte("Transfer-Encoding")), []byte("chunked")) {
chunked = true
} else {
l := proto.Header(c.respBuf, []byte("Content-Length"))
if len(l) > 0 {
contentLength, _ = strconv.Atoi(string(l))
}
}
currentContentLength += len(proto.Body(c.respBuf[:readBytes]))
}
}
if chunked {
// Check if chunked message finished
if bytes.HasSuffix(c.respBuf[:readBytes], chunkedSuffix) {
break
}
} else if contentLength != -1 {
if currentContentLength > contentLength {
Debug("[HTTPClient] disconnected, wrong length", currentContentLength, contentLength)
c.Disconnect()
break
} else if currentContentLength == contentLength {
break
}
}
} else {
if currentChunk == nil {
currentChunk = make([]byte, readChunkSize)
}
n, err = c.conn.Read(currentChunk)
if err == io.EOF {
break
} else if err != nil {
Debug("[HTTPClient] Read the whole body error:", err, c.baseURL)
break
}
readBytes += int(n)
chunks++
currentContentLength += n
if chunked {
// Check if chunked message finished
if bytes.HasSuffix(currentChunk[:n], chunkedSuffix) {
break
}
} else if contentLength != -1 {
if currentContentLength > contentLength {
Debug("[HTTPClient] disconnected, wrong length", currentContentLength, contentLength)
c.Disconnect()
break
} else if currentContentLength == contentLength {
break
}
} else {
Debug("[HTTPClient] disconnected, can't find Content-Length or Chunked")
c.Disconnect()
break
}
}
if readBytes >= maxResponseSize {
Debug("[HTTPClient] Body is more than the max size", maxResponseSize,
c.baseURL)
break
}
// For following chunks expect less timeout
timeout = time.Now().Add(c.config.Timeout / 5)
}
if err != nil {
Debug("[HTTPClient] Response read error", err, c.conn, readBytes)
response = errorPayload(HTTP_TIMEOUT)
return
}
if readBytes > len(c.respBuf) {
readBytes = len(c.respBuf)
}
payload := make([]byte, readBytes)
copy(payload, c.respBuf[:readBytes])
if c.config.Debug {
Debug("[HTTPClient] Received:", string(payload))
}
if c.config.FollowRedirects > 0 && c.redirectsCount < c.config.FollowRedirects {
status := payload[9:12]
// 3xx requests
if status[0] == '3' {
c.redirectsCount++
location := proto.Header(payload, []byte("Location"))
redirectPayload := []byte("GET " + string(location) + " HTTP/1.1\r\n\r\n")
if c.config.Debug {
Debug("[HTTPClient] Redirecting to: " + string(location))
}
return c.Send(redirectPayload)
}
}
c.redirectsCount = 0
return payload, err
}
func (c *HTTPClient) Get(path string) (response []byte, err error) {
payload := "GET " + path + " HTTP/1.1\r\n\r\n"
return c.Send([]byte(payload))
}
func (c *HTTPClient) Post(path string, body []byte) (response []byte, err error) {
payload := "POST " + path + " HTTP/1.1\r\n"
payload += "Content-Length: " + strconv.Itoa(len(body)) + "\r\n\r\n"
payload += string(body)
return c.Send([]byte(payload))
}
const (
// https://support.cloudflare.com/hc/en-us/articles/200171936-Error-520-Web-server-is-returning-an-unknown-error
HTTP_UNKNOWN_ERROR = "520"
// https://support.cloudflare.com/hc/en-us/articles/200171916-Error-521-Web-server-is-down
HTTP_CONNECTION_ERROR = "521"
// https://support.cloudflare.com/hc/en-us/articles/200171906-Error-522-Connection-timed-out
HTTP_CONNECTION_TIMEOUT = "522"
// https://support.cloudflare.com/hc/en-us/articles/200171946-Error-523-Origin-is-unreachable
HTTP_UNREACHABLE = "523"
// https://support.cloudflare.com/hc/en-us/articles/200171926-Error-524-A-timeout-occurred
HTTP_TIMEOUT = "524"
)
var errorPayloadTemplate = "HTTP/1.1 202 Accepted\r\nDate: Mon, 17 Aug 2015 14:10:11 GMT\r\nContent-Length: 0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n"
func errorPayload(errorCode string) []byte {
payload := make([]byte, len(errorPayloadTemplate))
copy(payload, errorPayloadTemplate)
copy(payload[29:58], []byte(time.Now().Format(time.RFC1123)))
copy(payload[9:12], errorCode)
return payload
}
+413
View File
@@ -0,0 +1,413 @@
package main
import (
"bytes"
"crypto/rand"
"github.com/buger/gor/proto"
"io/ioutil"
_ "log"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
_ "reflect"
"sync"
"testing"
"time"
)
func TestHTTPClientURLPort(t *testing.T) {
c1 := NewHTTPClient("http://example.com", &HTTPClientConfig{})
if c1.baseURL != "http://example.com" {
t.Error("Sould not add 80 port for http:", c1.baseURL)
}
c2 := NewHTTPClient("https://example.com", &HTTPClientConfig{})
if c2.baseURL != "https://example.com:443" {
t.Error("Sould add 443 port for https:", c2.baseURL)
}
c3 := NewHTTPClient("https://example.com:1", &HTTPClientConfig{})
if c3.baseURL != "https://example.com:1" {
t.Error("Sould use specified port:", c3.baseURL)
}
c4 := NewHTTPClient("example.com", &HTTPClientConfig{})
if c4.baseURL != "http://example.com" {
t.Error("Sould not add default protocol:", c4.baseURL)
}
}
func TestHTTPClientSend(t *testing.T) {
wg := new(sync.WaitGroup)
payload := func(reqType string) []byte {
switch reqType {
case "GET":
return []byte("GET / HTTP/1.1\r\n\r\n")
case "POST":
return []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
case "POST_CHUNKED":
return []byte("POST / HTTP/1.1\r\nHost: www.w3.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
}
return []byte("")
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
defer r.Body.Close()
body, _ := ioutil.ReadAll(r.Body)
if len(r.TransferEncoding) > 0 && r.TransferEncoding[0] == "chunked" {
if string(body) != "Wikipedia in\r\n\r\nchunks." {
t.Error("Wrong POST body:", body, string(body))
}
} else {
if string(body) != "a=1&b=2" {
buf, _ := httputil.DumpRequest(r, true)
t.Error("Wrong POST body:", string(body), string(buf))
}
}
}
wg.Done()
}))
defer server.Close()
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true})
wg.Add(4)
client.Send(payload("POST"))
client.Send(payload("GET"))
client.Send(payload("POST_CHUNKED"))
client.Send(payload("POST"))
wg.Wait()
}
// https://github.com/buger/gor/issues/184
func TestHTTPClientResponseBuffer(t *testing.T) {
testCases := []struct {
name string
responseSize int
buffserSize int
expectedSize int
timeout time.Duration
}{
{"Chunked, buffer overflow", 10 * 1024, 1024, 1024, 50 * time.Millisecond},
{"Chunked, fits buffer", 10 * 1024, 64 * 1024, 10*1024 + 145 /* headers length + chunked meta */, 50 * time.Millisecond},
{"Content-Length, buffer overflow", 1024, 1000, 1000, 50 * time.Millisecond},
{"Content-Length, fits buffer", 1024, 64 * 1024, 1024 + 118, 50 * time.Millisecond},
}
for _, tc := range testCases {
wg := new(sync.WaitGroup)
payload := []byte("GET / HTTP/1.1\r\n\r\n")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
size := tc.responseSize // 1kb
rb := make([]byte, size)
rand.Read(rb)
w.Write(rb[:size/2])
w.Write(rb[size/2:])
wg.Done()
}))
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true, ResponseBufferSize: tc.buffserSize, Timeout: 100 * time.Millisecond})
wg.Add(2)
start := time.Now()
client.Send(payload)
resp, err := client.Send(payload)
stop := time.Now()
if err != nil {
t.Error("Request error", err)
}
if stop.Sub(start) > tc.timeout {
t.Error("Request took too long", stop.Sub(start), tc.timeout)
}
if len(resp) != tc.expectedSize {
t.Error(tc.name, " - Wrong response size:", tc.expectedSize, len(resp))
} else {
if !bytes.Equal(resp[0:8], []byte("HTTP/1.1")) {
t.Error(tc.name, " - Response buffer contains data from previous request", string(resp), len(resp))
}
}
wg.Wait()
server.Close()
}
}
func TestHTTPClientHTTPSSend(t *testing.T) {
wg := new(sync.WaitGroup)
payload := func(reqType string) []byte {
switch reqType {
case "GET":
return []byte("GET / HTTP/1.1\r\n\r\n")
case "POST":
return []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
case "POST_CHUNKED":
return []byte("POST / HTTP/1.1\r\nHost: www.w3.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
}
return []byte("")
}
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
defer r.Body.Close()
body, _ := ioutil.ReadAll(r.Body)
if len(r.TransferEncoding) > 0 && r.TransferEncoding[0] == "chunked" {
if string(body) != "Wikipedia in\r\n\r\nchunks." {
t.Error("Wrong POST body:", body, string(body))
}
} else {
if string(body) != "a=1&b=2" {
buf, _ := httputil.DumpRequest(r, true)
t.Error("Wrong POST body:", string(body), string(buf))
}
}
}
wg.Done()
}))
defer server.Close()
client := NewHTTPClient(server.URL, &HTTPClientConfig{})
wg.Add(4)
client.Send(payload("POST"))
client.Send(payload("GET"))
client.Send(payload("POST_CHUNKED"))
client.Send(payload("POST"))
wg.Wait()
}
func TestHTTPClientServerInstantDisconnect(t *testing.T) {
wg := new(sync.WaitGroup)
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
ln, _ := net.Listen("tcp", "127.0.0.1:0")
defer ln.Close()
go func() {
for {
conn, err := ln.Accept()
if err != nil {
break
}
conn.Close()
wg.Done()
}
}()
client := NewHTTPClient(ln.Addr().String(), &HTTPClientConfig{})
wg.Add(2)
client.Send(GETPayload)
client.Send(GETPayload)
wg.Wait()
}
func TestHTTPClientServerNoKeepAlive(t *testing.T) {
wg := new(sync.WaitGroup)
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
ln, _ := net.Listen("tcp", "127.0.0.1:0")
defer ln.Close()
go func() {
for {
conn, err := ln.Accept()
if err != nil {
break
}
buf := make([]byte, 4096)
reqLen, err := conn.Read(buf)
if err != nil {
t.Error("Error reading:", err.Error())
}
Debug("Received: ", string(buf[0:reqLen]))
conn.Write([]byte("OK"))
// No keep-alive connections
conn.Close()
wg.Done()
}
}()
client := NewHTTPClient(ln.Addr().String(), &HTTPClientConfig{})
wg.Add(2)
client.Send(GETPayload)
client.Send(GETPayload)
wg.Wait()
}
func TestHTTPClientRedirect(t *testing.T) {
wg := new(sync.WaitGroup)
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.Redirect(w, r, "/new", 301)
}
wg.Done()
}))
defer server.Close()
client := NewHTTPClient(server.URL, &HTTPClientConfig{FollowRedirects: 1, Debug: false})
// Should do 2 queries
wg.Add(2)
client.Send(GETPayload)
wg.Wait()
}
func TestHTTPClientRedirectLimit(t *testing.T) {
wg := new(sync.WaitGroup)
GETPayload := []byte("GET / HTTP/1.1\r\n\r\n")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.Redirect(w, r, "/r1", 301)
}
if r.URL.Path == "/r1" {
http.Redirect(w, r, "/r2", 301)
}
if r.URL.Path == "/r2" {
http.Redirect(w, r, "/new", 301)
}
wg.Done()
}))
defer server.Close()
client := NewHTTPClient(server.URL, &HTTPClientConfig{FollowRedirects: 2, Debug: false})
// Have 3 redirects + 1 GET, but should do only 2 redirects + GET
wg.Add(3)
client.Send(GETPayload)
wg.Wait()
}
func TestHTTPClientHandleHTTP10(t *testing.T) {
wg := new(sync.WaitGroup)
GETPayload := []byte("GET http://foobar.com/path HTTP/1.0\r\n\r\n")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/path" {
t.Error("Path not match:", r.URL.Path)
}
wg.Done()
}))
defer server.Close()
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true})
wg.Add(1)
client.Send(GETPayload)
wg.Wait()
}
func TestHTTPClientErrors(t *testing.T) {
req := []byte("GET http://foobar.com/path HTTP/1.0\r\n\r\n")
// Port not exists
client := NewHTTPClient("http://127.0.0.1:1", &HTTPClientConfig{Debug: true})
if resp, err := client.Send(req); err != nil {
if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) {
t.Error("Should return status 521 for connection refused, instead:", string(s))
}
} else {
t.Error("Should throw error")
}
client = NewHTTPClient("http://not.existing", &HTTPClientConfig{Debug: true})
if resp, err := client.Send(req); err != nil {
if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) {
t.Error("Should return status 521 for no such host, instead:", string(s))
}
} else {
t.Error("Should throw error")
}
// Non routable IP address to simulate connection timeout
client = NewHTTPClient("http://10.255.255.1", &HTTPClientConfig{Debug: true, ConnectionTimeout: 100 * time.Millisecond})
if resp, err := client.Send(req); err != nil {
if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) {
t.Error("Should return status 521 for io/timeout:", string(s))
}
} else {
t.Error("Should throw error")
}
// Connecting but io timeout on read
ln, _ := net.Listen("tcp", "127.0.0.1:0")
client = NewHTTPClient("http://"+ln.Addr().String(), &HTTPClientConfig{Debug: true, Timeout: 10 * time.Millisecond})
defer ln.Close()
if resp, err := client.Send(req); err != nil {
if s := proto.Status(resp); !bytes.Equal(s, []byte("524")) {
t.Error("Should return status 524 for io read, instead:", string(s))
}
} else {
t.Error("Should throw error")
}
// Response read error read tcp [::1]:51128: connection reset by peer &{{0xc20802a000}}
ln1, _ := net.Listen("tcp", "127.0.0.1:0")
go func() {
ln1.Accept()
}()
defer ln1.Close()
client = NewHTTPClient("http://"+ln1.Addr().String(), &HTTPClientConfig{Debug: true, Timeout: 10 * time.Millisecond})
if resp, err := client.Send(req); err != nil {
if s := proto.Status(resp); !bytes.Equal(s, []byte("524")) {
t.Error("Should return status 524 for connection reset by peer, instead:", string(s))
}
} else {
t.Error("Should throw error")
}
}
+157
View File
@@ -0,0 +1,157 @@
package main
import (
"bytes"
"hash/fnv"
"github.com/buger/gor/proto"
)
type HTTPModifier struct {
config *HTTPModifierConfig
}
func NewHTTPModifier(config *HTTPModifierConfig) *HTTPModifier {
// Optimization to skip modifier completely if we do not need it
if len(config.urlRegexp) == 0 &&
len(config.urlNegativeRegexp) == 0 &&
len(config.urlRewrite) == 0 &&
len(config.headerFilters) == 0 &&
len(config.headerNegativeFilters) == 0 &&
len(config.headerHashFilters) == 0 &&
len(config.paramHashFilters) == 0 &&
len(config.params) == 0 &&
len(config.headers) == 0 &&
len(config.methods) == 0 {
return nil
}
return &HTTPModifier{config: config}
}
func (m *HTTPModifier) Rewrite(payload []byte) (response []byte) {
if !proto.IsHTTPPayload(payload) {
return payload
}
if len(m.config.methods) > 0 {
method := proto.Method(payload)
matched := false
for _, m := range m.config.methods {
if bytes.Equal(method, m) {
matched = true
break
}
}
if !matched {
return
}
}
if len(m.config.headers) > 0 {
for _, header := range m.config.headers {
payload = proto.SetHeader(payload, []byte(header.Name), []byte(header.Value))
}
}
if len(m.config.params) > 0 {
for _, param := range m.config.params {
payload = proto.SetPathParam(payload, param.Name, param.Value)
}
}
if len(m.config.urlRegexp) > 0 {
path := proto.Path(payload)
matched := false
for _, f := range m.config.urlRegexp {
if f.regexp.Match(path) {
matched = true
break
}
}
if !matched {
return
}
}
if len(m.config.urlNegativeRegexp) > 0 {
path := proto.Path(payload)
for _, f := range m.config.urlNegativeRegexp {
if f.regexp.Match(path) {
return
}
}
}
if len(m.config.headerFilters) > 0 {
for _, f := range m.config.headerFilters {
value := proto.Header(payload, f.name)
if len(value) > 0 && !f.regexp.Match(value) {
return
}
}
}
if len(m.config.headerNegativeFilters) > 0 {
for _, f := range m.config.headerNegativeFilters {
value := proto.Header(payload, f.name)
if len(value) > 0 && f.regexp.Match(value) {
return
}
}
}
if len(m.config.headerHashFilters) > 0 {
for _, f := range m.config.headerHashFilters {
value := proto.Header(payload, f.name)
if len(value) > 0 {
hasher := fnv.New32a()
hasher.Write(value)
if (hasher.Sum32() % 100) >= f.percent {
return
}
}
}
}
if len(m.config.paramHashFilters) > 0 {
for _, f := range m.config.paramHashFilters {
value, s, _ := proto.PathParam(payload, f.name)
if s != -1 {
hasher := fnv.New32a()
hasher.Write(value)
if (hasher.Sum32() % 100) >= f.percent {
return
}
}
}
}
if len(m.config.urlRewrite) > 0 {
path := proto.Path(payload)
for _, f := range m.config.urlRewrite {
if f.src.Match(path) {
path = f.src.ReplaceAll(path, f.target)
payload = proto.SetPath(payload, path)
break
}
}
}
return payload
}
+215
View File
@@ -0,0 +1,215 @@
package main
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
// HTTPModifierConfig holds configuration options for built-in traffic modifier
type HTTPModifierConfig struct {
urlNegativeRegexp HTTPUrlRegexp
urlRegexp HTTPUrlRegexp
urlRewrite UrlRewriteMap
headerFilters HTTPHeaderFilters
headerNegativeFilters HTTPHeaderFilters
headerHashFilters HTTPHashFilters
paramHashFilters HTTPHashFilters
params HTTPParams
headers HTTPHeaders
methods HTTPMethods
}
//
// Handling of --http-allow-header, --http-disallow-header options
//
type headerFilter struct {
name []byte
regexp *regexp.Regexp
}
// HTTPHeaderFilters holds list of headers and their regexps
type HTTPHeaderFilters []headerFilter
func (h *HTTPHeaderFilters) String() string {
return fmt.Sprint(*h)
}
func (h *HTTPHeaderFilters) Set(value string) error {
valArr := strings.SplitN(value, ":", 2)
if len(valArr) < 2 {
return errors.New("need both header and value, colon-delimited (ex. user_id:^169$).")
}
r, err := regexp.Compile(valArr[1])
if err != nil {
return err
}
*h = append(*h, headerFilter{name: []byte(valArr[0]), regexp: r})
return nil
}
//
// Handling of --http-allow-header-hash and --http-allow-param-hash options
//
type hashFilter struct {
name []byte
percent uint32
}
type HTTPHashFilters []hashFilter
func (h *HTTPHashFilters) String() string {
return fmt.Sprint(*h)
}
func (h *HTTPHashFilters) Set(value string) error {
valArr := strings.SplitN(value, ":", 2)
if len(valArr) < 2 {
return errors.New("need both header and value, colon-delimited (ex. user_id:50%)")
}
f := hashFilter{name: []byte(valArr[0])}
if strings.Contains(valArr[1], "%") {
p, _ := strconv.ParseInt(valArr[1][:len(valArr[1])-1], 0, 0)
f.percent = uint32(p)
} else if strings.Contains(valArr[1], "/") {
// DEPRECATED format
var num, den uint64
fracArr := strings.Split(valArr[1], "/")
num, _ = strconv.ParseUint(fracArr[0], 10, 64)
den, _ = strconv.ParseUint(fracArr[1], 10, 64)
f.percent = uint32((float64(num) / float64(den)) * 100)
} else {
return errors.New("Value should be percent and contain '%'")
}
*h = append(*h, f)
return nil
}
//
// Handling of --http-set-header option
//
type HTTPHeaders []HTTPHeader
type HTTPHeader struct {
Name string
Value string
}
func (h *HTTPHeaders) String() string {
return fmt.Sprint(*h)
}
func (h *HTTPHeaders) Set(value string) error {
v := strings.SplitN(value, ":", 2)
if len(v) != 2 {
return errors.New("Expected `Key: Value`")
}
header := HTTPHeader{
strings.TrimSpace(v[0]),
strings.TrimSpace(v[1]),
}
*h = append(*h, header)
return nil
}
//
// Handling of --http-set-param option
//
type HTTPParams []HTTPParam
type HTTPParam struct {
Name []byte
Value []byte
}
func (h *HTTPParams) String() string {
return fmt.Sprint(*h)
}
func (h *HTTPParams) Set(value string) error {
v := strings.SplitN(value, "=", 2)
if len(v) != 2 {
return errors.New("Expected `Key=Value`")
}
param := HTTPParam{
[]byte(strings.TrimSpace(v[0])),
[]byte(strings.TrimSpace(v[1])),
}
*h = append(*h, param)
return nil
}
//
// Handling of --http-allow-method option
//
type HTTPMethods [][]byte
func (h *HTTPMethods) String() string {
return fmt.Sprint(*h)
}
func (h *HTTPMethods) Set(value string) error {
*h = append(*h, []byte(value))
return nil
}
//
// Handling of --http-rewrite-url option
//
type urlRewrite struct {
src *regexp.Regexp
target []byte
}
type UrlRewriteMap []urlRewrite
func (r *UrlRewriteMap) String() string {
return fmt.Sprint(*r)
}
func (r *UrlRewriteMap) Set(value string) error {
valArr := strings.SplitN(value, ":", 2)
if len(valArr) < 2 {
return errors.New("need both src and target, colon-delimited (ex. /a:/b)")
}
regexp, err := regexp.Compile(valArr[0])
if err != nil {
return err
}
*r = append(*r, urlRewrite{src: regexp, target: []byte(valArr[1])})
return nil
}
//
// Handling of --http-allow-url option
//
type urlRegexp struct {
regexp *regexp.Regexp
}
type HTTPUrlRegexp []urlRegexp
func (r *HTTPUrlRegexp) String() string {
return fmt.Sprint(*r)
}
func (r *HTTPUrlRegexp) Set(value string) error {
regexp, err := regexp.Compile(value)
*r = append(*r, urlRegexp{regexp: regexp})
return err
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"testing"
)
func TestHTTPHeaderFilters(t *testing.T) {
filters := HTTPHeaderFilters{}
err := filters.Set("Header1:^$")
if err != nil {
t.Error("Should not error on Header1:^$")
}
err = filters.Set("Header2:^:$")
if err != nil {
t.Error("Should not error on Header2:^:$")
}
// Missing colon
err = filters.Set("Header3-^$")
if err == nil {
t.Error("Should error on Header2:^:$")
}
}
func TestHTTPHashFilters(t *testing.T) {
filters := HTTPHashFilters{}
err := filters.Set("Header1:1/2")
if err != nil {
t.Error("Should support old syntax")
}
if filters[0].percent != 50 {
t.Error("Wrong percentage", filters[0].percent)
}
err = filters.Set("Header2:1")
if err == nil {
t.Error("Should error on Header2 because no % symbol")
}
err = filters.Set("Header2:10%")
if err != nil {
t.Error("Should pass")
}
if filters[1].percent != 10 {
t.Error("Wrong percentage", filters[1].percent)
}
}
func TestUrlRewriteMap(t *testing.T) {
var err error
rewrites := UrlRewriteMap{}
if err = rewrites.Set("/v1/user/([^\\/]+)/ping:/v2/user/$1/ping"); err != nil {
t.Error("Should set mapping", err)
}
if err = rewrites.Set("/v1/user/([^\\/]+)/ping"); err == nil {
t.Error("Should not set mapping without :")
}
}
+247
View File
@@ -0,0 +1,247 @@
package main
import (
"bytes"
"github.com/buger/gor/proto"
"testing"
)
func TestHTTPModifierWithoutConfig(t *testing.T) {
if NewHTTPModifier(&HTTPModifierConfig{}) != nil {
t.Error("If no config specified should not be initialized")
}
}
func TestHTTPModifierHeaderFilters(t *testing.T) {
filters := HTTPHeaderFilters{}
filters.Set("Host:^www.w3.org$")
modifier := NewHTTPModifier(&HTTPModifierConfig{
headerFilters: filters,
})
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if len(modifier.Rewrite(payload)) == 0 {
t.Error("Request should pass filters")
}
filters = HTTPHeaderFilters{}
// Setting filter that not match our header
filters.Set("Host:^www.w4.org$")
modifier = NewHTTPModifier(&HTTPModifierConfig{
headerFilters: filters,
})
if len(modifier.Rewrite(payload)) != 0 {
t.Error("Request should not pass filters")
}
}
func TestHTTPModifierHeaderNegativeFilters(t *testing.T) {
filters := HTTPHeaderFilters{}
filters.Set("Host:^www.w3.org$")
modifier := NewHTTPModifier(&HTTPModifierConfig{
headerNegativeFilters: filters,
})
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w4.org\r\n\r\na=1&b=2")
if len(modifier.Rewrite(payload)) == 0 {
t.Error("Request should pass filters")
}
filters = HTTPHeaderFilters{}
// Setting filter that not match our header
filters.Set("Host:^www.w4.org$")
modifier = NewHTTPModifier(&HTTPModifierConfig{
headerNegativeFilters: filters,
})
if len(modifier.Rewrite(payload)) != 0 {
t.Error("Request should not pass filters")
}
}
func TestHTTPModifierURLRewrite(t *testing.T) {
var url, newURL []byte
rewrites := UrlRewriteMap{}
payload := func(url []byte) []byte {
return []byte("POST " + string(url) + " HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
}
err := rewrites.Set("/v1/user/([^\\/]+)/ping:/v2/user/$1/ping")
if err != nil {
t.Error("Should not error on /v1/user/([^\\/]+)/ping:/v2/user/$1/ping")
}
modifier := NewHTTPModifier(&HTTPModifierConfig{
urlRewrite: rewrites,
})
url = []byte("/v1/user/joe/ping")
if newURL = proto.Path(modifier.Rewrite(payload(url))); bytes.Equal(newURL, url) {
t.Error("Request url should have been rewritten, wasn't", string(newURL))
}
url = []byte("/v1/user/ping")
if newURL = proto.Path(modifier.Rewrite(payload(url))); !bytes.Equal(newURL, url) {
t.Error("Request url should have been rewritten, wasn't", string(newURL))
}
}
func TestHTTPModifierHeaderHashFilters(t *testing.T) {
filters := HTTPHashFilters{}
filters.Set("Header2:1/2")
modifier := NewHTTPModifier(&HTTPModifierConfig{
headerHashFilters: filters,
})
payload := func(header []byte) []byte {
return []byte("POST / HTTP/1.1\r\n" + string(header) + "Content-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
}
if p := modifier.Rewrite(payload([]byte(""))); len(p) == 0 {
t.Error("Request should pass filters if Header does not exist")
}
if p := modifier.Rewrite(payload([]byte("Header2: 3\r\n"))); len(p) > 0 {
t.Error("Request should not pass filters, Header2 hash too high")
}
if p := modifier.Rewrite(payload([]byte("Header2: 1\r\n"))); len(p) == 0 {
t.Error("Request should pass filters")
}
}
func TestHTTPModifierParamHashFilters(t *testing.T) {
filters := HTTPHashFilters{}
filters.Set("user_id:1/2")
modifier := NewHTTPModifier(&HTTPModifierConfig{
paramHashFilters: filters,
})
payload := func(value []byte) []byte {
return []byte("POST /" + string(value) + " HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
}
if p := modifier.Rewrite(payload([]byte(""))); len(p) == 0 {
t.Error("Request should pass filters if param does not exist")
}
if p := modifier.Rewrite(payload([]byte("?user_id=3"))); len(p) > 0 {
t.Error("Request should not pass filters", string(p))
}
if p := modifier.Rewrite(payload([]byte("?user_id=1"))); len(p) == 0 {
t.Error("Request should pass filters")
}
}
func TestHTTPModifierHeaders(t *testing.T) {
headers := HTTPHeaders{}
headers.Set("Header1:1")
headers.Set("Host:localhost")
modifier := NewHTTPModifier(&HTTPModifierConfig{
headers: headers,
})
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
newPayload := []byte("POST /post HTTP/1.1\r\nHeader1: 1\r\nContent-Length: 7\r\nHost: localhost\r\n\r\na=1&b=2")
if payload = modifier.Rewrite(payload); !bytes.Equal(payload, newPayload) {
t.Error("Should update request headers", string(payload))
}
}
func TestHTTPModifierURLRegexp(t *testing.T) {
filters := HTTPUrlRegexp{}
filters.Set("/v1/app")
filters.Set("/v1/api")
modifier := NewHTTPModifier(&HTTPModifierConfig{
urlRegexp: filters,
})
payload := func(url string) []byte {
return []byte("POST " + url + " HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
}
if len(modifier.Rewrite(payload("/v1/app/test"))) == 0 {
t.Error("Should pass url")
}
if len(modifier.Rewrite(payload("/v1/api/test"))) == 0 {
t.Error("Should pass url")
}
if len(modifier.Rewrite(payload("/other"))) > 0 {
t.Error("Should not pass url")
}
}
func TestHTTPModifierURLNegativeRegexp(t *testing.T) {
filters := HTTPUrlRegexp{}
filters.Set("/restricted1")
filters.Set("/some/restricted2")
modifier := NewHTTPModifier(&HTTPModifierConfig{
urlNegativeRegexp: filters,
})
payload := func(url string) []byte {
return []byte("POST " + url + " HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
}
if len(modifier.Rewrite(payload("/v1/app/test"))) == 0 {
t.Error("Should pass url")
}
if len(modifier.Rewrite(payload("/restricted1"))) > 0 {
t.Error("Should not pass url")
}
if len(modifier.Rewrite(payload("/some/restricted2"))) > 0 {
t.Error("Should not pass url")
}
}
func TestHTTPModifierSetHeader(t *testing.T) {
filters := HTTPHeaders{}
filters.Set("User-Agent:Gor")
modifier := NewHTTPModifier(&HTTPModifierConfig{
headers: filters,
})
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
payloadAfter := []byte("POST /post HTTP/1.1\r\nUser-Agent: Gor\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if payload = modifier.Rewrite(payload); !bytes.Equal(payloadAfter, payload) {
t.Error("Should add new header", string(payload))
}
}
func TestHTTPModifierSetParam(t *testing.T) {
filters := HTTPParams{}
filters.Set("api_key=1")
modifier := NewHTTPModifier(&HTTPModifierConfig{
params: filters,
})
payload := []byte("POST /post?api_key=1234 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
payloadAfter := []byte("POST /post?api_key=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if payload = modifier.Rewrite(payload); !bytes.Equal(payloadAfter, payload) {
t.Error("Should override param", string(payload))
}
}
+48
View File
@@ -0,0 +1,48 @@
package main
import (
"time"
)
// DummyInput used for debugging. It generate 1 "GET /"" request per second.
type DummyInput struct {
data chan []byte
}
// NewDummyInput constructor for DummyInput
func NewDummyInput(options string) (di *DummyInput) {
di = new(DummyInput)
di.data = make(chan []byte)
go di.emit()
return
}
func (i *DummyInput) Read(data []byte) (int, error) {
buf := <-i.data
copy(data, buf)
return len(buf), nil
}
func (i *DummyInput) emit() {
ticker := time.NewTicker(time.Second)
for {
select {
case <-ticker.C:
uuid := uuid()
reqh := payloadHeader(RequestPayload, uuid, time.Now().UnixNano())
i.data <- append(reqh, []byte("GET / HTTP/1.1\r\nHost: www.w3.org\r\nUser-Agent: Go 1.1 package http\r\nAccept-Encoding: gzip\r\n\r\n")...)
resh := payloadHeader(ResponsePayload, uuid, 1)
i.data <- append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...)
}
}
}
func (i *DummyInput) String() string {
return "Dummy Input"
}
+226
View File
@@ -0,0 +1,226 @@
package main
import (
"bufio"
"bytes"
"compress/gzip"
"errors"
"io"
"log"
"os"
"path/filepath"
"strings"
"strconv"
"time"
)
// FileInput can read requests generated by FileOutput
type FileInput struct {
data chan []byte
exit chan bool
path string
fileInputReaders []*fileInputReader
speedFactor float64
loop bool
}
type fileInputReader struct {
reader *bufio.Reader
meta [][]byte
data []byte
file *os.File
timestamp int64
}
// NewFileInput constructor for FileInput. Accepts file path as argument.
func NewFileInput(path string, loop bool) (i *FileInput) {
i = new(FileInput)
i.data = make(chan []byte)
i.exit = make(chan bool)
i.path = path
i.speedFactor = 1
i.loop = loop
if err := i.init(); err != nil {
return
}
go i.emit()
return
}
type NextFileNotFound struct{}
func (_ *NextFileNotFound) Error() string {
return "There is no new files"
}
func (i *FileInput) init() (err error) {
var matches []string
if matches, err = filepath.Glob(i.path); err != nil {
log.Println("Wrong file pattern", i.path, err)
return
}
if len(matches) == 0 {
log.Println("No files match pattern: ", i.path)
return errors.New("No matching files")
}
i.fileInputReaders = make([]*fileInputReader, len(matches))
for idx, p := range matches {
file, _ := os.Open(p)
fileInputReader := &fileInputReader{}
fileInputReader.file = file
if strings.HasSuffix(p, ".gz") {
gzReader, err := gzip.NewReader(file)
if err != nil {
log.Fatal(err)
}
fileInputReader.reader = bufio.NewReader(gzReader)
} else {
fileInputReader.reader = bufio.NewReader(file)
}
fileInputReader.readNextInput()
i.fileInputReaders[idx] = fileInputReader
}
return nil
}
func (i *FileInput) Read(data []byte) (int, error) {
buf := <-i.data
copy(data, buf)
return len(buf), nil
}
func (i *FileInput) String() string {
return "File input: " + i.path
}
func (f *fileInputReader) readNextInput() {
nextInput := f.nextInput()
f.parseNextInput(nextInput)
}
func (f *fileInputReader) parseNextInput(input []byte) {
if (input != nil) {
f.meta = payloadMeta(input)
f.timestamp, _ = strconv.ParseInt(string(f.meta[2]), 10, 64)
f.data = input
}
}
func (f *fileInputReader) nextInput() []byte {
payloadSeparatorAsBytes := []byte(payloadSeparator)
var buffer bytes.Buffer
for {
line, err := f.reader.ReadBytes('\n')
if err != nil {
if err != io.EOF {
log.Fatal(err)
}
if err == io.EOF {
f.file.Close()
f.file = nil
return nil
}
}
if bytes.Equal(payloadSeparatorAsBytes[1:], line) {
asBytes := buffer.Bytes()
// Bytes() returns only pointer, so to remove data-race copy the data to an array
newBuf := make([]byte, len(asBytes) - 1)
copy(newBuf, asBytes)
return newBuf
}
buffer.Write(line)
}
}
func (i *FileInput) nextInputReader() *fileInputReader {
var nextFileInputReader *fileInputReader
for _, fileInputReader := range i.fileInputReaders {
if fileInputReader.file == nil {
continue
}
if fileInputReader.meta[0][0] == ResponsePayload {
return fileInputReader
}
if nextFileInputReader == nil || nextFileInputReader.timestamp > fileInputReader.timestamp {
nextFileInputReader = fileInputReader
continue
}
}
return nextFileInputReader;
}
func (i *FileInput) emit() {
var lastTime int64 = -1
for {
fileInputReader := i.nextInputReader()
if fileInputReader == nil {
if i.loop {
i.init()
lastTime = -1
continue
} else {
break;
}
}
if fileInputReader.meta[0][0] == RequestPayload {
lastTime = i.simulateRequestDelay(fileInputReader, lastTime)
}
select {
case <-i.exit:
for _, fileInputReader := range i.fileInputReaders {
if fileInputReader.file != nil {
fileInputReader.file.Close()
}
}
break
case i.data <- fileInputReader.data:
fileInputReader.readNextInput()
}
}
log.Printf("FileInput: end of file '%s'\n", i.path)
}
func (i*FileInput) simulateRequestDelay(fileInputReader *fileInputReader, lastTime int64) int64 {
if lastTime != -1 {
timeDiff := fileInputReader.timestamp - lastTime
if i.speedFactor != 1 {
timeDiff = int64(float64(timeDiff) / i.speedFactor)
}
time.Sleep(time.Duration(timeDiff))
}
return fileInputReader.timestamp
}
func (i *FileInput) Close() error {
i.exit <- true
return nil
}
+384
View File
@@ -0,0 +1,384 @@
package main
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"os"
"sync"
"syscall"
"testing"
"time"
)
var _ = log.Println
func TestInputFileWithGET(t *testing.T) {
input := NewTestInput()
rg := NewRequestGenerator([]io.Reader{input}, func() { input.EmitGET() }, 1)
readPayloads := [][]byte{}
// Given a capture file with a GET request
expectedCaptureFile := CreateCaptureFile(rg)
defer expectedCaptureFile.TearDown()
// When the request is read from the capture file
err := ReadFromCaptureFile(expectedCaptureFile.file, 1, func(data []byte) {
readPayloads = append(readPayloads, Duplicate(data))
})
// The read request should match the original request
if err != nil {
t.Error(err)
} else {
if !expectedCaptureFile.PayloadsEqual(readPayloads) {
t.Error("Request read back from file should match")
}
}
}
func TestInputFileWithPayloadLargerThan64Kb(t *testing.T) {
input := NewTestInput()
rg := NewRequestGenerator([]io.Reader{input}, func() { input.EmitSizedPOST(64 * 1024) }, 1)
readPayloads := [][]byte{}
// Given a capture file with a request over 64Kb
expectedCaptureFile := CreateCaptureFile(rg)
defer expectedCaptureFile.TearDown()
// When the request is read from the capture file
err := ReadFromCaptureFile(expectedCaptureFile.file, 1, func(data []byte) {
readPayloads = append(readPayloads, Duplicate(data))
})
// The read request should match the original request
if err != nil {
t.Error(err)
} else {
if !expectedCaptureFile.PayloadsEqual(readPayloads) {
t.Error("Request read back from file should match")
}
}
}
func TestInputFileWithGETAndPOST(t *testing.T) {
input := NewTestInput()
rg := NewRequestGenerator([]io.Reader{input}, func() {
input.EmitGET()
input.EmitPOST()
}, 2)
readPayloads := [][]byte{}
// Given a capture file with a GET request
expectedCaptureFile := CreateCaptureFile(rg)
defer expectedCaptureFile.TearDown()
// When the requests are read from the capture file
err := ReadFromCaptureFile(expectedCaptureFile.file, 2, func(data []byte) {
readPayloads = append(readPayloads, Duplicate(data))
})
// The read requests should match the original request
if err != nil {
t.Error(err)
} else {
if !expectedCaptureFile.PayloadsEqual(readPayloads) {
t.Error("Request read back from file should match")
}
}
}
func TestInputFileMultipleFilesWithRequestsOnly(t *testing.T) {
rnd := rand.Int63()
file1, _ := os.OpenFile(fmt.Sprintf("/tmp/%d_0", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
file1.Write([]byte("1 1 1\ntest1"))
file1.Write([]byte(payloadSeparator))
file1.Write([]byte("1 1 3\ntest2"))
file1.Write([]byte(payloadSeparator))
file1.Close()
file2, _ := os.OpenFile(fmt.Sprintf("/tmp/%d_1", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
file2.Write([]byte("1 1 2\ntest3"))
file2.Write([]byte(payloadSeparator))
file2.Write([]byte("1 1 4\ntest4"))
file2.Write([]byte(payloadSeparator))
file2.Close()
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false)
buf := make([]byte, 1000)
for i := '1'; i <= '4'; i++ {
n, _ := input.Read(buf)
if buf[4] != byte(i) {
t.Error("Should emit requests in right order", string(buf[:n]))
}
}
os.Remove(file1.Name())
os.Remove(file2.Name())
}
func TestInputFileRequestsWithLatency(t *testing.T) {
rnd := rand.Int63()
file, _ := os.OpenFile(fmt.Sprintf("/tmp/%d", rnd), os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0660)
defer file.Close()
file.Write([]byte("1 1 100000000\nrequest1"))
file.Write([]byte(payloadSeparator))
file.Write([]byte("1 2 150000000\nrequest2"))
file.Write([]byte(payloadSeparator))
file.Write([]byte("1 3 250000000\nrequest3"))
file.Write([]byte(payloadSeparator))
input := NewFileInput(fmt.Sprintf("/tmp/%d", rnd), false)
buf := make([]byte, 1000)
start := time.Now().UnixNano()
for i := 0; i < 3; i++ {
input.Read(buf)
}
end := time.Now().UnixNano()
var expectedLatency int64 = 250000000 - 100000000
realLatency := end - start
if realLatency < expectedLatency {
t.Errorf("Should emit requests respecting latency. Expected: %v, real: %v", expectedLatency, realLatency)
}
if realLatency > expectedLatency + 10000000 {
t.Errorf("Should emit requests respecting latency. Expected: %v, real: %v", expectedLatency, realLatency)
}
}
func TestInputFileMultipleFilesWithRequestsAndResponses(t *testing.T) {
rnd := rand.Int63()
file1, _ := os.OpenFile(fmt.Sprintf("/tmp/%d_0", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
file1.Write([]byte("1 1 1\nrequest1"))
file1.Write([]byte(payloadSeparator))
file1.Write([]byte("2 1 1\nresponse1"))
file1.Write([]byte(payloadSeparator))
file1.Write([]byte("1 2 3\nrequest2"))
file1.Write([]byte(payloadSeparator))
file1.Write([]byte("2 2 3\nresponse2"))
file1.Write([]byte(payloadSeparator))
file1.Close()
file2, _ := os.OpenFile(fmt.Sprintf("/tmp/%d_1", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
file2.Write([]byte("1 3 2\nrequest3"))
file2.Write([]byte(payloadSeparator))
file2.Write([]byte("2 3 2\nresponse3"))
file2.Write([]byte(payloadSeparator))
file2.Write([]byte("1 4 4\nrequest4"))
file2.Write([]byte(payloadSeparator))
file2.Write([]byte("2 4 4\nresponse4"))
file2.Write([]byte(payloadSeparator))
file2.Close()
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false)
buf := make([]byte, 1000)
for i := '1'; i <= '4'; i++ {
n, _ := input.Read(buf)
if buf[0] != '1' && buf[4] != byte(i) {
t.Error("Shound emit requests in right order", string(buf[:n]))
}
n, _ = input.Read(buf)
if buf[0] != '2' && buf[4] != byte(i) {
t.Error("Shound emit responses in right order", string(buf[:n]))
}
}
os.Remove(file1.Name())
os.Remove(file2.Name())
}
func TestInputFileLoop(t *testing.T) {
rnd := rand.Int63()
file, _ := os.OpenFile(fmt.Sprintf("/tmp/%d", rnd), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
file.Write([]byte("1 1 1\ntest1"))
file.Write([]byte(payloadSeparator))
file.Write([]byte("1 1 2\ntest2"))
file.Write([]byte(payloadSeparator))
file.Close()
input := NewFileInput(fmt.Sprintf("/tmp/%d", rnd), true)
buf := make([]byte, 1000)
// Even if we have just 2 requests in file, it should indifinitly loop
for i := 0; i < 1000; i++ {
input.Read(buf)
}
input.Close()
os.Remove(file.Name())
}
func TestInputFileCompressed(t *testing.T) {
rnd := rand.Int63()
output := NewFileOutput(fmt.Sprintf("/tmp/%d_0.gz", rnd), &FileOutputConfig{flushInterval: time.Minute, append: true})
for i := 0; i < 1000; i++ {
output.Write([]byte("1 1 1\r\ntest"))
}
name1 := output.file.Name()
output.Close()
output2 := NewFileOutput(fmt.Sprintf("/tmp/%d_1.gz", rnd), &FileOutputConfig{flushInterval: time.Minute, append: true})
for i := 0; i < 1000; i++ {
output2.Write([]byte("1 1 1\r\ntest"))
}
name2 := output2.file.Name()
output2.Close()
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false)
buf := make([]byte, 1000)
for i := 0; i < 2000; i++ {
input.Read(buf)
}
os.Remove(name1)
os.Remove(name2)
}
type CaptureFile struct {
data [][]byte
file *os.File
}
func NewExpectedCaptureFile(data [][]byte, file *os.File) *CaptureFile {
ecf := new(CaptureFile)
ecf.file = file
ecf.data = data
return ecf
}
func (expectedCaptureFile *CaptureFile) TearDown() {
if expectedCaptureFile.file != nil {
syscall.Unlink(expectedCaptureFile.file.Name())
}
}
type RequestGenerator struct {
inputs []io.Reader
emit func()
wg *sync.WaitGroup
}
func NewRequestGenerator(inputs []io.Reader, emit func(), count int) (rg *RequestGenerator) {
rg = new(RequestGenerator)
rg.inputs = inputs
rg.emit = emit
rg.wg = new(sync.WaitGroup)
rg.wg.Add(count)
return
}
func (expectedCaptureFile *CaptureFile) PayloadsEqual(other [][]byte) bool {
if len(expectedCaptureFile.data) != len(other) {
return false
}
for i, payload := range other {
if !bytes.Equal(expectedCaptureFile.data[i], payload) {
return false
}
}
return true
}
func CreateCaptureFile(requestGenerator *RequestGenerator) *CaptureFile {
f, err := ioutil.TempFile("", "testmainconf")
if err != nil {
panic(err)
}
quit := make(chan int)
readPayloads := [][]byte{}
output := NewTestOutput(func(data []byte) {
readPayloads = append(readPayloads, Duplicate(data))
requestGenerator.wg.Done()
})
outputFile := NewFileOutput(f.Name(), &FileOutputConfig{flushInterval: time.Minute, append: true})
Plugins.Inputs = requestGenerator.inputs
Plugins.Outputs = []io.Writer{output, outputFile}
go Start(quit)
requestGenerator.emit()
requestGenerator.wg.Wait()
time.Sleep(100 * time.Millisecond)
outputFile.Close()
close(quit)
return NewExpectedCaptureFile(readPayloads, f)
}
func ReadFromCaptureFile(captureFile *os.File, count int, callback writeCallback) (err error) {
quit := make(chan int)
wg := new(sync.WaitGroup)
input := NewFileInput(captureFile.Name(), false)
output := NewTestOutput(func(data []byte) {
callback(data)
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
wg.Add(count)
go Start(quit)
done := make(chan int, 1)
go func() {
wg.Wait()
done <- 1
}()
select {
case <-done:
break
case <-time.After(2 * time.Second):
err = errors.New("Timed out")
}
close(quit)
return
}
func Duplicate(data []byte) (duplicate []byte) {
duplicate = make([]byte, len(data))
copy(duplicate, data)
return
}
+76
View File
@@ -0,0 +1,76 @@
package main
import (
"log"
"net"
"net/http"
"net/http/httputil"
"time"
)
// HTTPInput used for sending requests to Gor via http
type HTTPInput struct {
data chan []byte
address string
listener net.Listener
}
// NewHTTPInput constructor for HTTPInput. Accepts address with port which he will listen on.
func NewHTTPInput(address string) (i *HTTPInput) {
i = new(HTTPInput)
i.data = make(chan []byte, 10000)
i.address = address
i.listen(address)
return
}
func (i *HTTPInput) Read(data []byte) (int, error) {
buf := <-i.data
header := payloadHeader(RequestPayload, uuid(), time.Now().UnixNano())
copy(data[0:len(header)], header)
copy(data[len(header):], buf)
return len(buf) + len(header), nil
}
func (i *HTTPInput) handler(w http.ResponseWriter, r *http.Request) {
r.URL.Scheme = "http"
r.URL.Host = i.listener.Addr().String()
buf, _ := httputil.DumpRequestOut(r, true)
http.Error(w, http.StatusText(200), 200)
select {
case i.data <- buf:
default:
Debug("[INPUT-HTTP] Dropping requests because output can't process them fast enough")
}
}
func (i *HTTPInput) listen(address string) {
var err error
mux := http.NewServeMux()
mux.HandleFunc("/", i.handler)
i.listener, err = net.Listen("tcp", address)
if err != nil {
log.Fatal("HTTP input listener failure:", err)
}
go func() {
err = http.Serve(i.listener, mux)
if err != nil {
log.Fatal("HTTP input serve failure:", err)
}
}()
}
func (i *HTTPInput) String() string {
return "HTTP input: " + i.address
}
+72
View File
@@ -0,0 +1,72 @@
package main
import (
"github.com/buger/gor/proto"
"io"
"log"
"net/http"
"os/exec"
"strings"
"sync"
"testing"
)
func TestHTTPInput(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewHTTPInput("127.0.0.1:0")
output := NewTestOutput(func(data []byte) {
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
address := strings.Replace(input.listener.Addr().String(), "[::]", "127.0.0.1", -1)
for i := 0; i < 100; i++ {
wg.Add(1)
http.Get("http://" + address + "/")
}
wg.Wait()
close(quit)
}
func TestInputHTTPLargePayload(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
dd := exec.Command("dd", "if=/dev/urandom", "of=/tmp/large", "bs=1", "count=4000000")
err := dd.Run()
if err != nil {
log.Fatal("dd error:", err)
}
input := NewHTTPInput("127.0.0.1:0")
output := NewTestOutput(func(data []byte) {
if len(proto.Body(payloadBody(data))) != 4000000 {
t.Error("Should receive full file")
}
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
wg.Add(1)
address := strings.Replace(input.listener.Addr().String(), "[::]", "127.0.0.1", -1)
curl := exec.Command("curl", "http://"+address, "--data-binary", "@/tmp/large")
err = curl.Run()
if err != nil {
log.Fatal("curl error:", err)
}
wg.Wait()
close(quit)
}
+104
View File
@@ -0,0 +1,104 @@
package main
import (
"github.com/buger/gor/proto"
raw "github.com/buger/gor/raw_socket_listener"
"log"
"net"
"time"
)
// RAWInput used for intercepting traffic for given address
type RAWInput struct {
data chan *raw.TCPMessage
address string
expire time.Duration
quit chan bool
engine int
realIPHeader []byte
trackResponse bool
listener *raw.Listener
}
// Available engines for intercepting traffic
const (
EngineRawSocket = 1 << iota
EnginePcap
)
// NewRAWInput constructor for RAWInput. Accepts address with port as argument.
func NewRAWInput(address string, engine int, trackResponse bool, expire time.Duration, realIPHeader string) (i *RAWInput) {
i = new(RAWInput)
i.data = make(chan *raw.TCPMessage)
i.address = address
i.expire = expire
i.engine = engine
i.realIPHeader = []byte(realIPHeader)
i.quit = make(chan bool)
i.trackResponse = trackResponse
i.listen(address)
i.listener.IsReady()
return
}
func (i *RAWInput) Read(data []byte) (int, error) {
msg := <-i.data
buf := msg.Bytes()
var header []byte
if msg.IsIncoming {
header = payloadHeader(RequestPayload, msg.UUID(), msg.Start.UnixNano())
if len(i.realIPHeader) > 0 {
buf = proto.SetHeader(buf, i.realIPHeader, []byte(msg.IP().String()))
}
} else {
header = payloadHeader(ResponsePayload, msg.UUID(), msg.End.UnixNano()-msg.AssocMessage.Start.UnixNano())
}
copy(data[0:len(header)], header)
copy(data[len(header):], buf)
return len(buf) + len(header), nil
}
func (i *RAWInput) listen(address string) {
Debug("Listening for traffic on: " + address)
host, port, err := net.SplitHostPort(address)
if err != nil {
log.Fatal("input-raw: error while parsing address", err)
}
i.listener = raw.NewListener(host, port, i.engine, i.trackResponse, i.expire)
ch := i.listener.Receiver()
go func() {
for {
select {
case <-i.quit:
return
default:
}
// Receiving TCPMessage object
m := <-ch
i.data <- m
}
}()
}
func (i *RAWInput) String() string {
return "Intercepting traffic from: " + i.address
}
func (i *RAWInput) Close() error {
i.listener.Close()
close(i.quit)
return nil
}
+374
View File
@@ -0,0 +1,374 @@
package main
import (
"bytes"
"github.com/buger/gor/proto"
"io"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
const testRawExpire = time.Millisecond * 200
func TestRAWInputIPv4(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
listener, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
origin := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
go origin.Serve(listener)
defer listener.Close()
originAddr := listener.Addr().String()
var respCounter, reqCounter int64
input := NewRAWInput(originAddr, EnginePcap, true, testRawExpire, "X-Real-IP")
defer input.Close()
output := NewTestOutput(func(data []byte) {
if data[0] == '1' {
body := payloadBody(data)
if len(proto.Header(body, []byte("X-Real-IP"))) == 0 {
t.Error("Should have X-Real-IP header", string(body))
}
atomic.AddInt64(&reqCounter, 1)
} else {
atomic.AddInt64(&respCounter, 1)
}
if Settings.debug {
log.Println(reqCounter, respCounter)
}
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
client := NewHTTPClient("http://"+listener.Addr().String(), &HTTPClientConfig{})
go Start(quit)
for i := 0; i < 100; i++ {
// request + response
wg.Add(2)
client.Get("/")
time.Sleep(2 * time.Millisecond)
}
wg.Wait()
close(quit)
}
func TestRAWInputIPv6(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
listener, err := net.Listen("tcp", "[::1]:0")
if err != nil {
t.Fatal(err)
}
origin := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
go origin.Serve(listener)
defer listener.Close()
originAddr := listener.Addr().String()
var respCounter, reqCounter int64
input := NewRAWInput(originAddr, EnginePcap, true, testRawExpire, "")
defer input.Close()
output := NewTestOutput(func(data []byte) {
if data[0] == '1' {
atomic.AddInt64(&reqCounter, 1)
} else {
atomic.AddInt64(&respCounter, 1)
}
if Settings.debug {
log.Println(reqCounter, respCounter)
}
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
client := NewHTTPClient("http://"+listener.Addr().String(), &HTTPClientConfig{})
go Start(quit)
for i := 0; i < 100; i++ {
// request + response
wg.Add(2)
client.Get("/")
time.Sleep(2 * time.Millisecond)
}
wg.Wait()
close(quit)
}
func TestInputRAW100Expect(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
fileContent, _ := ioutil.ReadFile("COMM-LICENSE")
// Origing and Replay server initialization
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
ioutil.ReadAll(r.Body)
wg.Done()
}))
defer origin.Close()
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
input := NewRAWInput(originAddr, EnginePcap, true, time.Second, "")
defer input.Close()
// We will use it to get content of raw HTTP request
testOutput := NewTestOutput(func(data []byte) {
switch data[0] {
case RequestPayload:
if strings.Contains(string(data), "Expect: 100-continue") {
t.Error("Should not contain 100-continue header")
}
wg.Done()
case ResponsePayload:
wg.Done()
}
})
replay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, _ := ioutil.ReadAll(r.Body)
if !bytes.Equal(body, fileContent) {
buf, _ := httputil.DumpRequest(r, true)
t.Error("Wrong POST body:", string(buf))
}
wg.Done()
}))
defer replay.Close()
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{testOutput, httpOutput}
go Start(quit)
// Origin + Response/Request Test Output + Request Http Output
wg.Add(4)
curl := exec.Command("curl", "http://"+originAddr, "--data-binary", "@COMM-LICENSE")
err := curl.Run()
if err != nil {
log.Fatal(err)
}
wg.Wait()
close(quit)
}
func TestInputRAWChunkedEncoding(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
fileContent, _ := ioutil.ReadFile("README.md")
// Origing and Replay server initialization
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
ioutil.ReadAll(r.Body)
wg.Done()
}))
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
input := NewRAWInput(originAddr, EnginePcap, true, time.Second, "")
defer input.Close()
replay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, _ := ioutil.ReadAll(r.Body)
if !bytes.Equal(body, fileContent) {
buf, _ := httputil.DumpRequest(r, true)
t.Error("Wrong POST body:", string(buf))
}
wg.Done()
}))
defer replay.Close()
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{Debug: true})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{httpOutput}
go Start(quit)
wg.Add(2)
curl := exec.Command("curl", "http://"+originAddr, "--header", "Transfer-Encoding: chunked", "--header", "Expect:", "--data-binary", "@README.md")
err := curl.Run()
if err != nil {
log.Fatal(err)
}
wg.Wait()
close(quit)
}
func TestInputRAWLargePayload(t *testing.T) {
// FIXME: Large payloads does not work for travis for some reason...
if os.Getenv("TRAVIS_BUILD_DIR") != "" {
return
}
wg := new(sync.WaitGroup)
quit := make(chan int)
sizeB := 100 * 1000
// Generate 100kb file
dd := exec.Command("dd", "if=/dev/urandom", "of=/tmp/large", "bs=1", "count="+strconv.Itoa(sizeB))
err := dd.Run()
if err != nil {
log.Fatal("dd error:", err)
}
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
body, _ := ioutil.ReadAll(req.Body)
if len(body) != sizeB {
t.Error("File size should be 1mb:", len(body))
}
wg.Done()
}))
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
input := NewRAWInput(originAddr, EnginePcap, true, testRawExpire, "")
defer input.Close()
replay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
body, _ := ioutil.ReadAll(req.Body)
// // req.Body = http.MaxBytesReader(w, req.Body, 1*1024*1024)
// // buf := make([]byte, 1*1024*1024)
// n, _ := req.Body.Read(buf)
// body := buf[0:n]
if len(body) != sizeB {
t.Errorf("File size should be %d bytes: %d", sizeB, len(body))
}
wg.Done()
}))
defer replay.Close()
httpOutput := NewHTTPOutput(replay.URL, &HTTPOutputConfig{Debug: false})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{httpOutput}
go Start(quit)
wg.Add(2)
curl := exec.Command("curl", "http://"+originAddr, "--header", "Transfer-Encoding: chunked", "--header", "Expect:", "--data-binary", "@/tmp/large")
err = curl.Run()
if err != nil {
log.Fatal("curl error:", err)
}
wg.Wait()
close(quit)
}
func BenchmarkRAWInput(b *testing.B) {
quit := make(chan int)
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
defer origin.Close()
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
var respCounter, reqCounter int64
input := NewRAWInput(originAddr, EnginePcap, true, testRawExpire, "")
defer input.Close()
output := NewTestOutput(func(data []byte) {
if data[0] == '1' {
atomic.AddInt64(&reqCounter, 1)
} else {
atomic.AddInt64(&respCounter, 1)
}
// log.Println("Captured ", reqCounter, "requests and ", respCounter, " responses")
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
emitted := 0
fileContent, _ := ioutil.ReadFile("LICENSE.txt")
for i := 0; i < b.N; i++ {
wg := new(sync.WaitGroup)
wg.Add(10 * 100)
emitted += 10 * 100
for w := 0; w < 100; w++ {
go func() {
client := NewHTTPClient(origin.URL, &HTTPClientConfig{})
for i := 0; i < 10; i++ {
if rand.Int63n(2) == 0 {
client.Post("/", fileContent)
} else {
client.Get("/")
}
time.Sleep(time.Duration(rand.Int63n(50)) * time.Millisecond)
wg.Done()
}
}()
}
wg.Wait()
}
time.Sleep(400 * time.Millisecond)
log.Println("Emitted ", emitted, ", Captured ", reqCounter, "requests and ", respCounter, " responses")
close(quit)
}
+94
View File
@@ -0,0 +1,94 @@
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"net"
"os"
)
// TCPInput used for internal communication
type TCPInput struct {
data chan []byte
address string
listener net.Listener
}
// NewTCPInput constructor for TCPInput, accepts address with port
func NewTCPInput(address string) (i *TCPInput) {
i = new(TCPInput)
i.data = make(chan []byte, 1000)
i.address = address
i.listen(address)
return
}
func (i *TCPInput) Read(data []byte) (int, error) {
buf := <-i.data
copy(data, buf)
return len(buf), nil
}
func (i *TCPInput) listen(address string) {
listener, err := net.Listen("tcp", address)
i.listener = listener
if err != nil {
log.Fatal("Can't start:", err)
}
go func() {
for {
conn, err := listener.Accept()
if err != nil {
log.Println("Error while Accept()", err)
continue
}
go i.handleConnection(conn)
}
}()
}
func (i *TCPInput) handleConnection(conn net.Conn) {
defer conn.Close()
payloadSeparatorAsBytes := []byte(payloadSeparator)
reader := bufio.NewReader(conn)
var buffer bytes.Buffer
for {
line, err := reader.ReadBytes('\n')
if err != nil {
if err != io.EOF {
fmt.Fprintln(os.Stderr, "Unexpected error in input tcp connection:", err)
}
break
}
if bytes.Equal(payloadSeparatorAsBytes[1:], line) {
asBytes := buffer.Bytes()
buffer.Reset()
newBuf := make([]byte, len(asBytes)-1)
copy(newBuf, asBytes)
i.data <- newBuf
} else {
buffer.Write(line)
}
}
}
func (i *TCPInput) String() string {
return "TCP input: " + i.address
}
+48
View File
@@ -0,0 +1,48 @@
package main
import (
"io"
"log"
"net"
"sync"
"testing"
)
func TestTCPInput(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTCPInput("127.0.0.1:0")
output := NewTestOutput(func(data []byte) {
wg.Done()
})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
tcpAddr, err := net.ResolveTCPAddr("tcp", input.listener.Addr().String())
if err != nil {
log.Fatal(err)
}
conn, err := net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
log.Fatal(err)
}
msg := []byte("1 1 1\nGET / HTTP/1.1\r\n\r\n")
for i := 0; i < 100; i++ {
wg.Add(1)
conn.Write(msg)
conn.Write([]byte(payloadSeparator))
}
wg.Wait()
close(quit)
}
+96
View File
@@ -0,0 +1,96 @@
package main
import (
"fmt"
"io"
"math/rand"
"strconv"
"strings"
"time"
)
// Limiter is a wrapper for input or output plugin which adds rate limiting
type Limiter struct {
plugin interface{}
limit int
isPercent bool
currentRPS int
currentTime int64
}
func parseLimitOptions(options string) (limit int, isPercent bool) {
if strings.Contains(options, "%") {
limit, _ = strconv.Atoi(strings.Split(options, "%")[0])
isPercent = true
} else {
limit, _ = strconv.Atoi(options)
isPercent = false
}
return
}
// NewLimiter constructor for Limiter, accepts plugin and options
// `options` allow to sprcify relatve or absolute limiting
func NewLimiter(plugin interface{}, options string) io.ReadWriter {
l := new(Limiter)
l.limit, l.isPercent = parseLimitOptions(options)
l.plugin = plugin
l.currentTime = time.Now().UnixNano()
// FileInput have its own rate limiting. Unlike other inputs we not just dropping requests, we can slow down or speed up request emittion.
if fi, ok := l.plugin.(*FileInput); ok && l.isPercent {
fi.speedFactor = float64(l.limit) / float64(100)
}
return l
}
func (l *Limiter) isLimited() bool {
// File input have its own limiting algorithm
if _, ok := l.plugin.(*FileInput); ok && l.isPercent {
return false
}
if l.isPercent {
return l.limit <= rand.Intn(100)
}
if (time.Now().UnixNano() - l.currentTime) > time.Second.Nanoseconds() {
l.currentTime = time.Now().UnixNano()
l.currentRPS = 0
}
if l.currentRPS >= l.limit {
return true
}
l.currentRPS++
return false
}
func (l *Limiter) Write(data []byte) (n int, err error) {
if l.isLimited() {
return 0, nil
}
n, err = l.plugin.(io.Writer).Write(data)
return
}
func (l *Limiter) Read(data []byte) (n int, err error) {
n, err = l.plugin.(io.Reader).Read(data)
if l.isLimited() {
return 0, nil
}
return
}
func (l *Limiter) String() string {
return fmt.Sprintf("Limiting %s to: %d (isPercent: %b)", l.plugin, l.limit, l.isPercent)
}
+106
View File
@@ -0,0 +1,106 @@
// +build !race
package main
import (
"io"
"sync"
"testing"
)
func TestOutputLimiter(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
output := NewLimiter(NewTestOutput(func(data []byte) {
wg.Done()
}), "10")
wg.Add(10)
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
for i := 0; i < 100; i++ {
input.EmitGET()
}
wg.Wait()
close(quit)
}
func TestInputLimiter(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewLimiter(NewTestInput(), "10")
output := NewTestOutput(func(data []byte) {
wg.Done()
})
wg.Add(10)
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
for i := 0; i < 100; i++ {
input.(*Limiter).plugin.(*TestInput).EmitGET()
}
wg.Wait()
close(quit)
}
// Should limit all requests
func TestPercentLimiter1(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
output := NewLimiter(NewTestOutput(func(data []byte) {
wg.Done()
}), "0%")
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
for i := 0; i < 100; i++ {
input.EmitGET()
}
wg.Wait()
close(quit)
}
// Should not limit at all
func TestPercentLimiter2(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
output := NewLimiter(NewTestOutput(func(data []byte) {
wg.Done()
}), "100%")
wg.Add(100)
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
for i := 0; i < 100; i++ {
input.EmitGET()
}
wg.Wait()
close(quit)
}
-80
View File
@@ -1,80 +0,0 @@
// Listener capture TCP traffic using RAW SOCKETS.
// Note: it requires sudo or root access.
//
// Rigt now it suport only HTTP, and only GET requests.
package listener
import (
"bufio"
"bytes"
"fmt"
"log"
"net"
"net/http"
"os"
)
type HttpRequest struct {
Tag string // Not used yet
Method string // Right now only 'GET'
Url string // Request URL
Headers map[string]string // Request Headers
}
// Enable debug logging only if "--verbose" flag passed
func Debug(v ...interface{}) {
if Settings.verbose {
log.Println(v...)
}
}
func greeting() {
}
// Because its sub-program, Run acts as `main`
func Run() {
if os.Getuid() != 0 {
fmt.Println("Please start the listener as root or sudo!")
fmt.Println("This is required since listener sniff traffic on given port.")
os.Exit(1)
}
fmt.Println("Listening for HTTP traffic on", Settings.port, "port")
fmt.Println("Forwarding requests to replay server:", Settings.ReplayServer())
// Connection to reaplay server
serverAddr, err := net.ResolveUDPAddr("udp4", Settings.ReplayServer())
conn, err := net.DialUDP("udp", nil, serverAddr)
if err != nil {
log.Fatal("Connection error", err)
}
// Sniffing traffic from given port
listener := RAWTCPListen("0.0.0.0", Settings.port)
for {
message := listener.Receive()
go func(m *TCPMessage) {
if Settings.verbose {
buf := bytes.NewBuffer(m.Bytes())
reader := bufio.NewReader(buf)
request, err := http.ReadRequest(reader)
if err != nil {
Debug("Error while parsing request:", string(m.Bytes()))
} else {
request.ParseMultipartForm(32 << 20)
Debug("Forwarding request:", request)
}
}
conn.Write(m.Bytes())
}(message)
}
conn.Close()
}
-105
View File
@@ -1,105 +0,0 @@
package listener
import (
"log"
"net"
"time"
)
type RAWTCPListener struct {
messages map[uint32]*TCPMessage // buffer of TCPMessages waiting to be send
c_packets chan *TCPPacket
c_messages chan *TCPMessage
addr string
port int
}
func RAWTCPListen(addr string, port int) (listener *RAWTCPListener) {
listener = &RAWTCPListener{}
listener.messages = make(map[uint32]*TCPMessage)
listener.c_packets = make(chan *TCPPacket)
listener.c_messages = make(chan *TCPMessage)
listener.addr = addr
listener.port = port
go listener.listen()
go listener.readTCPPackets()
return
}
func (t *RAWTCPListener) listen() {
for {
var messages chan *TCPMessage
var message *TCPMessage
for _, msg := range t.messages {
if msg.Complete() {
messages = t.c_messages
message = msg
}
}
select {
case messages <- message:
delete(t.messages, message.ask)
case packet := <-t.c_packets:
t.processTCPPacket(packet)
// Ensure that this will be run at least each 200 ms, to ensure that all messages will be send
// Without it last message may not be send (it will be send only on next TCP packets)
case <-time.After(200 * time.Millisecond):
}
}
}
func (t *RAWTCPListener) readTCPPackets() {
conn, e := net.ListenPacket("ip4:tcp", t.addr)
defer conn.Close()
if e != nil {
log.Fatal(e)
}
for {
buf := make([]byte, 65565)
n, _, err := conn.ReadFrom(buf)
if err != nil {
Debug("Error:", err)
}
if n > 0 {
packet := NewTCPPacket(buf[:n])
if int(packet.dest_port) == t.port {
t.c_packets <- packet
}
}
}
}
//
func (t *RAWTCPListener) processTCPPacket(packet *TCPPacket) {
// We interested only in packets that contain some data
if !(packet.f_ask && packet.f_psh) {
return
}
ask := packet.asknowledgement
if _, ok := t.messages[ask]; !ok {
t.messages[ask] = NewTCPMessage(ask)
}
t.messages[ask].AddPacket(packet)
}
func (t *RAWTCPListener) Receive() *TCPMessage {
return <-t.c_messages
}
-47
View File
@@ -1,47 +0,0 @@
package listener
import (
"flag"
"os"
"strings"
)
const (
defaultPort = 80
defaultNetworkInterface = "any"
defaultReplayAddress = "localhost:28020"
)
type ListenerSettings struct {
networkInterface string
port int
replayAddress string
verbose bool
}
var Settings ListenerSettings = ListenerSettings{}
func (s *ListenerSettings) ReplayServer() string {
if !strings.Contains(s.replayAddress, ":") {
return s.replayAddress + ":28020"
}
return s.replayAddress
}
func init() {
if len(os.Args) < 2 || os.Args[1] != "listen" {
return
}
flag.IntVar(&Settings.port, "p", defaultPort, "Specify the http server port whose traffic you want to capture")
flag.StringVar(&Settings.networkInterface, "i", defaultNetworkInterface, "By default it try to listen on all network interfaces.To get list of interfaces run `ifconfig`")
flag.StringVar(&Settings.replayAddress, "r", defaultReplayAddress, "Address of replay server.")
flag.BoolVar(&Settings.verbose, "verbose", false, "Log requests")
}
-64
View File
@@ -1,64 +0,0 @@
package listener
import (
"sort"
"time"
)
// TCPMessage ensure that all TCP packets for given request is received, and processed in right sequence
// Its needed because all TCP message can be fragmented or re-transmitted
//
// Each TCP Packet have 2 ids: asknowledgement - message_id, and sequence - packet_id
// Message can be compiled from unique packets with same message_id which sorted by sequence
// Message is received if we did't receive any packets for 200ms OR if we received packet with "fin" flag
type TCPMessage struct {
ask uint32 // Message ID
packets map[int]*TCPPacket // map[packet.sequence]*TCPPacket
updated int64 // time of last packet
}
func NewTCPMessage(ask uint32) (msg *TCPMessage) {
msg = &TCPMessage{}
msg.packets = make(map[int]*TCPPacket)
msg.updated = time.Now().UnixNano()
msg.ask = ask
return
}
// Sort packets in right orders and return message content
func (t *TCPMessage) Bytes() (output []byte) {
mk := make([]int, len(t.packets))
i := 0
for k, _ := range t.packets {
mk[i] = k
i++
}
sort.Ints(mk)
for _, k := range mk {
output = append(output, t.packets[k].data...)
}
return
}
// Add packet to the message
func (t *TCPMessage) AddPacket(packet *TCPPacket) {
seq := int(packet.sequence)
if _, ok := t.packets[seq]; !ok {
t.packets[seq] = packet
} else {
Debug("Received packet with same sequence")
}
t.updated = time.Now().UnixNano()
}
// TCP message is complete if we not received any packets for 200ms since last packet
func (t *TCPMessage) Complete() bool {
ns := time.Now().UnixNano()
return (ns - t.updated) > int64(200*time.Millisecond)
}
-106
View File
@@ -1,106 +0,0 @@
package listener
import (
"bytes"
"encoding/binary"
"strconv"
"strings"
)
// Simple TCP packet parser
//
// Packet structure: http://en.wikipedia.org/wiki/Transmission_Control_Protocol
type TCPPacket struct {
packet []byte
buf *bytes.Buffer
source_port uint16
dest_port uint16
sequence uint32
asknowledgement uint32
doff_reserved uint16
tcph_length uint16
f_ns bool
f_crw bool
f_ece bool
f_urg bool
f_ask bool
f_psh bool
f_rst bool
f_syn bool
f_fin bool
window_size uint16
checksum uint16
data []byte
}
func NewTCPPacket(b []byte) (p *TCPPacket) {
buf := bytes.NewBuffer(b)
p = &TCPPacket{packet: b, buf: buf}
p.Parse()
return p
}
// Helper for binary.Read
func (t *TCPPacket) read(data interface{}) {
binary.Read(t.buf, binary.BigEndian, data)
}
// Inspired by: https://gist.github.com/clicube/4978853
func (t *TCPPacket) Parse() {
t.read(&t.source_port)
t.read(&t.dest_port)
t.read(&t.sequence)
t.read(&t.asknowledgement)
t.read(&t.doff_reserved)
t.tcph_length = t.doff_reserved >> 12 * 4
t.f_ns = (t.doff_reserved & 256) != 0
t.f_crw = (t.doff_reserved & 128) != 0
t.f_ece = (t.doff_reserved & 64) != 0
t.f_urg = (t.doff_reserved & 32) != 0
t.f_ask = (t.doff_reserved & 16) != 0
t.f_psh = (t.doff_reserved & 8) != 0
t.f_rst = (t.doff_reserved & 4) != 0
t.f_syn = (t.doff_reserved & 2) != 0
t.f_fin = (t.doff_reserved & 1) != 0
t.read(&t.window_size)
t.read(&t.checksum)
t.data = t.packet[t.tcph_length:]
}
func (t *TCPPacket) String() string {
return strings.Join([]string{
"Source port: " + strconv.Itoa(int(t.source_port)),
"Dest port:" + strconv.Itoa(int(t.dest_port)),
"Sequence:" + strconv.Itoa(int(t.sequence)),
"Acknowledgement:" + strconv.Itoa(int(t.asknowledgement)),
"Header len:" + strconv.Itoa(int(t.tcph_length)),
"Flag ns:" + strconv.FormatBool(t.f_ns),
"Flag crw:" + strconv.FormatBool(t.f_crw),
"Flag ece:" + strconv.FormatBool(t.f_ece),
"Flag urg:" + strconv.FormatBool(t.f_urg),
"Flag ask:" + strconv.FormatBool(t.f_ask),
"Flag psh:" + strconv.FormatBool(t.f_psh),
"Flag rst:" + strconv.FormatBool(t.f_rst),
"Flag syn:" + strconv.FormatBool(t.f_syn),
"Flag fin:" + strconv.FormatBool(t.f_fin),
"Window size:" + strconv.Itoa(int(t.window_size)),
"Checksum:" + strconv.Itoa(int(t.checksum)),
"Data:" + string(t.data),
}, "\n")
}
+125
View File
@@ -0,0 +1,125 @@
package main
import (
"bufio"
"encoding/hex"
"fmt"
"io"
"log"
"os"
"os/exec"
"strings"
"sync"
)
type Middleware struct {
command string
data chan []byte
mu sync.Mutex
Stdin io.Writer
Stdout io.Reader
}
func NewMiddleware(command string) *Middleware {
m := new(Middleware)
m.command = command
m.data = make(chan []byte, 1000)
commands := strings.Split(command, " ")
cmd := exec.Command(commands[0], commands[1:]...)
m.Stdout, _ = cmd.StdoutPipe()
m.Stdin, _ = cmd.StdinPipe()
if Settings.verbose {
cmd.Stderr = os.Stderr
}
go m.read(m.Stdout)
go func() {
err := cmd.Start()
if err != nil {
log.Fatal(err)
}
err = cmd.Wait()
if err != nil {
log.Fatal(err)
}
}()
return m
}
func (m *Middleware) ReadFrom(plugin io.Reader) {
Debug("[MIDDLEWARE-MASTER] Starting reading from", plugin)
go m.copy(m.Stdin, plugin)
}
func (m *Middleware) copy(to io.Writer, from io.Reader) {
buf := make([]byte, 5*1024*1024)
dst := make([]byte, len(buf)*2)
for {
nr, _ := from.Read(buf)
if nr > 0 && len(buf) > nr {
hex.Encode(dst, buf[0:nr])
dst[nr*2] = '\n'
m.mu.Lock()
to.Write(dst[0 : nr*2+1])
m.mu.Unlock()
if Settings.debug {
Debug("[MIDDLEWARE-MASTER] Sending:", string(buf[0:nr]), "From:", from)
}
}
}
}
func (m *Middleware) read(from io.Reader) {
reader := bufio.NewReader(from)
var line []byte
var e error
for {
if line, e = reader.ReadBytes('\n'); e != nil {
if e == io.EOF {
continue
} else {
break
}
}
buf := make([]byte, len(line)/2)
if _, err := hex.Decode(buf, line[:len(line)-1]); err != nil {
fmt.Fprintln(os.Stderr, "Failed to decode input payload", err, len(line))
}
if Settings.debug {
Debug("[MIDDLEWARE-MASTER] Received:", string(buf))
}
m.data <- buf
}
return
}
func (m *Middleware) Read(data []byte) (int, error) {
buf := <-m.data
copy(data, buf)
return len(buf), nil
}
func (m *Middleware) String() string {
return fmt.Sprintf("Modifying traffic using '%s' command", m.command)
}
+220
View File
@@ -0,0 +1,220 @@
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"github.com/buger/gor/proto"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
)
type fakeServiceCb func(string, int, []byte)
// Simple service that generate token on request, and require this token for accesing to secure area
func NewFakeSecureService(wg *sync.WaitGroup, cb fakeServiceCb) *httptest.Server {
active_tokens := make([]string, 0)
var mu sync.Mutex
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
mu.Lock()
defer mu.Unlock()
Debug("Received request: " + req.URL.String())
switch req.URL.Path {
case "/token":
// Generate random token
token_length := 10
buf := make([]byte, token_length)
rand.Read(buf)
token := hex.EncodeToString(buf)
active_tokens = append(active_tokens, token)
w.Write([]byte(token))
cb(req.URL.Path, 200, []byte(token))
case "/secure":
token := req.URL.Query().Get("token")
token_found := false
for _, t := range active_tokens {
if t == token {
token_found = true
break
}
}
if token_found {
w.WriteHeader(http.StatusAccepted)
cb(req.URL.Path, 202, []byte(nil))
} else {
w.WriteHeader(http.StatusForbidden)
cb(req.URL.Path, 403, []byte(nil))
}
}
wg.Done()
}))
return server
}
func TestFakeSecureService(t *testing.T) {
var resp, token []byte
wg := new(sync.WaitGroup)
server := NewFakeSecureService(wg, func(path string, status int, resp []byte) {
})
defer server.Close()
wg.Add(3)
client := NewHTTPClient(server.URL, &HTTPClientConfig{Debug: true})
resp, _ = client.Get("/token")
token = proto.Body(resp)
// Right token
resp, _ = client.Get("/secure?token=" + string(token))
if !bytes.Equal(proto.Status(resp), []byte("202")) {
t.Error("Valid token should return status 202:", string(proto.Status(resp)))
}
// Wrong tokens forbidden
resp, _ = client.Get("/secure?token=wrong")
if !bytes.Equal(proto.Status(resp), []byte("403")) {
t.Error("Wrong token should returns status 403:", string(proto.Status(resp)))
}
wg.Wait()
}
func TestEchoMiddleware(t *testing.T) {
wg := new(sync.WaitGroup)
from := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Env", "prod")
w.Header().Set("RequestPath", r.URL.Path)
wg.Done()
}))
defer from.Close()
to := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Env", "test")
w.Header().Set("RequestPath", r.URL.Path)
wg.Done()
}))
defer to.Close()
quit := make(chan int)
Settings.middleware = "./examples/middleware/echo.sh"
// Catch traffic from one service
fromAddr := strings.Replace(from.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
input := NewRAWInput(fromAddr, EnginePcap, true, testRawExpire, "")
defer input.Close()
// And redirect to another
output := NewHTTPOutput(to.URL, &HTTPOutputConfig{Debug: false})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
// Start Gor
go Start(quit)
// Wait till middleware initialization
time.Sleep(100 * time.Millisecond)
// Should receive 2 requests from original + 2 from replayed
client := NewHTTPClient(from.URL, &HTTPClientConfig{Debug: false})
for i := 0; i < 10; i++ {
wg.Add(4)
// Request should be echoed
client.Get("/a")
time.Sleep(5 * time.Millisecond)
client.Get("/b")
time.Sleep(5 * time.Millisecond)
}
wg.Wait()
close(quit)
time.Sleep(200 * time.Millisecond)
Settings.middleware = ""
}
func TestTokenMiddleware(t *testing.T) {
var resp, token []byte
wg := new(sync.WaitGroup)
from := NewFakeSecureService(wg, func(path string, status int, tok []byte) {
time.Sleep(10 * time.Millisecond)
})
defer from.Close()
to := NewFakeSecureService(wg, func(path string, status int, tok []byte) {
switch path {
case "/secure":
if status != 202 {
t.Error("Server should receive valid rewritten token")
}
}
time.Sleep(10 * time.Millisecond)
})
defer to.Close()
quit := make(chan int)
Settings.middleware = "go run ./examples/middleware/token_modifier.go"
fromAddr := strings.Replace(from.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
// Catch traffic from one service
input := NewRAWInput(fromAddr, EnginePcap, true, testRawExpire, "")
defer input.Close()
// And redirect to another
output := NewHTTPOutput(to.URL, &HTTPOutputConfig{Debug: true})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
// Start Gor
go Start(quit)
// Wait for middleware to initialize
// Give go compiller time to build programm
time.Sleep(500 * time.Millisecond)
// Should receive 2 requests from original + 2 from replayed
wg.Add(4)
client := NewHTTPClient(from.URL, &HTTPClientConfig{Debug: true})
// Sending traffic to original service
resp, _ = client.Get("/token")
token = proto.Body(resp)
// When delay is too smal, middleware does not always rewrite requests in time
// Hopefuly client will have delay more then 100ms :)
time.Sleep(100 * time.Millisecond)
resp, _ = client.Get("/secure?token=" + string(token))
if !bytes.Equal(proto.Status(resp), []byte("202")) {
t.Error("Valid token should return 202:", proto.Status(resp))
}
wg.Wait()
close(quit)
time.Sleep(100 * time.Millisecond)
Settings.middleware = ""
}
+26
View File
@@ -0,0 +1,26 @@
package main
import (
"fmt"
)
// DummyOutput used for debugging, prints all incoming requests
type DummyOutput struct {
}
// NewDummyOutput constructor for DummyOutput
func NewDummyOutput() (di *DummyOutput) {
di = new(DummyOutput)
return
}
func (i *DummyOutput) Write(data []byte) (int, error) {
fmt.Println(string(data))
return len(data), nil
}
func (i *DummyOutput) String() string {
return "Dummy Output"
}
+241
View File
@@ -0,0 +1,241 @@
package main
import (
"bufio"
"compress/gzip"
"fmt"
"io"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
)
var dateFileNameFuncs = map[string]func() string{
"%Y": func() string { return time.Now().Format("2006") },
"%m": func() string { return time.Now().Format("01") },
"%d": func() string { return time.Now().Format("02") },
"%H": func() string { return time.Now().Format("15") },
"%M": func() string { return time.Now().Format("04") },
"%S": func() string { return time.Now().Format("05") },
"%NS": func() string { return fmt.Sprint(time.Now().Nanosecond()) },
}
type FileOutputConfig struct {
flushInterval time.Duration
sizeLimit unitSizeVar
queueLimit int
append bool
}
// FileOutput output plugin
type FileOutput struct {
mu sync.Mutex
pathTemplate string
currentName string
file *os.File
queueLength int
chunkSize int
writer io.Writer
config *FileOutputConfig
}
// NewFileOutput constructor for FileOutput, accepts path
func NewFileOutput(pathTemplate string, config *FileOutputConfig) *FileOutput {
o := new(FileOutput)
o.pathTemplate = pathTemplate
o.config = config
o.updateName()
// Force flushing every minute
go func() {
for {
time.Sleep(o.config.flushInterval)
o.flush()
}
}()
go func() {
for {
time.Sleep(time.Second)
o.updateName()
}
}()
return o
}
func getFileIndex(name string) int {
ext := filepath.Ext(name)
withoutExt := strings.TrimSuffix(name, ext)
if idx := strings.LastIndex(withoutExt, "_"); idx != -1 {
if i, err := strconv.Atoi(withoutExt[idx+1:]); err == nil {
return i
}
}
return -1
}
func setFileIndex(name string, idx int) string {
idxS := strconv.Itoa(idx)
ext := filepath.Ext(name)
withoutExt := strings.TrimSuffix(name, ext)
if i := strings.LastIndex(withoutExt, "_"); i != -1 {
if _, err := strconv.Atoi(withoutExt[i+1:]); err == nil {
withoutExt = withoutExt[:i]
}
}
return withoutExt + "_" + idxS + ext
}
func withoutIndex(s string) string {
if i := strings.LastIndex(s, "_"); i != -1 {
return s[:i]
}
return s
}
type sortByFileIndex []string
func (s sortByFileIndex) Len() int {
return len(s)
}
func (s sortByFileIndex) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s sortByFileIndex) Less(i, j int) bool {
if withoutIndex(s[i]) == withoutIndex(s[j]) {
return getFileIndex(s[i]) < getFileIndex(s[j])
}
return s[i] < s[j]
}
func (o *FileOutput) filename() string {
defer o.mu.Unlock()
o.mu.Lock()
path := o.pathTemplate
for name, fn := range dateFileNameFuncs {
path = strings.Replace(path, name, fn(), -1)
}
if !o.config.append {
nextChunk := false
if o.currentName == "" ||
((o.config.queueLimit > 0 && o.queueLength >= o.config.queueLimit) ||
(o.config.sizeLimit > 0 && o.chunkSize >= int(o.config.sizeLimit))) {
nextChunk = true
}
ext := filepath.Ext(path)
withoutExt := strings.TrimSuffix(path, ext)
if matches, err := filepath.Glob(withoutExt + "*" + ext); err == nil {
if len(matches) == 0 {
return setFileIndex(path, 0)
}
sort.Sort(sortByFileIndex(matches))
last := matches[len(matches)-1]
fileIndex := 0
if idx := getFileIndex(last); idx != -1 {
fileIndex = idx
if nextChunk {
fileIndex++
}
}
return setFileIndex(last, fileIndex)
}
}
return path
}
func (o *FileOutput) updateName() {
o.currentName = filepath.Clean(o.filename())
}
func (o *FileOutput) Write(data []byte) (n int, err error) {
if !isOriginPayload(data) {
return len(data), nil
}
if o.file == nil || o.currentName != o.file.Name() {
o.mu.Lock()
o.Close()
o.file, err = os.OpenFile(o.currentName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
o.file.Sync()
if strings.HasSuffix(o.currentName, ".gz") {
o.writer = gzip.NewWriter(o.file)
} else {
o.writer = bufio.NewWriter(o.file)
}
if err != nil {
log.Fatal(o, "Cannot open file %q. Error: %s", o.currentName, err)
}
o.queueLength = 0
o.mu.Unlock()
}
o.writer.Write(data)
o.writer.Write([]byte(payloadSeparator))
o.queueLength++
return len(data), nil
}
func (o *FileOutput) flush() {
defer o.mu.Unlock()
o.mu.Lock()
if o.file != nil {
if strings.HasSuffix(o.currentName, ".gz") {
o.writer.(*gzip.Writer).Flush()
} else {
o.writer.(*bufio.Writer).Flush()
}
if stat, err := o.file.Stat(); err != nil {
o.chunkSize = int(stat.Size())
}
}
}
func (o *FileOutput) String() string {
return "File output: " + o.file.Name()
}
func (o *FileOutput) Close() error {
if o.file != nil {
if strings.HasSuffix(o.currentName, ".gz") {
o.writer.(*gzip.Writer).Close()
} else {
o.writer.(*bufio.Writer).Flush()
}
o.file.Close()
}
return nil
}
+39
View File
@@ -0,0 +1,39 @@
package main
import (
"strconv"
"strings"
)
var dataUnitMap = map[byte]int64{
'k': 1024,
'm': 1024 * 1024,
'g': 1024 * 1024 * 1024,
}
func parseDataUnit(s string) int64 {
// Allow kb, mb, gb
if strings.HasSuffix(s, "b") {
s = s[:len(s)-1]
}
if unit, ok := dataUnitMap[s[len(s)-1]]; ok {
size, _ := strconv.ParseInt(s[:len(s)-1], 10, 64)
return unit * size
} else {
// If no unit specified use bytes
size, _ := strconv.ParseInt(s, 10, 64)
return size
}
}
type unitSizeVar int64
func (u unitSizeVar) String() string {
return strconv.Itoa(int(u))
}
func (u unitSizeVar) Set(s string) error {
u = unitSizeVar(parseDataUnit(s))
return nil
}
+285
View File
@@ -0,0 +1,285 @@
package main
import (
"fmt"
"io"
"math/rand"
"os"
"reflect"
"sort"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestFileOutput(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
output := NewFileOutput("/tmp/test_requests.gor", &FileOutputConfig{flushInterval: time.Minute, append: true})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
for i := 0; i < 100; i++ {
wg.Add(2)
input.EmitGET()
input.EmitPOST()
}
time.Sleep(100 * time.Millisecond)
output.flush()
close(quit)
quit = make(chan int)
var counter int64
input2 := NewFileInput("/tmp/test_requests.gor", false)
output2 := NewTestOutput(func(data []byte) {
atomic.AddInt64(&counter, 1)
wg.Done()
})
Plugins.Inputs = []io.Reader{input2}
Plugins.Outputs = []io.Writer{output2}
go Start(quit)
wg.Wait()
close(quit)
}
func TestFileOutputWithNameCleaning(t *testing.T) {
output := &FileOutput{pathTemplate: "./test_requests.gor", config: &FileOutputConfig{flushInterval: time.Minute, append: false}}
expectedFileName := "test_requests_0.gor"
output.updateName()
if expectedFileName != output.currentName {
t.Errorf("Expected path %s but got %s", expectedFileName, output.currentName)
}
}
func TestFileOutputPathTemplate(t *testing.T) {
output := &FileOutput{pathTemplate: "/tmp/log-%Y-%m-%d-%S", config: &FileOutputConfig{flushInterval: time.Minute, append: true}}
now := time.Now()
expectedPath := fmt.Sprintf("/tmp/log-%s-%s-%s-%s", now.Format("2006"), now.Format("01"), now.Format("02"), now.Format("05"))
path := output.filename()
if expectedPath != path {
t.Errorf("Expected path %s but got %s", expectedPath, path)
}
}
func TestFileOutputMultipleFiles(t *testing.T) {
output := NewFileOutput("/tmp/log-%Y-%m-%d-%S", &FileOutputConfig{append: true, flushInterval: time.Minute})
if output.file != nil {
t.Error("Should not initialize file if no writes")
}
output.Write([]byte("1 1 1\r\ntest"))
name1 := output.file.Name()
output.Write([]byte("1 1 1\r\ntest"))
name2 := output.file.Name()
time.Sleep(time.Second)
output.updateName()
output.Write([]byte("1 1 1\r\ntest"))
name3 := output.file.Name()
if name2 != name1 {
t.Error("Fast changes should happen in same file:", name1, name2, name3)
}
if name3 == name1 {
t.Error("File name should change:", name1, name2, name3)
}
os.Remove(name1)
os.Remove(name3)
}
func TestFileOutputCompression(t *testing.T) {
output := NewFileOutput("/tmp/log-%Y-%m-%d-%S.gz", &FileOutputConfig{append: true, flushInterval: time.Minute})
if output.file != nil {
t.Error("Should not initialize file if no writes")
}
for i := 0; i < 1000; i++ {
output.Write([]byte("1 1 1\r\ntest"))
}
name := output.file.Name()
output.Close()
s, _ := os.Stat(name)
if s.Size() == 12*1000 {
t.Error("Should be compressed file:", s.Size())
}
os.Remove(name)
}
func TestParseDataUnit(t *testing.T) {
var tests = []struct {
value string
size int64
}{
{"100kb", dataUnitMap['k'] * 100},
{"100k", dataUnitMap['k'] * 100},
{"1kb", dataUnitMap['k']},
{"1g", dataUnitMap['g']},
{"10m", dataUnitMap['m'] * 10},
{"zsaa312", 0},
}
for _, c := range tests {
if parseDataUnit(c.value) != c.size {
t.Error(c.value, "should be", c.size, "instead", parseDataUnit(c.value))
}
}
}
func TestGetFileIndex(t *testing.T) {
var tests = []struct {
path string
index int
}{
{"/tmp/logs", -1},
{"/tmp/logs_1", 1},
{"/tmp/logs_2.gz", 2},
{"/tmp/logs_0.gz", 0},
}
for _, c := range tests {
if getFileIndex(c.path) != c.index {
t.Error(c.path, "should be", c.index, "instead", getFileIndex(c.path))
}
}
}
func TestSetFileIndex(t *testing.T) {
var tests = []struct {
path string
index int
newPath string
}{
{"/tmp/logs", 0, "/tmp/logs_0"},
{"/tmp/logs.gz", 1, "/tmp/logs_1.gz"},
{"/tmp/logs_1", 0, "/tmp/logs_0"},
{"/tmp/logs_0", 10, "/tmp/logs_10"},
{"/tmp/logs_0.gz", 10, "/tmp/logs_10.gz"},
{"/tmp/logs_underscores.gz", 10, "/tmp/logs_underscores_10.gz"},
}
for _, c := range tests {
if setFileIndex(c.path, c.index) != c.newPath {
t.Error(c.path, "should be", c.newPath, "instead", setFileIndex(c.path, c.index))
}
}
}
func TestFileOutputAppendQueueLimitOverflow(t *testing.T) {
rnd := rand.Int63()
name := fmt.Sprintf("/tmp/%d", rnd)
output := NewFileOutput(name, &FileOutputConfig{append: false, flushInterval: time.Minute, queueLimit: 2})
output.Write([]byte("1 1 1\r\ntest"))
name1 := output.file.Name()
output.Write([]byte("1 1 1\r\ntest"))
name2 := output.file.Name()
output.updateName()
output.Write([]byte("1 1 1\r\ntest"))
name3 := output.file.Name()
if name2 != name1 || name1 != fmt.Sprintf("/tmp/%d_0", rnd) {
t.Error("Fast changes should happen in same file:", name1, name2, name3)
}
if name3 == name1 || name3 != fmt.Sprintf("/tmp/%d_1", rnd) {
t.Error("File name should change:", name1, name2, name3)
}
os.Remove(name1)
os.Remove(name3)
}
func TestFileOutputAppendQueueLimitNoOverflow(t *testing.T) {
rnd := rand.Int63()
name := fmt.Sprintf("/tmp/%d", rnd)
output := NewFileOutput(name, &FileOutputConfig{append: false, flushInterval: time.Minute, queueLimit: 3})
output.Write([]byte("1 1 1\r\ntest"))
name1 := output.file.Name()
output.Write([]byte("1 1 1\r\ntest"))
name2 := output.file.Name()
output.updateName()
output.Write([]byte("1 1 1\r\ntest"))
name3 := output.file.Name()
if name2 != name1 || name1 != fmt.Sprintf("/tmp/%d_0", rnd) {
t.Error("Fast changes should happen in same file:", name1, name2, name3)
}
if name3 != name1 || name3 != fmt.Sprintf("/tmp/%d_0", rnd) {
t.Error("File name should not change:", name1, name2, name3)
}
os.Remove(name1)
os.Remove(name3)
}
func TestFileOutputAppendQueueLimitGzips(t *testing.T) {
rnd := rand.Int63()
name := fmt.Sprintf("/tmp/%d.gz", rnd)
output := NewFileOutput(name, &FileOutputConfig{append: false, flushInterval: time.Minute, queueLimit: 2})
output.Write([]byte("1 1 1\r\ntest"))
name1 := output.file.Name()
output.Write([]byte("1 1 1\r\ntest"))
name2 := output.file.Name()
output.updateName()
output.Write([]byte("1 1 1\r\ntest"))
name3 := output.file.Name()
if name2 != name1 || name1 != fmt.Sprintf("/tmp/%d_0.gz", rnd) {
t.Error("Fast changes should happen in same file:", name1, name2, name3)
}
if name3 == name1 || name3 != fmt.Sprintf("/tmp/%d_1.gz", rnd) {
t.Error("File name should change:", name1, name2, name3)
}
os.Remove(name1)
os.Remove(name3)
}
func TestFileOutputSort(t *testing.T) {
var files = []string{"2016_0", "2014_10", "2015_0", "2015_10", "2015_2"}
var expected = []string{"2014_10", "2015_0", "2015_2", "2015_10", "2016_0"}
sort.Sort(sortByFileIndex(files))
if !reflect.DeepEqual(files, expected) {
t.Error("Should properly sort file names using indexes", files, expected)
}
}
+219
View File
@@ -0,0 +1,219 @@
package main
import (
"io"
"sync/atomic"
"time"
"github.com/buger/gor/proto"
)
const initialDynamicWorkers = 10
type response struct {
payload []byte
uuid []byte
roundTripTime int64
}
// HTTPOutputConfig struct for holding http output configuration
type HTTPOutputConfig struct {
redirectLimit int
stats bool
workers int
elasticSearch string
Timeout time.Duration
OriginalHost bool
BufferSize int
Debug bool
TrackResponses bool
}
// HTTPOutput plugin manage pool of workers which send request to replayed server
// By default workers pool is dynamic and starts with 10 workers
// You can specify fixed number of workers using `--output-http-workers`
type HTTPOutput struct {
// Keep this as first element of struct because it guarantees 64bit
// alignment. atomic.* functions crash on 32bit machines if operand is not
// aligned at 64bit. See https://github.com/golang/go/issues/599
activeWorkers int64
address string
limit int
queue chan []byte
responses chan response
needWorker chan int
config *HTTPOutputConfig
queueStats *GorStat
elasticSearch *ESPlugin
}
// NewHTTPOutput constructor for HTTPOutput
// Initialize workers
func NewHTTPOutput(address string, config *HTTPOutputConfig) io.Writer {
o := new(HTTPOutput)
o.address = address
o.config = config
if o.config.stats {
o.queueStats = NewGorStat("output_http")
}
o.queue = make(chan []byte, 1000)
o.responses = make(chan response, 1000)
o.needWorker = make(chan int, 1)
// Initial workers count
if o.config.workers == 0 {
o.needWorker <- initialDynamicWorkers
} else {
o.needWorker <- o.config.workers
}
if o.config.elasticSearch != "" {
o.elasticSearch = new(ESPlugin)
o.elasticSearch.Init(o.config.elasticSearch)
}
if len(Settings.middleware) > 0 {
o.config.TrackResponses = true
}
go o.workerMaster()
return o
}
func (o *HTTPOutput) workerMaster() {
for {
newWorkers := <-o.needWorker
for i := 0; i < newWorkers; i++ {
go o.startWorker()
}
// Disable dynamic scaling if workers poll fixed size
if o.config.workers != 0 {
return
}
}
}
func (o *HTTPOutput) startWorker() {
client := NewHTTPClient(o.address, &HTTPClientConfig{
FollowRedirects: o.config.redirectLimit,
Debug: o.config.Debug,
OriginalHost: o.config.OriginalHost,
Timeout: o.config.Timeout,
ResponseBufferSize: o.config.BufferSize,
})
deathCount := 0
atomic.AddInt64(&o.activeWorkers, 1)
for {
select {
case data := <-o.queue:
o.sendRequest(client, data)
deathCount = 0
case <-time.After(time.Millisecond * 100):
// When dynamic scaling enabled workers die after 2s of inactivity
if o.config.workers == 0 {
deathCount++
} else {
continue
}
if deathCount > 20 {
workersCount := atomic.LoadInt64(&o.activeWorkers)
// At least 1 startWorker should be alive
if workersCount != 1 {
atomic.AddInt64(&o.activeWorkers, -1)
return
}
}
}
}
}
func (o *HTTPOutput) Write(data []byte) (n int, err error) {
if !isRequestPayload(data) {
return len(data), nil
}
buf := make([]byte, len(data))
copy(buf, data)
o.queue <- buf
if o.config.stats {
o.queueStats.Write(len(o.queue))
}
if o.config.workers == 0 {
workersCount := atomic.LoadInt64(&o.activeWorkers)
if len(o.queue) > int(workersCount) {
o.needWorker <- len(o.queue)
}
}
return len(data), nil
}
func (o *HTTPOutput) Read(data []byte) (int, error) {
resp := <-o.responses
Debug("[OUTPUT-HTTP] Received response:", string(resp.payload))
header := payloadHeader(ReplayedResponsePayload, resp.uuid, resp.roundTripTime)
copy(data[0:len(header)], header)
copy(data[len(header):], resp.payload)
return len(resp.payload) + len(header), nil
}
func (o *HTTPOutput) sendRequest(client *HTTPClient, request []byte) {
meta := payloadMeta(request)
if len(meta) < 2 {
return
}
uuid := meta[1]
body := payloadBody(request)
if !proto.IsHTTPPayload(body) {
return
}
start := time.Now()
resp, err := client.Send(body)
stop := time.Now()
if err != nil {
Debug("Request error:", err)
}
if o.config.TrackResponses {
o.responses <- response{resp, uuid, stop.UnixNano() - start.UnixNano()}
}
if o.elasticSearch != nil {
o.elasticSearch.ResponseAnalyze(request, resp, start, stop)
}
}
func (o *HTTPOutput) String() string {
return "HTTP output: " + o.address
}
+154
View File
@@ -0,0 +1,154 @@
package main
import (
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
_ "net/http/httputil"
"sync"
"testing"
"time"
)
func TestHTTPOutput(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.Header.Get("User-Agent") != "Gor" {
t.Error("Wrong header")
}
if req.Method == "OPTIONS" {
t.Error("Wrong method")
}
if req.Method == "POST" {
defer req.Body.Close()
body, _ := ioutil.ReadAll(req.Body)
if string(body) != "a=1&b=2" {
t.Error("Wrong POST body:", string(body))
}
}
wg.Done()
}))
defer server.Close()
headers := HTTPHeaders{HTTPHeader{"User-Agent", "Gor"}}
methods := HTTPMethods{[]byte("GET"), []byte("PUT"), []byte("POST")}
Settings.modifierConfig = HTTPModifierConfig{headers: headers, methods: methods}
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{Debug: true})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
for i := 0; i < 100; i++ {
wg.Add(2) // OPTIONS should be ignored
input.EmitPOST()
input.EmitOPTIONS()
input.EmitGET()
}
wg.Wait()
close(quit)
Settings.modifierConfig = HTTPModifierConfig{}
}
func TestHTTPOutputKeepOriginalHost(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
input := NewTestInput()
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.Host != "custom-host.com" {
t.Error("Wrong header", req.Host)
}
wg.Done()
}))
defer server.Close()
headers := HTTPHeaders{HTTPHeader{"Host", "custom-host.com"}}
Settings.modifierConfig = HTTPModifierConfig{headers: headers}
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{Debug: false, OriginalHost: true})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
wg.Add(1)
input.EmitGET()
wg.Wait()
close(quit)
Settings.modifierConfig = HTTPModifierConfig{}
}
func TestOutputHTTPSSL(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
// Origing and Replay server initialization
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wg.Done()
}))
input := NewTestInput()
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
wg.Add(2)
input.EmitPOST()
input.EmitGET()
wg.Wait()
close(quit)
}
func BenchmarkHTTPOutput(b *testing.B) {
wg := new(sync.WaitGroup)
quit := make(chan int)
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(50 * time.Millisecond)
wg.Done()
}))
defer server.Close()
input := NewTestInput()
output := NewHTTPOutput(server.URL, &HTTPOutputConfig{})
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
for i := 0; i < b.N; i++ {
wg.Add(1)
input.EmitPOST()
}
wg.Wait()
close(quit)
}
+99
View File
@@ -0,0 +1,99 @@
package main
import (
"fmt"
"io"
"log"
"net"
"time"
)
// TCPOutput used for sending raw tcp payloads
// Currently used for internal communication between listener and replay server
// Can be used for transfering binary payloads like protocol buffers
type TCPOutput struct {
address string
limit int
buf chan []byte
bufStats *GorStat
}
// NewTCPOutput constructor for TCPOutput
// Initialize 10 workers which hold keep-alive connection
func NewTCPOutput(address string) io.Writer {
o := new(TCPOutput)
o.address = address
o.buf = make(chan []byte, 100)
if Settings.outputTCPStats {
o.bufStats = NewGorStat("output_tcp")
}
for i := 0; i < 10; i++ {
go o.worker()
}
return o
}
func (o *TCPOutput) worker() {
retries := 1
conn, err := o.connect(o.address)
for {
if err == nil {
break
}
log.Println("Can't connect to aggregator instance, reconnecting in 1 second. Retries:", retries)
time.Sleep(1 * time.Second)
conn, err = o.connect(o.address)
retries++
}
if retries > 0 {
log.Println("Connected to aggregator instance after ", retries, " retries")
}
defer conn.Close()
for {
conn.Write(<-o.buf)
_, err := conn.Write([]byte(payloadSeparator))
if err != nil {
log.Println("Lost connection with aggregator instance, reconnecting")
go o.worker()
break
}
}
}
func (o *TCPOutput) Write(data []byte) (n int, err error) {
if !isOriginPayload(data) {
return len(data), nil
}
// We have to copy, because sending data in multiple threads
newBuf := make([]byte, len(data))
copy(newBuf, data)
o.buf <- newBuf
if Settings.outputTCPStats {
o.bufStats.Write(len(o.buf))
}
return len(data), nil
}
func (o *TCPOutput) connect(address string) (conn net.Conn, err error) {
conn, err = net.Dial("tcp", address)
return
}
func (o *TCPOutput) String() string {
return fmt.Sprintf("TCP output %s, limit: %d", o.address, o.limit)
}
+88
View File
@@ -0,0 +1,88 @@
package main
import (
"bufio"
"io"
"log"
"net"
"sync"
"testing"
)
func TestTCPOutput(t *testing.T) {
wg := new(sync.WaitGroup)
quit := make(chan int)
listener := startTCP(func(data []byte) {
wg.Done()
})
input := NewTestInput()
output := NewTCPOutput(listener.Addr().String())
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
for i := 0; i < 100; i++ {
wg.Add(1)
input.EmitGET()
}
wg.Wait()
close(quit)
}
func startTCP(cb func([]byte)) net.Listener {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal("Can't start:", err)
}
go func() {
for {
conn, _ := listener.Accept()
defer conn.Close()
go func() {
reader := bufio.NewReader(conn)
scanner := bufio.NewScanner(reader)
scanner.Split(payloadScanner)
for scanner.Scan() {
cb(scanner.Bytes())
}
}()
}
}()
return listener
}
func BenchmarkTCPOutput(b *testing.B) {
wg := new(sync.WaitGroup)
quit := make(chan int)
listener := startTCP(func(data []byte) {
wg.Done()
})
input := NewTestInput()
output := NewTCPOutput(listener.Addr().String())
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
go Start(quit)
b.ResetTimer()
for i := 0; i < b.N; i++ {
wg.Add(1)
input.EmitGET()
}
wg.Wait()
close(quit)
}
+139
View File
@@ -0,0 +1,139 @@
package main
import (
"io"
"reflect"
"strings"
"sync"
"time"
)
// InOutPlugins struct for holding references to plugins
type InOutPlugins struct {
Inputs []io.Reader
Outputs []io.Writer
All []interface{}
}
var pluginMu sync.Mutex
// Plugins holds all the plugin objects
var Plugins *InOutPlugins = new(InOutPlugins)
// extractLimitOptions detects if plugin get called with limiter support
// Returns address and limit
func extractLimitOptions(options string) (string, string) {
split := strings.Split(options, "|")
if len(split) > 1 {
return split[0], split[1]
}
return split[0], ""
}
// Automatically detects type of plugin and initialize it
//
// See this article if curious about relfect stuff below: http://blog.burntsushi.net/type-parametric-functions-golang
func registerPlugin(constructor interface{}, options ...interface{}) {
var path, limit string
vc := reflect.ValueOf(constructor)
// Pre-processing options to make it work with reflect
vo := []reflect.Value{}
for _, oi := range options {
vo = append(vo, reflect.ValueOf(oi))
}
if len(vo) > 0 {
// Removing limit options from path
path, limit = extractLimitOptions(vo[0].String())
// Writing value back without limiter "|" options
vo[0] = reflect.ValueOf(path)
}
// Calling our constructor with list of given options
plugin := vc.Call(vo)[0].Interface()
pluginWrapper := plugin
if limit != "" {
pluginWrapper = NewLimiter(plugin, limit)
} else {
pluginWrapper = plugin
}
_, isR := plugin.(io.Reader)
_, isW := plugin.(io.Writer)
// Some of the output can be Readers as well because return responses
if isR && !isW {
Plugins.Inputs = append(Plugins.Inputs, pluginWrapper.(io.Reader))
}
if isW {
Plugins.Outputs = append(Plugins.Outputs, pluginWrapper.(io.Writer))
}
Plugins.All = append(Plugins.All, plugin)
}
// InitPlugins specify and initialize all available plugins
func InitPlugins() {
pluginMu.Lock()
defer pluginMu.Unlock()
for _, options := range Settings.inputDummy {
registerPlugin(NewDummyInput, options)
}
for range Settings.outputDummy {
registerPlugin(NewDummyOutput)
}
if Settings.outputStdout {
registerPlugin(NewDummyOutput)
}
engine := EnginePcap
if Settings.inputRAWEngine == "raw_socket" {
engine = EngineRawSocket
}
for _, options := range Settings.inputRAW {
registerPlugin(NewRAWInput, options, engine, Settings.inputRAWTrackResponse, time.Duration(0), Settings.inputRAWRealIPHeader)
}
for _, options := range Settings.inputTCP {
registerPlugin(NewTCPInput, options)
}
for _, options := range Settings.outputTCP {
registerPlugin(NewTCPOutput, options)
}
for _, options := range Settings.inputFile {
registerPlugin(NewFileInput, options, Settings.inputFileLoop)
}
for _, options := range Settings.outputFile {
registerPlugin(NewFileOutput, options, &Settings.outputFileConfig)
}
for _, options := range Settings.inputHTTP {
registerPlugin(NewHTTPInput, options)
}
// If we explicitly set Host header http output should not rewrite it
// Fix: https://github.com/buger/gor/issues/174
for _, header := range Settings.modifierConfig.headers {
if header.Name == "Host" {
Settings.outputHTTPConfig.OriginalHost = true
break
}
}
for _, options := range Settings.outputHTTP {
registerPlugin(NewHTTPOutput, options, &Settings.outputHTTPConfig)
}
}
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"io"
"testing"
)
func TestPluginsRegistration(t *testing.T) {
Plugins.Inputs = []io.Reader{}
Plugins.Outputs = []io.Writer{}
Settings.inputDummy = MultiOption{"[]"}
Settings.outputDummy = MultiOption{"[]"}
Settings.outputHTTP = MultiOption{"www.example.com|10"}
Settings.inputFile = MultiOption{"/dev/null"}
InitPlugins()
if len(Plugins.Inputs) != 2 {
t.Errorf("Should be 2 inputs %d", len(Plugins.Inputs))
}
if _, ok := Plugins.Inputs[0].(*DummyInput); !ok {
t.Errorf("First input should be DummyInput")
}
if _, ok := Plugins.Inputs[1].(*FileInput); !ok {
t.Errorf("Second input should be FileInput")
}
if len(Plugins.Outputs) != 2 {
t.Errorf("Should be 2 output %d", len(Plugins.Outputs))
}
if _, ok := Plugins.Outputs[0].(*DummyOutput); !ok {
t.Errorf("First output should be DummyOutput")
}
if l, ok := Plugins.Outputs[1].(*Limiter); ok {
if _, ok := l.plugin.(*HTTPOutput); !ok {
t.Errorf("HTTPOutput should be wrapped in limiter")
}
} else {
t.Errorf("Second output should be Limiter")
}
}
+341
View File
@@ -0,0 +1,341 @@
/*
Package proto provides byte-level interaction with HTTP request payload.
Example of HTTP payload for future references, new line symbols escaped:
POST /upload HTTP/1.1\r\n
User-Agent: Gor\r\n
Content-Length: 11\r\n
\r\n
Hello world
GET /index.html HTTP/1.1\r\n
User-Agent: Gor\r\n
\r\n
\r\n
*/
package proto
import (
"bytes"
"github.com/buger/gor/byteutils"
)
// In HTTP newline defined by 2 bytes (for both windows and *nix support)
var CLRF = []byte("\r\n")
// New line acts as separator: end of Headers or Body (in some cases)
var EmptyLine = []byte("\r\n\r\n")
// Separator for Header line. Header looks like: `HeaderName: value`
var HeaderDelim = []byte(": ")
// MIMEHeadersEndPos finds end of the Headers section, which should end with empty line.
func MIMEHeadersEndPos(payload []byte) int {
return bytes.Index(payload, EmptyLine)
}
// MIMEHeadersStartPos finds start of Headers section
// It just finds position of second line (first contains location and method).
func MIMEHeadersStartPos(payload []byte) int {
return bytes.Index(payload, CLRF) + 2 // Find first line end
}
func isLower(b byte) bool {
if 'a' <= b && b <= 'z' {
return true
}
return false
}
func toUpper(b byte) byte {
if 'a' <= b && b <= 'z' {
b -= 'a' - 'A'
}
return b
}
func toLower(b byte) byte {
if 'A' <= b && b <= 'Z' {
b += 'a' - 'A'
}
return b
}
func headerIndex(payload []byte, name []byte) int {
isLower := isLower(name[0])
i := 0
for {
if i >= len(payload) {
return -1
}
if payload[i] == '\n' {
i++
// We are at the end
if i == len(payload) {
return -1
}
if payload[i] == name[0] ||
(!isLower && payload[i] == toLower(name[0])) ||
(isLower && payload[i] == toUpper(name[0])) {
i++
j := 1
for {
if j == len(name) {
// Matched, and return start of the header
return i - len(name)
}
if payload[i] != name[j] {
break
}
// If compound header name do one more case check: Content-Length or Transfer-Encoding
if name[j] == '-' {
i++
j++
if !(payload[i] == name[j] ||
(!isLower && payload[i] == toLower(name[j])) ||
(isLower && payload[i] == toUpper(name[j]))) {
break
}
}
j++
i++
}
}
}
i++
}
return -1
}
// header return value and positions of header/value start/end.
// If not found, value will be blank, and headerStart will be -1
// Do not support multi-line headers.
func header(payload []byte, name []byte) (value []byte, headerStart, headerEnd, valueStart, valueEnd int) {
headerStart = headerIndex(payload, name)
if headerStart == -1 {
return
}
valueStart = headerStart + len(name) + 1 // Skip ":" after header name
headerEnd = valueStart + bytes.IndexByte(payload[valueStart:], '\n')
for valueStart < headerEnd { // Ignore empty space after ':'
if payload[valueStart] == ' ' {
valueStart++
} else {
break
}
}
valueEnd = valueStart + bytes.IndexByte(payload[valueStart:], '\n')
if payload[headerEnd-1] == '\r' {
valueEnd--
}
// ignore empty space at end of header value
for valueStart < valueEnd {
if payload[valueEnd-1] == ' ' {
valueEnd--
} else {
break
}
}
value = payload[valueStart:valueEnd]
return
}
// Header returns header value, if header not found, value will be blank
func Header(payload, name []byte) []byte {
val, _, _, _, _ := header(payload, name)
return val
}
// SetHeader sets header value. If header not found it creates new one.
// Returns modified request payload
func SetHeader(payload, name, value []byte) []byte {
_, hs, _, vs, ve := header(payload, name)
if hs != -1 {
// If header found we just replace its value
return byteutils.Replace(payload, vs, ve, value)
}
return AddHeader(payload, name, value)
}
// AddHeader takes http payload and appends new header to the start of headers section
// Returns modified request payload
func AddHeader(payload, name, value []byte) []byte {
header := make([]byte, len(name)+2+len(value)+2)
copy(header[0:], name)
copy(header[len(name):], HeaderDelim)
copy(header[len(name)+2:], value)
copy(header[len(header)-2:], CLRF)
mimeStart := MIMEHeadersStartPos(payload)
return byteutils.Insert(payload, mimeStart, header)
}
// DelHeader takes http payload and removes header name from headers section
// Returns modified request payload
func DeleteHeader(payload, name[]byte) []byte {
_, hs, he, _, _ := header(payload, name)
if hs != -1 {
newHeader := make([]byte, len(payload) - (he - hs) - 1)
copy(newHeader[:hs], payload[:hs])
copy(newHeader[hs:], payload[he + 1:])
return newHeader
}
return payload
}
// Body returns request/response body
func Body(payload []byte) []byte {
// 4 -> len(EMPTY_LINE)
return payload[MIMEHeadersEndPos(payload)+4:]
}
// Path takes payload and retuns request path: Split(firstLine, ' ')[1]
func Path(payload []byte) []byte {
start := bytes.IndexByte(payload, ' ') + 1
end := bytes.IndexByte(payload[start:], ' ')
return payload[start : start+end]
}
// SetPath takes payload, sets new path and returns modified payload
func SetPath(payload, path []byte) []byte {
start := bytes.IndexByte(payload, ' ') + 1
end := bytes.IndexByte(payload[start:], ' ')
return byteutils.Replace(payload, start, start+end, path)
}
// PathParam returns URL query attribute by given name, if no found: valueStart will be -1
func PathParam(payload, name []byte) (value []byte, valueStart, valueEnd int) {
path := Path(payload)
if paramStart := bytes.Index(path, append(name, '=')); paramStart != -1 {
valueStart := paramStart + len(name) + 1
paramEnd := bytes.IndexByte(path[valueStart:], '&')
// Param can end with '&' (another param), or end of line
if paramEnd == -1 { // It is final param
paramEnd = len(path)
} else {
paramEnd += valueStart
}
return path[valueStart:paramEnd], valueStart, paramEnd
}
return []byte(""), -1, -1
}
// SetPathParam takes payload and updates path Query attribute
// If query param not found, it will append new
// Returns modified payload
func SetPathParam(payload, name, value []byte) []byte {
path := Path(payload)
_, vs, ve := PathParam(payload, name)
if vs != -1 { // If param found, replace its value and set new Path
newPath := make([]byte, len(path))
copy(newPath, path)
newPath = byteutils.Replace(newPath, vs, ve, value)
return SetPath(payload, newPath)
}
// if param not found append to end of url
// Adding 2 because of '?' or '&' at start, and '=' in middle
newParam := make([]byte, len(name)+len(value)+2)
if bytes.IndexByte(path, '?') == -1 {
newParam[0] = '?'
} else {
newParam[0] = '&'
}
// Copy "param=value" into buffer, after it looks like "?param=value"
copy(newParam[1:], name)
newParam[1+len(name)] = '='
copy(newParam[2+len(name):], value)
// Append param to the end of path
newPath := make([]byte, len(path)+len(newParam))
copy(newPath, path)
copy(newPath[len(path):], newParam)
return SetPath(payload, newPath)
}
// SetHost updates Host header for HTTP/1.1 or updates host in path for HTTP/1.0 or Proxy requests
// Returns modified payload
func SetHost(payload, url, host []byte) []byte {
// If this is HTTP 1.0 traffic or proxy traffic it may include host right into path variable, so instead of setting Host header we rewrite Path
// Fix for https://github.com/buger/gor/issues/156
if path := Path(payload); bytes.HasPrefix(path, []byte("http")) {
hostStart := bytes.IndexByte(path, ':') // : position "https?:"
hostStart += 3 // Skip 1 ':' and 2 '\'
hostEnd := hostStart + bytes.IndexByte(path[hostStart:], '/')
newPath := make([]byte, len(path))
copy(newPath, path)
newPath = byteutils.Replace(newPath, 0, hostEnd, url)
return SetPath(payload, newPath)
}
return SetHeader(payload, []byte("Host"), host)
}
// Method returns HTTP method
func Method(payload []byte) []byte {
end := bytes.IndexByte(payload, ' ')
return payload[:end]
}
// Status returns response status.
// It happend to be in same position as request payload path
func Status(payload []byte) []byte {
return Path(payload)
}
var httpMethods []string = []string{
"GET ", "OPTI", "HEAD", "POST", "PUT ", "DELE", "TRAC", "CONN", /* custom methods */"BAN", "PURG",
}
func IsHTTPPayload(payload []byte) bool {
if len(payload) < 4 {
return false
}
method := string(payload[0:4])
for _, m := range httpMethods {
if method == m {
return true
}
}
return false
}
+202
View File
@@ -0,0 +1,202 @@
package proto
import (
"bytes"
"testing"
)
func TestHeader(t *testing.T) {
var payload, val []byte
var headerStart int
// Value with space at start
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if val = Header(payload, []byte("Content-Length")); !bytes.Equal(val, []byte("7")) {
t.Error("Should find header value")
}
// Value with space at end
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7 \r\nHost: www.w3.org\r\n\r\na=1&b=2")
if val = Header(payload, []byte("Content-Length")); !bytes.Equal(val, []byte("7")) {
t.Error("Should find header value without space after 7")
}
// Value without space at start
payload = []byte("POST /post HTTP/1.1\r\nContent-Length:7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if val = Header(payload, []byte("Content-Length")); !bytes.Equal(val, []byte("7")) {
t.Error("Should find header value without space after :")
}
// Value is empty
payload = []byte("GET /p HTTP/1.1\r\nCookie:\r\nHost: www.w3.org\r\n\r\n")
if val = Header(payload, []byte("Cookie")); len(val) > 0 {
t.Error("Should return empty value")
}
// Wrong delimeter
payload = []byte("GET /p HTTP/1.1\r\nCookie: 123\nHost: www.w3.org\r\n\r\n")
if val = Header(payload, []byte("Cookie")); !bytes.Equal(val, []byte("123")) {
t.Error("Should handle wrong header delimeter")
}
// Header not found
if _, headerStart, _, _, _ = header(payload, []byte("Not-Found")); headerStart != -1 {
t.Error("Should not found header")
}
// Lower case headers
payload = []byte("POST /post HTTP/1.1\r\ncontent-length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if val = Header(payload, []byte("Content-Length")); !bytes.Equal(val, []byte("7")) {
t.Error("Should find lower case 2 word header")
}
payload = []byte("POST /post HTTP/1.1\r\ncontent-length: 7\r\nhost: www.w3.org\r\n\r\na=1&b=2")
if val = Header(payload, []byte("host")); !bytes.Equal(val, []byte("www.w3.org")) {
t.Error("Should find lower case 1 word header")
}
}
func TestMIMEHeadersEndPos(t *testing.T) {
head := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org")
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
end := MIMEHeadersEndPos(payload)
if !bytes.Equal(payload[:end], head) {
t.Error("Wrong headers end position:", end)
}
}
func TestMIMEHeadersStartPos(t *testing.T) {
headers := []byte("Content-Length: 7\r\nHost: www.w3.org")
payload := []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
start := MIMEHeadersStartPos(payload)
end := MIMEHeadersEndPos(payload)
if !bytes.Equal(payload[start:end], headers) {
t.Error("Wrong headers end position:", start, end)
}
}
func TestSetHeader(t *testing.T) {
var payload, payloadAfter []byte
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
payloadAfter = []byte("POST /post HTTP/1.1\r\nContent-Length: 14\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if payload = SetHeader(payload, []byte("Content-Length"), []byte("14")); !bytes.Equal(payload, payloadAfter) {
t.Error("Should update header if it exists", string(payload))
}
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
payloadAfter = []byte("POST /post HTTP/1.1\r\nUser-Agent: Gor\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if payload = SetHeader(payload, []byte("User-Agent"), []byte("Gor")); !bytes.Equal(payload, payloadAfter) {
t.Error("Should add header if not found", string(payload))
}
}
func TestDeleteHeader(t *testing.T) {
var payload, payloadAfter []byte
payload = []byte("POST /post HTTP/1.1\r\nUser-Agent: Gor\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
payloadAfter = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if payload = DeleteHeader(payload, []byte("User-Agent")); !bytes.Equal(payload, payloadAfter) {
t.Error("Should delete header if found", string(payload), string(payloadAfter))
}
//Whitespace at end of User-Agent
payload = []byte("POST /post HTTP/1.1\r\nUser-Agent: Gor \r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
payloadAfter = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if payload = DeleteHeader(payload, []byte("User-Agent")); !bytes.Equal(payload, payloadAfter) {
t.Error("Should delete header if found", string(payload), string(payloadAfter))
}
}
func TestPath(t *testing.T) {
var path, payload []byte
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if path = Path(payload); !bytes.Equal(path, []byte("/post")) {
t.Error("Should find path", string(path))
}
}
func TestSetPath(t *testing.T) {
var payload, payloadAfter []byte
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
payloadAfter = []byte("POST /new_path HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if payload = SetPath(payload, []byte("/new_path")); !bytes.Equal(payload, payloadAfter) {
t.Error("Should replace path", string(payload))
}
}
func TestPathParam(t *testing.T) {
var payload []byte
payload = []byte("POST /post?param=test&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if val, _, _ := PathParam(payload, []byte("param")); !bytes.Equal(val, []byte("test")) {
t.Error("Should detect attribute", string(val))
}
if val, _, _ := PathParam(payload, []byte("user_id")); !bytes.Equal(val, []byte("1")) {
t.Error("Should detect attribute", string(val))
}
}
func TestSetPathParam(t *testing.T) {
var payload, payloadAfter []byte
payload = []byte("POST /post?param=test&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
payloadAfter = []byte("POST /post?param=new&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if payload = SetPathParam(payload, []byte("param"), []byte("new")); !bytes.Equal(payload, payloadAfter) {
t.Error("Should replace existing value", string(payload))
}
payload = []byte("POST /post?param=test&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
payloadAfter = []byte("POST /post?param=test&user_id=2 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if payload = SetPathParam(payload, []byte("user_id"), []byte("2")); !bytes.Equal(payload, payloadAfter) {
t.Error("Should replace existing value", string(payload))
}
payload = []byte("POST /post HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
payloadAfter = []byte("POST /post?param=test HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if payload = SetPathParam(payload, []byte("param"), []byte("test")); !bytes.Equal(payload, payloadAfter) {
t.Error("Should set param if url have no params", string(payload))
}
payload = []byte("POST /post?param=test HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
payloadAfter = []byte("POST /post?param=test&user_id=1 HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if payload = SetPathParam(payload, []byte("user_id"), []byte("1")); !bytes.Equal(payload, payloadAfter) {
t.Error("Should set param at the end if url params", string(payload))
}
}
func TestSetHostHTTP10(t *testing.T) {
var payload, payloadAfter []byte
payload = []byte("POST http://example.com/post HTTP/1.0\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
payloadAfter = []byte("POST http://new.com/post HTTP/1.0\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
if payload = SetHost(payload, []byte("http://new.com"), []byte("new.com")); !bytes.Equal(payload, payloadAfter) {
t.Error("Should replace host", string(payload))
}
}
+87
View File
@@ -0,0 +1,87 @@
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"strconv"
)
const (
RequestPayload = '1'
ResponsePayload = '2'
ReplayedResponsePayload = '3'
)
func uuid() []byte {
b := make([]byte, 20)
rand.Read(b)
uuid := make([]byte, 40)
hex.Encode(uuid, b)
return uuid
}
var payloadSeparator = "\n🐵🙈🙉\n"
func payloadScanner(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.Index(data, []byte(payloadSeparator)); i >= 0 {
// We have a full newline-terminated line.
return i + len([]byte(payloadSeparator)), data[0:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
// Timing is request start or round-trip time, depending on payloadType
func payloadHeader(payloadType byte, uuid []byte, timing int64) (header []byte) {
sTime := strconv.FormatInt(timing, 10)
//Example:
// 3 f45590522cd1838b4a0d5c5aab80b77929dea3b3 1231\n
// `+ 1` indicates space characters or end of line
header = make([]byte, 1+1+len(uuid)+1+len(sTime)+1)
header[0] = payloadType
header[1] = ' '
header[2+len(uuid)] = ' '
header[len(header)-1] = '\n'
copy(header[2:], uuid)
copy(header[3+len(uuid):], sTime)
return header
}
func payloadBody(payload []byte) []byte {
headerSize := bytes.IndexByte(payload, '\n')
return payload[headerSize+1:]
}
func payloadMeta(payload []byte) [][]byte {
headerSize := bytes.IndexByte(payload, '\n')
if headerSize < 0 {
headerSize = 0
}
return bytes.Split(payload[:headerSize], []byte{' '})
}
func isOriginPayload(payload []byte) bool {
switch payload[0] {
case RequestPayload, ResponsePayload:
return true
default:
return false
}
}
func isRequestPayload(payload []byte) bool {
return payload[0] == RequestPayload
}
+682
View File
@@ -0,0 +1,682 @@
/*
Package rawSocket provides traffic sniffier using RAW sockets.
Capture traffic from socket using RAW_SOCKET's
http://en.wikipedia.org/wiki/Raw_socket
RAW_SOCKET allow you listen for traffic on any port (e.g. sniffing) because they operate on IP level.
Ports is TCP feature, same as flow control, reliable transmission and etc.
This package implements own TCP layer: TCP packets is parsed using tcp_packet.go, and flow control is managed by tcp_message.go
*/
package rawSocket
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"io"
"log"
"net"
"runtime"
"runtime/debug"
"strconv"
"strings"
"sync"
"time"
"github.com/buger/gor/proto"
)
var _ = fmt.Println
// Listener handle traffic capture
type Listener struct {
mu sync.Mutex
// buffer of TCPMessages waiting to be send
// ID -> TCPMessage
messages map[tcpID]*TCPMessage
// Expect: 100-continue request is send in 2 tcp messages
// We store ACK aliases to merge this packets together
ackAliases map[uint32]uint32
// To get ACK of second message we need to compute its Seq and wait for them message
seqWithData map[uint32]uint32
// Ack -> Req
respAliases map[uint32]*TCPMessage
// Ack -> ID
respWithoutReq map[uint32]tcpID
// Messages ready to be send to client
packetsChan chan []byte
// Messages ready to be send to client
messagesChan chan *TCPMessage
addr string // IP to listen
port uint16 // Port to listen
trackResponse bool
messageExpire time.Duration
conn net.PacketConn
pcapHandles []*pcap.Handle
quit chan bool
readyCh chan bool
}
type request struct {
id tcpID
start time.Time
ack uint32
}
// Available engines for intercepting traffic
const (
EngineRawSocket = 1 << iota
EnginePcap
)
// NewListener creates and initializes new Listener object
func NewListener(addr string, port string, engine int, trackResponse bool, expire time.Duration) (l *Listener) {
l = &Listener{}
l.packetsChan = make(chan []byte, 10000)
l.messagesChan = make(chan *TCPMessage, 10000)
l.quit = make(chan bool)
l.readyCh = make(chan bool, 1)
l.messages = make(map[tcpID]*TCPMessage)
l.ackAliases = make(map[uint32]uint32)
l.seqWithData = make(map[uint32]uint32)
l.respAliases = make(map[uint32]*TCPMessage)
l.respWithoutReq = make(map[uint32]tcpID)
l.trackResponse = trackResponse
l.addr = addr
_port, _ := strconv.Atoi(port)
l.port = uint16(_port)
if expire.Nanoseconds() == 0 {
expire = 2000 * time.Millisecond
}
l.messageExpire = expire
go l.listen()
// Special case for testing
if l.port != 0 {
switch engine {
case EngineRawSocket:
go l.readRAWSocket()
case EnginePcap:
go l.readPcap()
default:
log.Fatal("Unknown traffic interception engine:", engine)
}
}
return
}
func (t *Listener) listen() {
gcTicker := time.Tick(t.messageExpire / 2)
for {
select {
case <-t.quit:
if t.conn != nil {
t.conn.Close()
}
return
case data := <-t.packetsChan:
packet := ParseTCPPacket(data[:16], data[16:])
t.processTCPPacket(packet)
case <-gcTicker:
now := time.Now()
// Dispatch requests before responses
for _, message := range t.messages {
if now.Sub(message.End) >= t.messageExpire {
t.dispatchMessage(message)
}
}
}
}
}
func (t *Listener) deleteMessage(message *TCPMessage) {
delete(t.messages, message.ID())
delete(t.ackAliases, message.Ack)
if message.DataAck != 0 {
delete(t.ackAliases, message.DataAck)
}
if message.DataSeq != 0 {
delete(t.seqWithData, message.DataSeq)
}
delete(t.respAliases, message.ResponseAck)
}
func (t *Listener) dispatchMessage(message *TCPMessage) {
// If already dispatched
if _, ok := t.messages[message.ID()]; !ok {
return
}
t.deleteMessage(message)
// log.Println("Dispatching, message", message.Start.UnixNano(), message.Seq, message.Ack, string(message.Bytes()))
if message.IsIncoming {
// If there were response before request
// log.Println("Looking for Response: ", t.respWithoutReq, message.ResponseAck)
if t.trackResponse {
if respID, ok := t.respWithoutReq[message.ResponseAck]; ok {
if resp, rok := t.messages[respID]; rok {
// if resp.AssocMessage == nil {
// log.Println("FOUND RESPONSE")
resp.AssocMessage = message
message.AssocMessage = resp
if resp.IsFinished() {
defer t.dispatchMessage(resp)
}
// }
}
}
if resp, ok := t.messages[message.ResponseID]; ok {
resp.AssocMessage = message
}
}
} else {
if message.AssocMessage == nil {
if responseRequest, ok := t.respAliases[message.Ack]; ok {
message.AssocMessage = responseRequest
responseRequest.AssocMessage = message
}
}
delete(t.respAliases, message.Ack)
delete(t.respWithoutReq, message.Ack)
// Do not track responses which have no associated requests
if message.AssocMessage == nil {
// log.Println("Can't dispatch resp", message.Seq, message.Ack, string(message.Bytes()))
return
}
}
t.messagesChan <- message
}
// DeviceNotFoundError raised if user specified wrong ip
type DeviceNotFoundError struct {
addr string
}
func (e *DeviceNotFoundError) Error() string {
devices, _ := pcap.FindAllDevs()
if len(devices) == 0 {
return "Can't get list of network interfaces, ensure that you running Gor as root user or sudo.\nTo run as non-root users see this docs https://github.com/buger/gor/wiki/Running-as-non-root-user"
}
var msg string
msg += "Can't find interfaces with addr: " + e.addr + ". Provide available IP for intercepting traffic: \n"
for _, device := range devices {
msg += "Name: " + device.Name + "\n"
if device.Description != "" {
msg += "Description: " + device.Description + "\n"
}
for _, address := range device.Addresses {
msg += "- IP address: " + address.IP.String() + "\n"
}
}
return msg
}
func findPcapDevices(addr string) (interfaces []pcap.Interface, err error) {
devices, err := pcap.FindAllDevs()
if err != nil {
log.Fatal(err)
}
for _, device := range devices {
if (addr == "" || addr == "0.0.0.0" || addr == "[::]" || addr == "::") && len(device.Addresses) > 0 {
interfaces = append(interfaces, device)
continue
}
for _, address := range device.Addresses {
if device.Name == addr || address.IP.String() == addr {
interfaces = append(interfaces, device)
return interfaces, nil
}
}
}
if len(interfaces) == 0 {
return nil, &DeviceNotFoundError{addr}
} else {
return interfaces, nil
}
}
func (t *Listener) readPcap() {
devices, err := findPcapDevices(t.addr)
if err != nil {
log.Fatal(err)
}
bpfSupported := true
if runtime.GOOS == "darwin" {
bpfSupported = false
}
var wg sync.WaitGroup
wg.Add(len(devices))
for _, d := range devices {
go func(device pcap.Interface) {
handle, err := pcap.OpenLive(device.Name, 65536, true, t.messageExpire)
if err != nil {
log.Println("Pcap Error while opening device", device.Name, err)
wg.Done()
return
}
defer handle.Close()
t.mu.Lock()
t.pcapHandles = append(t.pcapHandles, handle)
var bpfDstHost, bpfSrcHost string
for i, addr := range device.Addresses {
bpfDstHost += "dst host " + addr.IP.String()
bpfSrcHost += "src host " + addr.IP.String()
if i != len(device.Addresses) - 1 {
bpfDstHost += " or "
bpfSrcHost += " or "
}
}
if bpfSupported {
var bpf string
if t.trackResponse {
bpf = "(tcp dst port " + strconv.Itoa(int(t.port)) + " and (" + bpfDstHost + ")) or (" + "tcp src port " + strconv.Itoa(int(t.port)) + " and (" + bpfSrcHost + "))"
} else {
bpf = "tcp dst port " + strconv.Itoa(int(t.port)) + " and (" + bpfDstHost + ")"
}
if err := handle.SetBPFFilter(bpf); err != nil {
log.Println("BPF filter error:", err, "Device:", device.Name, bpf)
wg.Done()
return
}
}
t.mu.Unlock()
var decoder gopacket.Decoder
// Special case for tunnel interface https://github.com/google/gopacket/issues/99
if handle.LinkType() == 12 {
decoder = layers.LayerTypeIPv4
} else {
decoder = handle.LinkType()
}
source := gopacket.NewPacketSource(handle, decoder)
source.Lazy = true
source.NoCopy = true
wg.Done()
var data, srcIP, dstIP []byte
for {
packet, err := source.NextPacket()
if err == io.EOF {
break
} else if err != nil {
continue
}
// We should remove network layer before parsing TCP/IP data
var of int
switch decoder {
case layers.LinkTypeEthernet:
of = 14
case layers.LinkTypePPP:
of = 1
case layers.LinkTypeFDDI:
of = 13
case layers.LinkTypeNull:
of = 4
case layers.LinkTypeLoop:
of = 4
case layers.LinkTypeRaw:
of = 0
case layers.LinkTypeLinuxSLL:
of = 16
default:
log.Println("Unknown packet layer", packet)
break
}
data = packet.Data()[of:]
version := uint8(data[0]) >> 4
if version == 4 {
ihl := uint8(data[0]) & 0x0F
// Truncated IP info
if len(data) < int(ihl*4) {
continue
}
srcIP = data[12:16]
dstIP = data[16:20]
data = data[ihl*4:]
} else {
// Truncated IP info
if len(data) < 40 {
continue
}
srcIP = data[8:24]
dstIP = data[24:40]
data = data[40:]
}
// Truncated TCP info
if len(data) < 13 {
continue
}
dataOffset := (data[12] & 0xF0) >> 4
// We need only packets with data inside
// Check that the buffer is larger than the size of the TCP header
if len(data) > int(dataOffset*4) {
if !bpfSupported {
destPort := binary.BigEndian.Uint16(data[2:4])
srcPort := binary.BigEndian.Uint16(data[0:2])
var addrCheck []byte
if destPort == t.port {
addrCheck = dstIP
}
if t.trackResponse && srcPort == t.port {
addrCheck = srcIP
}
if len(addrCheck) == 0 {
continue
}
addrMatched := false
for _, a := range device.Addresses {
if a.IP.Equal(net.IP(addrCheck)) {
addrMatched = true
break
}
}
if !addrMatched {
continue
}
}
newBuf := make([]byte, len(data)+16)
copy(newBuf[:16], srcIP)
copy(newBuf[16:], data)
t.packetsChan <- newBuf
}
}
}(d)
}
wg.Wait()
t.readyCh <- true
}
func (t *Listener) readRAWSocket() {
conn, e := net.ListenPacket("ip:tcp", t.addr)
t.conn = conn
if e != nil {
log.Fatal(e)
}
defer t.conn.Close()
buf := make([]byte, 64*1024) // 64kb
t.readyCh <- true
for {
// Note: ReadFrom receive messages without IP header
n, addr, err := t.conn.ReadFrom(buf)
if err != nil {
if strings.HasSuffix(err.Error(), "closed network connection") {
return
} else {
continue
}
}
if n > 0 {
if t.isValidPacket(buf[:n]) {
newBuf := make([]byte, n+16)
copy(newBuf[16:], buf[:n])
copy(newBuf[:16], []byte(addr.(*net.IPAddr).IP))
t.packetsChan <- newBuf
}
}
}
}
func (t *Listener) isValidPacket(buf []byte) bool {
// To avoid full packet parsing every time, we manually parsing values needed for packet filtering
// http://en.wikipedia.org/wiki/Transmission_Control_Protocol
destPort := binary.BigEndian.Uint16(buf[2:4])
srcPort := binary.BigEndian.Uint16(buf[0:2])
// Because RAW_SOCKET can't be bound to port, we have to control it by ourself
if destPort == t.port || (t.trackResponse && srcPort == t.port) {
// Get the 'data offset' (size of the TCP header in 32-bit words)
dataOffset := (buf[12] & 0xF0) >> 4
// We need only packets with data inside
// Check that the buffer is larger than the size of the TCP header
if len(buf) > int(dataOffset*4) {
// We should create new buffer because go slices is pointers. So buffer data shoud be immutable.
return true
}
}
return false
}
// Trying to add packet to existing message or creating new message
//
// For TCP message unique id is Acknowledgment number (see tcp_packet.go)
func (t *Listener) processTCPPacket(packet *TCPPacket) {
// Don't exit on panic
defer func() {
if r := recover(); r != nil {
log.Println("PANIC: pkg:", r, packet, string(debug.Stack()))
}
}()
// log.Println("Processing packet:", packet.Ack, packet.Seq, packet.ID)
var message *TCPMessage
isIncoming := packet.DestPort == t.port
// Seek for 100-expect chunks
if parentAck, ok := t.seqWithData[packet.Seq]; ok {
// In case if non-first data chunks comes first
for _, m := range t.messages {
if m.Ack == packet.Ack && bytes.Equal(m.packets[0].Addr, packet.Addr) {
t.deleteMessage(m)
if m.AssocMessage != nil {
m.AssocMessage.AssocMessage = nil
}
for _, pkt := range m.packets {
// log.Println("Updating ack", parentAck, pkt.Ack)
pkt.UpdateAck(parentAck)
// Re-queue this packets
t.processTCPPacket(pkt)
}
}
}
t.ackAliases[packet.Ack] = parentAck
packet.UpdateAck(parentAck)
}
if alias, ok := t.ackAliases[packet.Ack]; ok {
packet.UpdateAck(alias)
}
var responseRequest *TCPMessage
if !isIncoming {
responseRequest, _ = t.respAliases[packet.Ack]
}
message, ok := t.messages[packet.ID]
if !ok {
message = NewTCPMessage(packet.Seq, packet.Ack, isIncoming)
t.messages[packet.ID] = message
if !isIncoming {
if responseRequest != nil {
message.AssocMessage = responseRequest
responseRequest.AssocMessage = message
} else {
t.respWithoutReq[packet.Ack] = packet.ID
}
}
}
// Adding packet to message
message.AddPacket(packet)
// Handling Expect: 100-continue requests
if message.Is100Continue() {
seq := packet.Seq + uint32(len(packet.Data))
t.seqWithData[seq] = packet.Ack
message.DataSeq = seq
// In case if sequence packet came first
for _, m := range t.messages {
if m.Seq == seq {
t.deleteMessage(m)
if m.AssocMessage != nil {
message.AssocMessage = m.AssocMessage
}
// log.Println("2: Adding ack alias:", m.Ack, packet.Ack)
t.ackAliases[m.Ack] = packet.Ack
for _, pkt := range m.packets {
pkt.UpdateAck(packet.Ack)
message.AddPacket(pkt)
}
}
}
// Removing `Expect: 100-continue` header
packet.Data = proto.DeleteHeader(packet.Data, bExpectHeader)
}
// log.Println("Received message:", string(message.Bytes()), message.ID(), t.messages)
if isIncoming {
// If message have multiple packets, delete previous alias
if len(message.packets) > 1 {
delete(t.respAliases, message.ResponseAck)
}
message.UpdateResponseAck()
t.respAliases[message.ResponseAck] = message
}
// If message contains only single packet immediately dispatch it
if message.IsFinished() {
if isIncoming {
// log.Println("I'm finished", string(message.Bytes()), message.ResponseID, t.messages)
if t.trackResponse {
if resp, ok := t.messages[message.ResponseID]; ok {
t.dispatchMessage(message)
if resp.IsFinished() {
t.dispatchMessage(resp)
}
}
} else {
t.dispatchMessage(message)
}
} else {
if message.AssocMessage == nil {
return
}
if req, ok := t.messages[message.AssocMessage.ID()]; ok {
if req.IsFinished() {
t.dispatchMessage(req)
t.dispatchMessage(message)
}
}
}
}
}
func (t *Listener) IsReady() bool {
select {
case <-t.readyCh:
return true
case <-time.After(5 * time.Second):
return false
}
}
// Receive TCP messages from the listener channel
func (t *Listener) Receiver() chan *TCPMessage {
return t.messagesChan
}
func (t *Listener) Close() {
close(t.quit)
if t.conn != nil {
t.conn.Close()
}
for _, h := range t.pcapHandles {
h.Close()
}
return
}
+439
View File
@@ -0,0 +1,439 @@
package rawSocket
import (
"bytes"
"log"
"math/rand"
"sync/atomic"
"testing"
"time"
)
func TestRawListenerInput(t *testing.T) {
var req, resp *TCPMessage
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond)
defer listener.Close()
reqPacket := buildPacket(true, 1, 1, []byte("GET / HTTP/1.1\r\n\r\n"))
respAck := reqPacket.Seq + uint32(len(reqPacket.Data))
respPacket := buildPacket(false, respAck, reqPacket.Seq+1, []byte("HTTP/1.1 200 OK\r\n\r\n"))
listener.packetsChan <- reqPacket.Dump()
listener.packetsChan <- respPacket.Dump()
select {
case req = <-listener.messagesChan:
case <-time.After(time.Millisecond):
t.Error("Should return request immediately")
return
}
if !req.IsIncoming {
t.Error("Should be request")
}
select {
case resp = <-listener.messagesChan:
case <-time.After(20 * time.Millisecond):
t.Error("Should return response immediately")
return
}
if resp.IsIncoming {
t.Error("Should be response")
}
}
func TestRawListenerInputWithoutResponse(t *testing.T) {
var req *TCPMessage
listener := NewListener("", "0", EnginePcap, false, 10*time.Millisecond)
defer listener.Close()
reqPacket := buildPacket(true, 1, 1, []byte("GET / HTTP/1.1\r\n\r\n"))
listener.packetsChan <- reqPacket.Dump()
select {
case req = <-listener.messagesChan:
case <-time.After(time.Millisecond):
t.Error("Should return request immediately")
return
}
if !req.IsIncoming {
t.Error("Should be request")
}
}
func TestRawListenerResponse(t *testing.T) {
var req, resp *TCPMessage
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond)
defer listener.Close()
reqPacket := buildPacket(true, 1, 1, []byte("GET / HTTP/1.1\r\n\r\n"))
respPacket := buildPacket(false, 1+uint32(len(reqPacket.Data)), 2, []byte("HTTP/1.1 200 OK\r\n\r\n"))
// If response packet comes before request
listener.packetsChan <- respPacket.Dump()
listener.packetsChan <- reqPacket.Dump()
select {
case req = <-listener.messagesChan:
case <-time.After(time.Millisecond):
t.Error("Should return respose immediately")
return
}
if !req.IsIncoming {
t.Error("Should be request")
}
select {
case resp = <-listener.messagesChan:
case <-time.After(time.Millisecond):
t.Error("Should return response immediately")
return
}
if resp.IsIncoming {
t.Error("Should be response")
}
if !bytes.Equal(resp.UUID(), req.UUID()) {
t.Error("Resp and Req UUID should be equal")
}
}
func TestShort100Continue(t *testing.T) {
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond)
defer listener.Close()
reqPacket1 := buildPacket(true, 1, 1, []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\nExpect: 100-continue\r\n\r\n"))
// Packet with data have different Seq
reqPacket2 := buildPacket(true, 2, reqPacket1.Seq+uint32(len(reqPacket1.Data)), []byte("a"))
reqPacket3 := buildPacket(true, 2, reqPacket2.Seq+1, []byte("b"))
respPacket1 := buildPacket(false, 10, 3, []byte("HTTP/1.1 100 Continue\r\n"))
// panic(int(uint32(len(reqPacket1.Data)) + uint32(len(reqPacket2.Data)) + uint32(len(reqPacket3.Data))))
respPacket2 := buildPacket(false, reqPacket3.Seq+1 /* len of data */, 2, []byte("HTTP/1.1 200 OK\r\n"))
result := []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\nab")
testRawListener100Continue(t, listener, result, reqPacket1, reqPacket2, reqPacket3, respPacket1, respPacket2)
}
// Response comes before Request
func Test100ContinueWrongOrder(t *testing.T) {
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond)
defer listener.Close()
reqPacket1 := buildPacket(true, 1, 1, []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\nExpect: 100-continue\r\n\r\n"))
// Packet with data have different Seq
reqPacket2 := buildPacket(true, 2, reqPacket1.Seq+uint32(len(reqPacket1.Data)), []byte("a"))
reqPacket3 := buildPacket(true, 2, reqPacket2.Seq+1, []byte("b"))
respPacket1 := buildPacket(false, 10, 3, []byte("HTTP/1.1 100 Continue\r\n"))
// panic(int(uint32(len(reqPacket1.Data)) + uint32(len(reqPacket2.Data)) + uint32(len(reqPacket3.Data))))
respPacket2 := buildPacket(false, reqPacket3.Seq+1 /* len of data */, 2, []byte("HTTP/1.1 200 OK\r\n"))
result := []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\nab")
testRawListener100Continue(t, listener, result, respPacket1, respPacket2, reqPacket1, reqPacket2, reqPacket3)
}
func TestAlt100ContinueHeaderOrder(t *testing.T) {
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond)
defer listener.Close()
reqPacket1 := buildPacket(true, 1, 1, []byte("POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 2\r\n\r\n"))
// Packet with data have different Seq
reqPacket2 := buildPacket(true, 2, reqPacket1.Seq+uint32(len(reqPacket1.Data)), []byte("a"))
reqPacket3 := buildPacket(true, 2, reqPacket2.Seq+1, []byte("b"))
respPacket1 := buildPacket(false, 10, 3, []byte("HTTP/1.1 100 Continue\r\n"))
// panic(int(uint32(len(reqPacket1.Data)) + uint32(len(reqPacket2.Data)) + uint32(len(reqPacket3.Data))))
respPacket2 := buildPacket(false, reqPacket3.Seq+1 /* len of data */, 2, []byte("HTTP/1.1 200 OK\r\n"))
result := []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\nab")
testRawListener100Continue(t, listener, result, reqPacket1, reqPacket2, reqPacket3, respPacket1, respPacket2)
}
func testRawListener100Continue(t *testing.T, listener *Listener, result []byte, packets ...*TCPPacket) {
var req, resp *TCPMessage
for _, p := range packets {
listener.packetsChan <- p.Dump()
}
select {
case req = <-listener.messagesChan:
break
case <-time.After(11 * time.Millisecond):
t.Error("Should return response after expire time")
return
}
if !bytes.Equal(req.Bytes(), result) {
t.Error("Should receive full message", string(req.Bytes()))
}
if !req.IsIncoming {
t.Error("Should be request")
}
select {
case resp = <-listener.messagesChan:
break
case <-time.After(21 * time.Millisecond):
t.Error("Should return response after expire time")
return
}
if resp.IsIncoming {
t.Error("Should be response")
}
if !bytes.Equal(resp.UUID(), req.UUID()) {
t.Error("Resp and Req UUID should be equal")
}
}
func testChunkedSequence(t *testing.T, listener *Listener, packets ...*TCPPacket) {
var r, req, resp *TCPMessage
for _, p := range packets {
listener.packetsChan <- p.Dump()
}
select {
case r = <-listener.messagesChan:
if r.IsIncoming {
req = r
} else {
resp = r
}
break
case <-time.After(25 * time.Millisecond):
t.Error("Should return request after expire time")
return
}
select {
case r = <-listener.messagesChan:
if r.IsIncoming {
if req != nil {
t.Error("Request already received", r)
return
}
req = r
} else {
if resp != nil {
t.Error("Response already received", r)
return
}
resp = r
}
break
case <-time.After(25 * time.Millisecond):
t.Error("Should return request after expire time")
return
}
if !bytes.Equal(req.Bytes(), []byte("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n1\r\na\r\n1\r\nb\r\n0\r\n\r\n")) {
t.Error("Should receive full message", string(req.Bytes()))
}
if !req.IsIncoming {
t.Error("Should be request")
}
if resp.IsIncoming {
t.Error("Should be response")
}
if !bytes.Equal(resp.UUID(), req.UUID()) {
t.Error("Resp and Req UUID should be equal", string(resp.UUID()), string(req.UUID()))
}
time.Sleep(20 * time.Millisecond)
if len(listener.packetsChan) != 0 {
t.Fatal("packetsChan non empty:", listener.packetsChan)
}
if len(listener.messagesChan) != 0 {
t.Fatal("messagesChan non empty:", <-listener.messagesChan)
}
if len(listener.messages) != 0 {
t.Fatal("Messages non empty:", listener.messages)
}
if len(listener.ackAliases) != 0 {
t.Fatal("ackAliases non empty:", listener.ackAliases)
}
if len(listener.seqWithData) != 0 {
t.Fatal("seqWithData non empty:", listener.seqWithData)
}
if len(listener.respAliases) != 0 {
t.Fatal("respAliases non empty:", listener.respAliases)
}
if len(listener.respWithoutReq) != 0 {
t.Fatal("respWithoutReq non empty:", listener.respWithoutReq)
}
}
func permutation(n int, list []*TCPPacket) []*TCPPacket {
if len(list) == 1 {
return list
}
k := n % len(list)
first := []*TCPPacket{list[k]}
next := make([]*TCPPacket, len(list)-1)
copy(next, append(list[:k], list[k+1:]...))
return append(first, permutation(n/len(list), next)...)
}
// Response comes before Request
func TestRawListenerChunkedWrongOrder(t *testing.T) {
listener := NewListener("", "0", EnginePcap, true, 10*time.Millisecond)
defer listener.Close()
reqPacket1 := buildPacket(true, 1, 1, []byte("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nExpect: 100-continue\r\n\r\n"))
// Packet with data have different Seq
reqPacket2 := buildPacket(true, 2, reqPacket1.Seq+uint32(len(reqPacket1.Data)), []byte("1\r\na\r\n"))
reqPacket3 := buildPacket(true, 2, reqPacket2.Seq+uint32(len(reqPacket2.Data)), []byte("1\r\nb\r\n"))
reqPacket4 := buildPacket(true, 2, reqPacket3.Seq+uint32(len(reqPacket3.Data)), []byte("0\r\n\r\n"))
respPacket1 := buildPacket(false, 10, 3, []byte("HTTP/1.1 100 Continue\r\n"))
// panic(int(uint32(len(reqPacket1.Data)) + uint32(len(reqPacket2.Data)) + uint32(len(reqPacket3.Data))))
respPacket2 := buildPacket(false, reqPacket4.Seq+5 /* len of data */, 2, []byte("HTTP/1.1 200 OK\r\n"))
// Should re-construct message from all possible combinations
for i := 0; i < 6*5*4*3*2*1; i++ {
if i < 54 || i > 57 {
continue
}
packets := permutation(i, []*TCPPacket{reqPacket1, reqPacket2, reqPacket3, reqPacket4, respPacket1, respPacket2})
t.Log("permutation:", i)
testChunkedSequence(t, listener, packets...)
}
}
func chunkedPostMessage() []*TCPPacket {
ack := uint32(rand.Int63())
seq := uint32(rand.Int63())
reqPacket1 := buildPacket(true, ack, seq, []byte("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"))
// Packet with data have different Seq
reqPacket2 := buildPacket(true, ack, seq+47, []byte("1\r\na\r\n"))
reqPacket3 := buildPacket(true, ack, reqPacket2.Seq+5, []byte("1\r\nb\r\n"))
reqPacket4 := buildPacket(true, ack, reqPacket3.Seq+5, []byte("0\r\n\r\n"))
respPacket := buildPacket(false, reqPacket4.Seq+5 /* len of data */, ack, []byte("HTTP/1.1 200 OK\r\n"))
return []*TCPPacket{
reqPacket1, reqPacket2, reqPacket3, reqPacket4, respPacket,
}
}
func postMessage() []*TCPPacket {
ack := uint32(rand.Int63())
seq2 := uint32(rand.Int63())
seq := uint32(rand.Int63())
c := 10000
data := make([]byte, c)
rand.Read(data)
head := []byte("POST / HTTP/1.1\r\nContent-Length: 9958\r\n\r\n")
for i := range head {
data[i] = head[i]
}
return []*TCPPacket{
buildPacket(true, ack, seq, data),
buildPacket(false, seq+uint32(len(data)), seq2, []byte("HTTP/1.1 200 OK\r\n")),
}
}
func getMessage() []*TCPPacket {
ack := uint32(rand.Int63())
seq2 := uint32(rand.Int63())
seq := uint32(rand.Int63())
return []*TCPPacket{
buildPacket(true, ack, seq, []byte("GET / HTTP/1.1\r\n\r\n")),
buildPacket(false, seq+18, seq2, []byte("HTTP/1.1 200 OK\r\n")),
}
}
// Response comes before Request
func TestRawListenerBench(t *testing.T) {
l := NewListener("", "0", EnginePcap, true, 200*time.Millisecond)
defer l.Close()
// Should re-construct message from all possible combinations
for i := 0; i < 1000; i++ {
go func(i int) {
for j := 0; j < 100; j++ {
var packets []*TCPPacket
if j%5 == 0 {
packets = chunkedPostMessage()
} else if j%3 == 0 {
packets = postMessage()
} else {
packets = getMessage()
}
for _, p := range packets {
// Randomly drop packets
if (i+j)%5 == 0 {
if rand.Int63()%3 == 0 {
continue
}
}
l.packetsChan <- p.Dump()
time.Sleep(time.Millisecond)
}
time.Sleep(5 * time.Millisecond)
}
}(i)
}
ch := l.Receiver()
var count int32
for {
select {
case <-ch:
atomic.AddInt32(&count, 1)
case <-time.After(2000 * time.Millisecond):
log.Println("Emitted 200000 messages, captured: ", count, len(l.ackAliases), len(l.seqWithData), len(l.respAliases), len(l.respWithoutReq), len(l.packetsChan))
return
}
}
}
+303
View File
@@ -0,0 +1,303 @@
package rawSocket
import (
"bytes"
"crypto/sha1"
"encoding/binary"
"encoding/hex"
"github.com/buger/gor/proto"
"log"
"strconv"
"time"
"net"
)
var _ = log.Println
// TCPMessage ensure that all TCP packets for given request is received, and processed in right sequence
// Its needed because all TCP message can be fragmented or re-transmitted
//
// Each TCP Packet have 2 ids: acknowledgment - message_id, and sequence - packet_id
// Message can be compiled from unique packets with same message_id which sorted by sequence
// Message is received if we didn't receive any packets for 2000ms
type TCPMessage struct {
Seq uint32
Ack uint32
ResponseAck uint32
ResponseID tcpID
DataAck uint32
DataSeq uint32
AssocMessage *TCPMessage
Start time.Time
End time.Time
IsIncoming bool
packets []*TCPPacket
delChan chan *TCPMessage
}
// NewTCPMessage pointer created from a Acknowledgment number and a channel of messages readuy to be deleted
func NewTCPMessage(Seq, Ack uint32, IsIncoming bool) (msg *TCPMessage) {
msg = &TCPMessage{Seq: Seq, Ack: Ack, IsIncoming: IsIncoming}
msg.Start = time.Now()
return
}
// Bytes return message content
func (t *TCPMessage) Bytes() (output []byte) {
for _, p := range t.packets {
output = append(output, p.Data...)
}
return output
}
// Size returns total body size
func (t *TCPMessage) BodySize() (size int) {
if len(t.packets) == 0 {
return 0
}
size += len(proto.Body(t.packets[0].Data))
for _, p := range t.packets[1:] {
size += len(p.Data)
}
return
}
// Size returns total size of message
func (t *TCPMessage) Size() (size int) {
if len(t.packets) == 0 {
return 0
}
for _, p := range t.packets {
size += len(p.Data)
}
return
}
// AddPacket to the message and ensure packet uniqueness
// TCP allows that packet can be re-send multiple times
func (t *TCPMessage) AddPacket(packet *TCPPacket) {
packetFound := false
for _, pkt := range t.packets {
if packet.Seq == pkt.Seq {
packetFound = true
break
}
}
if !packetFound {
// Packets not always captured in same Seq order, and sometimes we need to prepend
if len(t.packets) == 0 || packet.Seq > t.packets[len(t.packets)-1].Seq {
t.packets = append(t.packets, packet)
} else if packet.Seq < t.packets[0].Seq {
t.packets = append([]*TCPPacket{packet}, t.packets...)
t.Seq = packet.Seq // Message Seq should indicated starting seq
} else { // insert somewhere in the middle...
for i, p := range t.packets {
if packet.Seq < p.Seq {
t.packets = append(t.packets[:i], append([]*TCPPacket{packet}, t.packets[i:]...)...)
break
}
}
}
if t.IsIncoming {
t.End = time.Now()
} else {
t.End = time.Now().Add(time.Millisecond)
}
if packet.OrigAck != 0 {
t.DataAck = packet.OrigAck
}
}
}
// Check if there is missing packet
func (t *TCPMessage) isSeqMissing() bool {
if len(t.packets) == 1 {
return false
}
for i, p := range t.packets {
// If final packet
if len(t.packets) == i+1 {
return false
}
np := t.packets[i+1]
if np.Seq != p.Seq+uint32(len(p.Data)) {
return true
}
}
return false
}
var EmptyLine = []byte("\r\n\r\n")
var ChunkEnd = []byte("0\r\n\r\n")
func (t *TCPMessage) isHeadersReceived() bool {
for _, p := range t.packets {
if bytes.LastIndex(p.Data, EmptyLine) != -1 {
return true
}
}
return false
}
// isMultipart returns true if message contains from multiple tcp packets
func (t *TCPMessage) IsFinished() bool {
payload := t.packets[0].Data
if len(payload) < 4 {
return true
}
m := payload[:4]
if t.IsIncoming {
// If one GET, OPTIONS, or HEAD request
if bytes.Equal(m, []byte("GET ")) || bytes.Equal(m, []byte("OPTI")) || bytes.Equal(m, []byte("HEAD")) {
if !t.isSeqMissing() && t.isHeadersReceived() {
return true
} else {
return false
}
} else {
// Sometimes header comes after the body :(
if bytes.Equal(m, []byte("POST")) || bytes.Equal(m, []byte("PUT ")) || bytes.Equal(m, []byte("PATC")) {
if t.isHeadersReceived() {
if length := proto.Header(payload, []byte("Content-Length")); len(length) > 0 {
l, _ := strconv.Atoi(string(length))
// If content-length equal current body length
if l > 0 && l == t.BodySize() {
return true
}
}
}
}
}
} else {
// Request not found
// Can be because response came first or request request was just missing
if t.AssocMessage == nil {
return false
}
if !bytes.Equal(m, []byte("HTTP")) {
return false
}
if length := proto.Header(payload, []byte("Content-Length")); len(length) > 0 {
if length[0] == '0' {
return true
}
l, _ := strconv.Atoi(string(length))
// If content-length equal current body length
if l > 0 && l == t.BodySize() {
return true
}
} else {
if enc := proto.Header(payload, []byte("Transfer-Encoding")); len(enc) == 0 {
return true
} else {
if len(t.packets) > 1 && bytes.LastIndex(t.packets[len(t.packets)-1].Data, ChunkEnd) != -1 {
return true
}
}
}
}
return false
}
var bExpectHeader = []byte("Expect:")
var bExpect100Value = []byte("100-continue")
var bPOST = []byte("POST")
var bCRLFx2 = []byte("\r\n\r\n")
func (t *TCPMessage) Is100Continue() bool {
d := t.packets[0].Data
if len(d) < 25 {
return false
}
if !bytes.Equal(d[0:4], bPOST) {
return false
}
// reading last 4 bytes for double CRLF
if !bytes.Equal(d[len(d)-4:], bCRLFx2) {
return false
}
// look for an expect:100-continue header
if !bytes.Equal(bExpect100Value, proto.Header(d, bExpectHeader)) {
return false
}
return true
}
func (t *TCPMessage) UUID() []byte {
var key []byte
if t.IsIncoming {
// log.Println("UUID:", t.Ack, t.Start.UnixNano())
key = strconv.AppendInt(key, t.Start.UnixNano(), 10)
key = strconv.AppendUint(key, uint64(t.Ack), 10)
} else {
// log.Println("RequestMessage:", t.AssocMessage.Ack, t.AssocMessage.Start.UnixNano())
key = strconv.AppendInt(key, t.AssocMessage.Start.UnixNano(), 10)
key = strconv.AppendUint(key, uint64(t.AssocMessage.Ack), 10)
}
uuid := make([]byte, 40)
sha := sha1.Sum(key)
hex.Encode(uuid, sha[:20])
return uuid
}
// UpdateResponseAck should be called after packet is added
func (t *TCPMessage) UpdateResponseAck() uint32 {
lastPacket := t.packets[len(t.packets)-1]
respAck := lastPacket.Seq + uint32(len(lastPacket.Data))
if t.ResponseAck != respAck {
t.ResponseAck = lastPacket.Seq + uint32(len(lastPacket.Data))
// We swappwed src and dst port
copy(t.ResponseID[:16], lastPacket.Addr)
copy(t.ResponseID[16:], lastPacket.Raw[2:4]) // Src port
copy(t.ResponseID[18:], lastPacket.Raw[0:2]) // Dest port
binary.BigEndian.PutUint32(t.ResponseID[20:24], t.ResponseAck)
}
return t.ResponseAck
}
func (t *TCPMessage) ID() tcpID {
return t.packets[0].ID
}
func (t *TCPMessage) IP() net.IP {
return net.IP(t.packets[0].Addr)
}
+191
View File
@@ -0,0 +1,191 @@
package rawSocket
import (
"bytes"
"encoding/binary"
_ "log"
"testing"
)
func buildPacket(isIncoming bool, Ack, Seq uint32, Data []byte) (packet *TCPPacket) {
var srcPort, destPort uint16
// For tests `listening` port is 0
if isIncoming {
srcPort = 1
} else {
destPort = 1
}
buf := make([]byte, 16)
binary.BigEndian.PutUint16(buf[2:4], destPort)
binary.BigEndian.PutUint16(buf[0:2], srcPort)
binary.BigEndian.PutUint32(buf[4:8], Seq)
binary.BigEndian.PutUint32(buf[8:12], Ack)
buf[12] = 64
buf = append(buf, Data...)
packet = ParseTCPPacket([]byte("123"), buf)
return packet
}
func buildMessage(p *TCPPacket) *TCPMessage {
isIncoming := false
if p.SrcPort == 1 {
isIncoming = true
}
m := NewTCPMessage(p.Seq, p.Ack, isIncoming)
m.AddPacket(p)
return m
}
func TestTCPMessagePacketsOrder(t *testing.T) {
msg := buildMessage(buildPacket(true, 1, 1, []byte("a")))
msg.AddPacket(buildPacket(true, 1, 2, []byte("b")))
if !bytes.Equal(msg.Bytes(), []byte("ab")) {
t.Error("Should contatenate packets in right order")
}
// When first packet have wrong order (Seq)
msg = buildMessage(buildPacket(true, 1, 2, []byte("b")))
msg.AddPacket(buildPacket(true, 1, 1, []byte("a")))
if !bytes.Equal(msg.Bytes(), []byte("ab")) {
t.Error("Should contatenate packets in right order")
}
// Should ignore packets with same sequence
msg = buildMessage(buildPacket(true, 1, 1, []byte("a")))
msg.AddPacket(buildPacket(true, 1, 1, []byte("a")))
if !bytes.Equal(msg.Bytes(), []byte("a")) {
t.Error("Should ignore packet with same Seq")
}
}
func TestTCPMessageSize(t *testing.T) {
msg := buildMessage(buildPacket(true, 1, 1, []byte("POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\na")))
msg.AddPacket(buildPacket(true, 1, 2, []byte("b")))
if msg.BodySize() != 2 {
t.Error("Should count only body", msg.BodySize())
}
if msg.Size() != 40 {
t.Error("Should count all sizes", msg.Size())
}
}
func TestTCPMessageIsFinished(t *testing.T) {
methodsWithoutBodies := []string{"GET", "OPTIONS", "HEAD"}
for _, m := range methodsWithoutBodies {
msg := buildMessage(buildPacket(true, 1, 1, []byte(m+" / HTTP/1.1\r\n\r\n")))
if !msg.IsFinished() {
t.Error(m, " request should be finished")
}
}
methodsWithBodies := []string{"POST", "PUT", "PATCH"}
for _, m := range methodsWithBodies {
msg := buildMessage(buildPacket(true, 1, 1, []byte(m+" / HTTP/1.1\r\nContent-Length: 1\r\n\r\na")))
if !msg.IsFinished() {
t.Error(m, " should be finished as body length == content length")
}
msg = buildMessage(buildPacket(true, 1, 1, []byte(m+" / HTTP/1.1\r\nContent-Length: 2\r\n\r\na")))
if msg.IsFinished() {
t.Error(m, " should not be finished as body length != content length")
}
}
msg := buildMessage(buildPacket(true, 1, 1, []byte("UNKNOWN / HTTP/1.1\r\n\r\n")))
if msg.IsFinished() {
t.Error("non http or wrong methods considered as not finished")
}
// Responses
msg = buildMessage(buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\n\r\n")))
msg.AssocMessage = &TCPMessage{}
if !msg.IsFinished() {
t.Error("Should mark simple response as finished")
}
msg = buildMessage(buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\n\r\n")))
msg.AssocMessage = nil
if msg.IsFinished() {
t.Error("Should not mark responses without associated requests")
}
msg = buildMessage(buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n")))
msg.AssocMessage = &TCPMessage{}
if msg.IsFinished() {
t.Error("Should mark chunked response as non finished")
}
msg = buildMessage(buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")))
msg.AssocMessage = &TCPMessage{}
if !msg.IsFinished() {
t.Error("Should mark Content-Length: 0 respones as finished")
}
msg = buildMessage(buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\nContent-Length: 1\r\n\r\na")))
msg.AssocMessage = &TCPMessage{}
if !msg.IsFinished() {
t.Error("Should mark valid Content-Length respones as finished")
}
msg = buildMessage(buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\na")))
msg.AssocMessage = &TCPMessage{}
if msg.IsFinished() {
t.Error("Should not mark not valid Content-Length respones as finished")
}
}
func TestTCPMessageIsSeqMissing(t *testing.T) {
p1 := buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\n"))
p2 := buildPacket(false, 1, p1.Seq+uint32(len(p1.Data)), []byte("Content-Length: 10\r\n\r\n"))
p3 := buildPacket(false, 1, p2.Seq+uint32(len(p2.Data)), []byte("a"))
msg := buildMessage(p1)
if msg.isSeqMissing() {
t.Error("Should be complete if have only 1 packet")
}
msg.AddPacket(p3)
if !msg.isSeqMissing() {
t.Error("Should be incomplete because missing middle component")
}
msg.AddPacket(p2)
if msg.isSeqMissing() {
t.Error("Should be complete once missing packet added")
}
}
func TestTCPMessageIsHeadersReceived(t *testing.T) {
p1 := buildPacket(false, 1, 1, []byte("HTTP/1.1 200 OK\r\n"))
p2 := buildPacket(false, 1, p1.Seq+uint32(len(p1.Data)), []byte("Content-Length: 10\r\n\r\n"))
msg := buildMessage(p1)
if msg.isHeadersReceived() {
t.Error("Should be complete if have only 1 packet")
}
msg.AddPacket(p2)
if !msg.isHeadersReceived() {
t.Error("Should found double new line: headers received")
}
}
+116
View File
@@ -0,0 +1,116 @@
package rawSocket
import (
"encoding/binary"
"log"
"strconv"
"strings"
)
var _ = log.Println
// TCP Flags
const (
fFIN = 1 << iota
fSYN
fRST
fPSH
fACK
fURG
fECE
fCWR
fNS
)
type tcpID [24]byte
// TCPPacket provides tcp packet parser
// Packet structure: http://en.wikipedia.org/wiki/Transmission_Control_Protocol
type TCPPacket struct {
SrcPort uint16
DestPort uint16
Seq uint32
Ack uint32
OrigAck uint32
DataOffset uint8
Raw []byte
Data []byte
Addr []byte
ID tcpID
}
// ParseTCPPacket takes address and tcp payload and returns parsed TCPPacket
func ParseTCPPacket(addr []byte, data []byte) (p *TCPPacket) {
p = &TCPPacket{Raw: data}
p.ParseBasic()
p.Addr = addr
p.GenID()
return
}
func (p *TCPPacket) GenID() {
copy(p.ID[:16], p.Addr)
copy(p.ID[16:], p.Raw[0:2]) // Src port
copy(p.ID[18:], p.Raw[2:4]) // Dest port
copy(p.ID[20:], p.Raw[8:12]) // Ack
}
func (p *TCPPacket) UpdateAck(ack uint32) {
p.OrigAck = p.Ack
p.Ack = ack
binary.BigEndian.PutUint32(p.Raw[8:12], ack)
p.GenID()
}
// ParseBasic set of fields
func (t *TCPPacket) ParseBasic() {
t.DestPort = binary.BigEndian.Uint16(t.Raw[2:4])
t.SrcPort = binary.BigEndian.Uint16(t.Raw[0:2])
t.Seq = binary.BigEndian.Uint32(t.Raw[4:8])
t.Ack = binary.BigEndian.Uint32(t.Raw[8:12])
t.DataOffset = (t.Raw[12] & 0xF0) >> 4
// log.Println("DataOffset:", t.DataOffset, t.DestPort, t.SrcPort, t.Seq, t.Ack)
t.Data = t.Raw[t.DataOffset*4:]
}
func (t *TCPPacket) Dump() []byte {
buf := make([]byte, len(t.Data)+16+16)
copy(buf[:16], t.Addr)
tcpBuf := buf[16:]
binary.BigEndian.PutUint16(tcpBuf[2:4], t.DestPort)
binary.BigEndian.PutUint16(tcpBuf[0:2], t.SrcPort)
binary.BigEndian.PutUint32(tcpBuf[4:8], t.Seq)
binary.BigEndian.PutUint32(tcpBuf[8:12], t.Ack)
tcpBuf[12] = 64
copy(tcpBuf[16:], t.Data)
return buf
}
// String output for a TCP Packet
func (t *TCPPacket) String() string {
maxLen := len(t.Data)
if maxLen > 200 {
maxLen = 200
}
return strings.Join([]string{
"Addr: " + string(t.Addr),
"Source port: " + strconv.Itoa(int(t.SrcPort)),
"Dest port:" + strconv.Itoa(int(t.DestPort)),
"Sequence:" + strconv.Itoa(int(t.Seq)),
"Acknowledgment:" + strconv.Itoa(int(t.Ack)),
"Header len:" + strconv.Itoa(int(t.DataOffset)),
"Data size:" + strconv.Itoa(len(t.Data)),
"Data:" + string(t.Data[:maxLen]),
}, "\n")
}
-98
View File
@@ -1,98 +0,0 @@
// Replay server receive requests objects from Listeners and forward it to given address.
// Basic usage:
//
// gor replay -f http://staging.server
//
//
// Rate limiting
//
// It can be useful if you want forward only part of production traffic, not to overload staging environment. You can specify desired request per second using "|" operator after server address:
//
// # staging.server not get more than 10 requests per second
// gor replay -f "http://staging.server|10"
//
//
// Forward to multiple addresses
//
// Just separate addresses by coma:
// gor replay -f "http://staging.server|10,http://dev.server|20"
//
//
// For more help run:
//
// gor replay -h
//
package replay
import (
"bufio"
"bytes"
"log"
"net"
"net/http"
)
const bufSize = 1024 * 10
// Enable debug logging only if "--verbose" flag passed
func Debug(v ...interface{}) {
if Settings.verbose {
log.Println(v...)
}
}
func ParseRequest(data []byte) (request *http.Request, err error) {
buf := bytes.NewBuffer(data)
reader := bufio.NewReader(buf)
request, err = http.ReadRequest(reader)
return
}
// Because its sub-program, Run acts as `main`
// Replay server listen to UDP traffic from Listeners
// Each request processed by RequestFactory
func Run() {
var buf [bufSize]byte
addr, err := net.ResolveUDPAddr("udp", Settings.Address())
if err != nil {
log.Fatal("Can't start:", err)
}
conn, err := net.ListenUDP("udp", addr)
log.Println("Starting replay server at:", Settings.Address())
if err != nil {
log.Fatal("Can't start:", err)
}
defer conn.Close()
for _, host := range Settings.ForwardedHosts() {
log.Println("Forwarding requests to:", host.Url, "limit:", host.Limit)
}
requestFactory := NewRequestFactory()
for {
n, _, err := conn.ReadFromUDP(buf[0:])
if err != nil {
continue
}
if n > 0 {
if n > bufSize {
Debug("Too large udp packet", bufSize)
}
if request, err := ParseRequest(buf[0:n]); err != nil {
Debug("Error while parsing request", err, buf[0:n])
} else {
requestFactory.Add(request)
}
}
}
}
-92
View File
@@ -1,92 +0,0 @@
package replay
import (
"net/http"
"net/url"
)
type HttpResponse struct {
host *ForwardHost
req *http.Request
resp *http.Response
err error
}
// Class for processing requests
//
// Basic workflow:
//
// 1. When request added via Add() it get pushed to `responses` chan
// 2. handleRequest() listen for `responses` chan and decide where request should be forwarded, and apply rate-limit if needed
// 3. sendRequest() forwards request and returns response info to `responses` chan
// 4. handleRequest() listen for `response` channel and updates stats
type RequestFactory struct {
responses chan *HttpResponse
requests chan *http.Request
}
// RequestFactory contstuctor
// One created, it starts listening for incoming requests: requests channel
func NewRequestFactory() (factory *RequestFactory) {
factory = &RequestFactory{}
factory.responses = make(chan *HttpResponse)
factory.requests = make(chan *http.Request)
go factory.handleRequests()
return
}
// Forward http request to given host
func (f *RequestFactory) sendRequest(host *ForwardHost, request *http.Request) {
client := &http.Client{}
URL := host.Url + request.URL.String()
request.RequestURI = ""
request.URL, _ = url.ParseRequestURI(URL)
if Settings.verbose {
Debug("Sending request:", request)
}
resp, err := client.Do(request)
if err == nil {
defer resp.Body.Close()
} else {
Debug("Request error:", err)
}
f.responses <- &HttpResponse{host, request, resp, err}
}
// Handle incoming requests, and they responses
func (f *RequestFactory) handleRequests() {
hosts := Settings.ForwardedHosts()
for {
select {
case req := <-f.requests:
for _, host := range hosts {
// Ensure that we have actual stats for given timestamp
host.Stat.Touch()
if host.Limit == 0 || host.Stat.Count < host.Limit {
// Increment Stat.Count
host.Stat.IncReq()
go f.sendRequest(host, req)
}
}
case resp := <-f.responses:
// Increment returned http code stats, and elapsed time
resp.host.Stat.IncResp(resp)
}
}
}
// Add request to channel for further processing
func (f *RequestFactory) Add(request *http.Request) {
f.requests <- request
}
-63
View File
@@ -1,63 +0,0 @@
package replay
import (
"time"
)
// Stats stores in context of current timestamp
type RequestStat struct {
timestamp int64
Codes map[int]int // { 200: 10, 404:2, 500:1 }
Count int // All requests including errors
Errors int // Rquests with errors (timeout or host not reachable). Not include 50x errors.
host *ForwardHost
}
// Ensure that current stats is actual (for current timestamp)
func (s *RequestStat) Touch() {
if s.timestamp != time.Now().Unix() {
s.reset()
}
}
// Called on request start
func (s *RequestStat) IncReq() {
s.Count++
}
// Called after response
func (s *RequestStat) IncResp(resp *HttpResponse) {
s.Touch()
if resp.err != nil {
s.Errors++
return
}
s.Codes[resp.resp.StatusCode]++
}
// Updated stats timestamp to current time and reset to zero all stats values
// TODO: Further on reset it should write stats to file
func (s *RequestStat) reset() {
if s.timestamp != 0 {
Debug("Host:", s.host.Url, "Requests:", s.Count, "Errors:", s.Errors, "Status codes:", s.Codes)
}
s.timestamp = time.Now().Unix()
s.Codes = make(map[int]int)
s.Count = 0
s.Errors = 0
}
// RequestStat constructor
func NewRequestStats(host *ForwardHost) (stat *RequestStat) {
stat = &RequestStat{host: host}
stat.reset()
return
}
-80
View File
@@ -1,80 +0,0 @@
package replay
import (
"strconv"
"strings"
"os"
"flag"
)
type ForwardHost struct {
Url string
Limit int
Stat *RequestStat
}
type ReplaySettings struct {
port int
host string
forwardAddress string
verbose bool
}
// ForwardedHosts implements forwardAddress syntax support for multiple hosts (coma separated), and rate limiting by specifing "|maxRps" after host name.
//
// -f "host1,http://host2|10,host3"
//
func (r *ReplaySettings) ForwardedHosts() (hosts []*ForwardHost) {
hosts = make([]*ForwardHost, 0, 10)
for _, address := range strings.Split(r.forwardAddress, ",") {
host_info := strings.Split(address, "|")
if strings.Index(host_info[0], "http") == -1 {
host_info[0] = "http://" + host_info[0]
}
host := &ForwardHost{Url: host_info[0]}
host.Stat = NewRequestStats(host)
if len(host_info) > 1 {
host.Limit, _ = strconv.Atoi(host_info[1])
}
hosts = append(hosts, host)
}
return
}
// Helper to return address with port, e.g.: 127.0.0.1:28020
func (r *ReplaySettings) Address() string {
return r.host + ":" + strconv.Itoa(r.port)
}
var Settings ReplaySettings = ReplaySettings{}
func init() {
if len(os.Args) < 2 || os.Args[1] != "replay" {
return
}
const (
defaultPort = 28020
defaultHost = "0.0.0.0"
defaultAddress = "http://localhost:8080"
)
flag.IntVar(&Settings.port, "p", defaultPort, "specify port number")
flag.StringVar(&Settings.host, "ip", defaultHost, "ip addresses to listen on")
flag.StringVar(&Settings.forwardAddress, "f", defaultAddress, "http address to forward traffic.\n\tYou can limit requests per second by adding `|num` after address.\n\tIf you have multiple addresses with different limits. For example: http://staging.example.com|100,http://dev.example.com|10")
flag.BoolVar(&Settings.verbose, "verbose", false, "Log requests")
}
+166
View File
@@ -0,0 +1,166 @@
package main
import (
"flag"
"fmt"
"os"
"sync"
"time"
)
var VERSION string
// MultiOption allows to specify multiple flags with same name and collects all values into array
type MultiOption []string
func (h *MultiOption) String() string {
return fmt.Sprint(*h)
}
// Set gets called multiple times for each flag with same name
func (h *MultiOption) Set(value string) error {
*h = append(*h, value)
return nil
}
// AppSettings is the struct of main configuration
type AppSettings struct {
verbose bool
debug bool
stats bool
splitOutput bool
inputDummy MultiOption
outputDummy MultiOption
outputStdout bool
inputTCP MultiOption
outputTCP MultiOption
outputTCPStats bool
inputFile MultiOption
inputFileLoop bool
outputFile MultiOption
outputFileConfig FileOutputConfig
inputRAW MultiOption
inputRAWEngine string
inputRAWTrackResponse bool
inputRAWRealIPHeader string
middleware string
inputHTTP MultiOption
outputHTTP MultiOption
outputHTTPConfig HTTPOutputConfig
modifierConfig HTTPModifierConfig
}
// Settings holds Gor configuration
var Settings AppSettings
func usage() {
fmt.Printf("Gor is a simple http traffic replication tool written in Go. Its main goal is to replay traffic from production servers to staging and dev environments.\nProject page: https://github.com/buger/gor\nAuthor: <Leonid Bugaev> leonsbox@gmail.com\nCurrent Version: %s\n\n", VERSION)
flag.PrintDefaults()
os.Exit(2)
}
func init() {
flag.Usage = usage
flag.BoolVar(&Settings.verbose, "verbose", false, "Turn on more verbose output")
flag.BoolVar(&Settings.debug, "debug", false, "Turn on debug output, shows all intercepted traffic. Works only when with `verbose` flag")
flag.BoolVar(&Settings.stats, "stats", false, "Turn on queue stats output")
flag.BoolVar(&Settings.splitOutput, "split-output", false, "By default each output gets same traffic. If set to `true` it splits traffic equally among all outputs.")
flag.Var(&Settings.inputDummy, "input-dummy", "Used for testing outputs. Emits 'Get /' request every 1s")
flag.Var(&Settings.outputDummy, "output-dummy", "DEPRECATED: use --output-stdout instead")
flag.BoolVar(&Settings.outputStdout, "output-stdout", false, "Used for testing inputs. Just prints to console data coming from inputs.")
flag.Var(&Settings.inputTCP, "input-tcp", "Used for internal communication between Gor instances. Example: \n\t# Receive requests from other Gor instances on 28020 port, and redirect output to staging\n\tgor --input-tcp :28020 --output-http staging.com")
flag.Var(&Settings.outputTCP, "output-tcp", "Used for internal communication between Gor instances. Example: \n\t# Listen for requests on 80 port and forward them to other Gor instance on 28020 port\n\tgor --input-raw :80 --output-tcp replay.local:28020")
flag.BoolVar(&Settings.outputTCPStats, "output-tcp-stats", false, "Report TCP output queue stats to console every 5 seconds.")
flag.Var(&Settings.inputFile, "input-file", "Read requests from file: \n\tgor --input-file ./requests.gor --output-http staging.com")
flag.BoolVar(&Settings.inputFileLoop, "input-file-loop", false, "Loop input files, useful for performance testing.")
flag.Var(&Settings.outputFile, "output-file", "Write incoming requests to file: \n\tgor --input-raw :80 --output-file ./requests.gor")
flag.DurationVar(&Settings.outputFileConfig.flushInterval, "output-file-flush-interval", time.Minute, "Interval for forcing buffer flush to the file, default: 60s.")
flag.BoolVar(&Settings.outputFileConfig.append, "output-file-append", false, "The flushed chunk is appended to existence file or not. ")
// Set default
Settings.outputFileConfig.sizeLimit.Set("32mb")
flag.Var(&Settings.outputFileConfig.sizeLimit, "output-file-size-limit", "Size of each chunk. Default: 32mb")
flag.IntVar(&Settings.outputFileConfig.queueLimit, "output-file-queue-limit", 256, "The length of the chunk queue. Default: 256")
flag.Var(&Settings.inputRAW, "input-raw", "Capture traffic from given port (use RAW sockets and require *sudo* access):\n\t# Capture traffic from 8080 port\n\tgor --input-raw :8080 --output-http staging.com")
flag.BoolVar(&Settings.inputRAWTrackResponse, "input-raw-track-response", false, "If turned on Gor will track responses in addition to requests, and they will be available to middleware and file output.")
flag.StringVar(&Settings.inputRAWEngine, "input-raw-engine", "libpcap", "Intercept traffic using `libpcap` (default), and `raw_socket`")
flag.StringVar(&Settings.inputRAWRealIPHeader, "input-raw-realip-header", "", "If not blank, injects header with given name and real IP value to the request payload. Usually this header should be named: X-Real-IP")
flag.StringVar(&Settings.middleware, "middleware", "", "Used for modifying traffic using external command")
flag.Var(&Settings.inputHTTP, "input-http", "Read requests from HTTP, should be explicitly sent from your application:\n\t# Listen for http on 9000\n\tgor --input-http :9000 --output-http staging.com")
flag.Var(&Settings.outputHTTP, "output-http", "Forwards incoming requests to given http address.\n\t# Redirect all incoming requests to staging.com address \n\tgor --input-raw :80 --output-http http://staging.com")
flag.IntVar(&Settings.outputHTTPConfig.BufferSize, "output-http-response-buffer", 0, "HTTP response buffer size, all data after this size will be discarded.")
flag.IntVar(&Settings.outputHTTPConfig.workers, "output-http-workers", 0, "Gor uses dynamic worker scaling by default. Enter a number to run a set number of workers.")
flag.IntVar(&Settings.outputHTTPConfig.redirectLimit, "output-http-redirects", 0, "Enable how often redirects should be followed.")
flag.DurationVar(&Settings.outputHTTPConfig.Timeout, "output-http-timeout", 0, "Specify HTTP request/response timeout. By default 5s. Example: --output-http-timeout 30s")
flag.BoolVar(&Settings.outputHTTPConfig.stats, "output-http-stats", false, "Report http output queue stats to console every 5 seconds.")
flag.BoolVar(&Settings.outputHTTPConfig.OriginalHost, "http-original-host", false, "Normally gor replaces the Host http header with the host supplied with --output-http. This option disables that behavior, preserving the original Host header.")
flag.BoolVar(&Settings.outputHTTPConfig.Debug, "output-http-debug", false, "Enables http debug output.")
flag.StringVar(&Settings.outputHTTPConfig.elasticSearch, "output-http-elasticsearch", "", "Send request and response stats to ElasticSearch:\n\tgor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'")
flag.Var(&Settings.modifierConfig.headers, "http-set-header", "Inject additional headers to http reqest:\n\tgor --input-raw :8080 --output-http staging.com --http-set-header 'User-Agent: Gor'")
flag.Var(&Settings.modifierConfig.headers, "output-http-header", "WARNING: `--output-http-header` DEPRECATED, use `--http-set-header` instead")
flag.Var(&Settings.modifierConfig.params, "http-set-param", "Set request url param, if param already exists it will be overwritten:\n\tgor --input-raw :8080 --output-http staging.com --http-set-param api_key=1")
flag.Var(&Settings.modifierConfig.methods, "http-allow-method", "Whitelist of HTTP methods to replay. Anything else will be dropped:\n\tgor --input-raw :8080 --output-http staging.com --http-allow-method GET --http-allow-method OPTIONS")
flag.Var(&Settings.modifierConfig.methods, "output-http-method", "WARNING: `--output-http-method` DEPRECATED, use `--http-allow-method` instead")
flag.Var(&Settings.modifierConfig.urlRegexp, "http-allow-url", "A regexp to match requests against. Filter get matched against full url with domain. Anything else will be dropped:\n\t gor --input-raw :8080 --output-http staging.com --http-allow-url ^www.")
flag.Var(&Settings.modifierConfig.urlRegexp, "output-http-url-regexp", "WARNING: `--output-http-url-regexp` DEPRECATED, use `--http-allow-url` instead")
flag.Var(&Settings.modifierConfig.urlNegativeRegexp, "http-disallow-url", "A regexp to match requests against. Filter get matched against full url with domain. Anything else will be forwarded:\n\t gor --input-raw :8080 --output-http staging.com --http-disallow-url ^www.")
flag.Var(&Settings.modifierConfig.urlRewrite, "http-rewrite-url", "Rewrite the request url based on a mapping:\n\tgor --input-raw :8080 --output-http staging.com --http-rewrite-url /v1/user/([^\\/]+)/ping:/v2/user/$1/ping")
flag.Var(&Settings.modifierConfig.urlRewrite, "output-http-rewrite-url", "WARNING: `--output-http-rewrite-url` DEPRECATED, use `--http-rewrite-url` instead")
flag.Var(&Settings.modifierConfig.headerFilters, "http-allow-header", "A regexp to match a specific header against. Requests with non-matching headers will be dropped:\n\t gor --input-raw :8080 --output-http staging.com --http-allow-header api-version:^v1")
flag.Var(&Settings.modifierConfig.headerFilters, "output-http-header-filter", "WARNING: `--output-http-header-filter` DEPRECATED, use `--http-allow-header` instead")
flag.Var(&Settings.modifierConfig.headerFilters, "http-disallow-header", "A regexp to match a specific header against. Requests with matching headers will be dropped:\n\t gor --input-raw :8080 --output-http staging.com --http-disallow-header \"User-Agent: Replayed by Gor\"")
flag.Var(&Settings.modifierConfig.headerHashFilters, "http-header-limiter", "Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific header:\n\t gor --input-raw :8080 --output-http staging.com --http-header-imiter user-id:25%")
flag.Var(&Settings.modifierConfig.headerHashFilters, "output-http-header-hash-filter", "WARNING: `output-http-header-hash-filter` DEPRECATED, use `--http-header-hash-limiter` instead")
flag.Var(&Settings.modifierConfig.paramHashFilters, "http-param-limiter", "Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific GET param:\n\t gor --input-raw :8080 --output-http staging.com --http-param-limiter user_id:25%")
}
var previousDebugTime int64
var debugMutex sync.Mutex
// Debug gets called only if --verbose flag specified
func Debug(args ...interface{}) {
if Settings.verbose {
debugMutex.Lock()
now := time.Now()
diff := float64(now.UnixNano()-previousDebugTime) / 1000000
previousDebugTime = now.UnixNano()
debugMutex.Unlock()
fmt.Printf("[DEBUG][PID %d][%d][%fms] ", os.Getpid(), now.UnixNano(), diff)
fmt.Println(args...)
}
}
+77
View File
@@ -0,0 +1,77 @@
package main
import (
"crypto/rand"
"encoding/base64"
"time"
)
// TestInput used for testing purpose, it allows emitting requests on demand
type TestInput struct {
data chan []byte
}
// NewTestInput constructor for TestInput
func NewTestInput() (i *TestInput) {
i = new(TestInput)
i.data = make(chan []byte, 100)
return
}
func (i *TestInput) Read(data []byte) (int, error) {
buf := <-i.data
header := payloadHeader(RequestPayload, uuid(), time.Now().UnixNano())
copy(data[0:len(header)], header)
copy(data[len(header):], buf)
return len(buf) + len(header), nil
}
// EmitGET emits GET request without headers
func (i *TestInput) EmitGET() {
i.data <- []byte("GET / HTTP/1.1\r\n\r\n")
}
// EmitPOST emits POST request with Content-Length
func (i *TestInput) EmitPOST() {
i.data <- []byte("POST /pub/WWW/ HTTP/1.1\r\nContent-Length: 7\r\nHost: www.w3.org\r\n\r\na=1&b=2")
}
// EmitChunkedPOST emits POST request with `Transfer-Encoding: chunked` and chunked body
func (i *TestInput) EmitChunkedPOST() {
i.data <- []byte("POST /pub/WWW/ HTTP/1.1\r\nHost: www.w3.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
}
// EmitLargePOST emits POST request with large payload (5mb)
func (i *TestInput) EmitLargePOST() {
size := 5 * 1024 * 1024 // 5 MB
rb := make([]byte, size)
rand.Read(rb)
rs := base64.URLEncoding.EncodeToString(rb)
i.data <- []byte("POST / HTTP/1.1\nHost: www.w3.org\nContent-Length:5242880\r\n\r\n" + rs)
Debug("Sent large POST")
}
// EmitSizedPOST emit a POST with a payload set to a supplied size
func (i *TestInput) EmitSizedPOST(payloadSize int) {
rb := make([]byte, payloadSize)
rand.Read(rb)
rs := base64.URLEncoding.EncodeToString(rb)
i.data <- []byte("POST / HTTP/1.1\nHost: www.w3.org\nContent-Length:5242880\r\n\r\n" + rs)
Debug("Sent large POST")
}
// EmitOPTIONS emits OPTIONS request, similar to GET
func (i *TestInput) EmitOPTIONS() {
i.data <- []byte("OPTIONS / HTTP/1.1\nHost: www.w3.org\r\n\r\n")
}
func (i *TestInput) String() string {
return "Test Input"
}
+26
View File
@@ -0,0 +1,26 @@
package main
type writeCallback func(data []byte)
// TestOutput used in testing to intercept any output into callback
type TestOutput struct {
cb writeCallback
}
// NewTestOutput constructor for TestOutput, accepts callback which get called on each incoming Write
func NewTestOutput(cb writeCallback) (i *TestOutput) {
i = new(TestOutput)
i.cb = cb
return
}
func (i *TestOutput) Write(data []byte) (int, error) {
i.cb(data)
return len(data), nil
}
func (i *TestOutput) String() string {
return "Test Input"
}