Compare commits

...
Author SHA1 Message Date
Leonid Bugaev 3e143d39dc Revert "Handle Transfer-Encoding: chunked" 2015-06-26 10:00:32 +05:00
Leonid Bugaev 6fe7ba8e86 Merge pull request #157 from buger/encoding-chunked
Handle Transfer-Encoding: chunked
2015-06-26 09:19:02 +05:00
Leonid Bugaev 534e9b29fa Merge branch 'master' into encoding-chunked
Conflicts:
	output_http.go
2015-06-26 09:12:29 +05:00
Leonid Bugaev 390f75fd3c Handle Transfer-Encoding: chunked 2015-06-26 09:08:16 +05:00
Leonid Bugaev a059648008 Merge pull request #150 from makern/64bit-align
Fix alignment crash on i386
2015-03-23 13:33:39 +05:00
Markus Kern b2e1eea6bf Fix alignment crash on i386 2015-03-22 21:22:44 +00:00
Leonid Bugaev 92ba321569 Merge pull request #148 from MartinNowak/pow2
simplify power of 2 test
2015-03-19 17:37:53 +05:00
Martin Nowak 2b497e6177 simplify power of 2 test 2015-03-19 11:08:49 +01:00
Leonid Bugaev 09f9cac6fb Merge pull request #145 from tarfu/master
Introduce possibility to enable redirects.
2015-03-15 08:40:29 +05:00
Tobias Breitwieser d05bc0d5e9 Introduce possibility to enable redirects. 2015-03-14 16:47:04 +01:00
Leonid Bugaev 1c0182188c Merge pull request #143 from falzm/fix-socket-perm-crash
Prevent crash if no permissions on port
2015-03-06 16:13:12 +05:00
Marc Falzon cef8b75496 Prevent crash if no permissions on port
This fix prevents Gor from crashing at startup if run without proper
permissions for listening on specified port.
2015-03-05 21:58:52 +01:00
Leonid Bugaev e9fd5eb273 Merge pull request #141 from wmoss/master
Properly handle errors on input sockets
2015-03-03 12:00:55 +05:00
Will Moss 85fbfff308 Properly handle errors on input sockets
`buf` is of length 0, so you never make it into
the loop to handle the errors. This means you end
up in a tight loop calling `ReadBytes` and always
getting back `io.EOF` and 0 bytes.
2015-03-02 21:52:40 -08:00
Leonid Bugaev a7aaf7df65 Merge pull request #140 from wmoss/master
Properly return from errors from ReadRequest
2015-02-28 09:57:33 +05:00
Will Moss c1c87ed4e2 Properly return from errors from ReadRequest 2015-02-26 16:07:52 -08:00
Leonid Bugaev 072f332956 Merge pull request #138 from wmoss/master
Make TCPOutput more robust to failures
2015-02-23 09:05:18 +05:00
Will Moss 7c68920a3b Make TCPOutput more robust to failures
This should allow for running a gor listener on a
production box and bringing up and down the replay
node without having to go restart the listener.
2015-02-22 15:49:35 -08:00
Leonid Bugaev b6351a8525 Fix travis test runner 2015-02-22 14:19:48 +05:00
Leonid Bugaev f557d412c4 Merge pull request #133 from buger/fix-post-requests
Fix POST requests
2015-01-11 20:04:23 +05:00
Leonid Bugaev ed5c2215f5 Fix POST requests 2015-01-06 17:50:36 +05:00
Leonid Bugaev ffe1646d8e Merge pull request #131 from chmac/patch-1
Minor typo correction
2014-12-07 20:37:26 +05:00
Callum Macdonald ae293320ad Minor typo correction 2014-12-07 16:20:49 +01:00
Leonid Bugaev eb7885cd56 Patch from @jcw9930
See https://github.com/buger/gor/pull/127#issuecomment-64538157
2014-11-26 16:30:17 +05:00
Leonid Bugaev b6977df90d Merge pull request #127 from jcw9930/url_regexp_rewrite
add regular expression matching to --output-http-rewrite-url
2014-10-29 15:24:42 +05:00
Jan Willies 15417894a9 add test that explicitly checks if rewrites.Rewrite(url) == /v2/user/joe/ping 2014-10-28 18:42:38 +01:00
Jan Willies 7f49314ec7 allow matched parts in -output-http-rewrite-url 2014-10-28 18:12:21 +01:00
Jan Willies 80572f5f18 add regular expression matching to --output-http-rewrite-url 2014-10-28 14:52:55 +01:00
13 changed files with 145 additions and 68 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
language: go
go: 1.3.3
script: sudo -E bash -c "source /etc/profile && gvm use go1.3.3 && export GOPATH=$HOME/gopath:$GOPATH && go get && GORACE='halt_on_error=1' go test -race -v"
script: sudo -E bash -c "source /etc/profile && eval '$(gimme 1.4)' && export GOPATH=$HOME/gopath:$GOPATH && go get && GORACE='halt_on_error=1' go test -race -v"
+37 -40
View File
@@ -34,7 +34,7 @@ It's recommended to use separate server for replaying traffic, but if you have e
sudo gor --input-raw :80 --output-http "http://staging.com"
```
### Guarante of replay and HTTP input
### Guarantee of replay and HTTP input
Due to how traffic interception works, there is chance of missing requests. If you want guarantee that requests will be replayed you can use http input, but it will require changes in your app as well.
```
@@ -43,6 +43,13 @@ sudo gor --input-http :28019 --output-http "http://staging.com"
Then in your application you should send copy (e.g. like reverse proxy) all incoming requests to Gor http input.
### Following redirects
If you have a scenario where following redirects is usefull you can do it like with:
```
gor --input-tcp replay.local:28020 --output-http http://staging.com --output-http-redirects 10
```
The given example will follow up to 10 redirects per request.
## Advanced use
@@ -186,59 +193,49 @@ https://github.com/buger/gor/releases
`gor -h` output:
```
-cpuprofile="": write cpu profile to file
-memprofile="": write memory profile to this file
-input-dummy=[]: Used for testing outputs. Emits 'Get /' request every 1s
-input-file=[]: Read requests from file:
gor --input-file ./requests.gor --output-http staging.com
gor --input-file ./requests.gor --output-http staging.com
-input-http=[]: Read requests from HTTP, should be explicitly sent from your application:
# Listen for http on 9000
gor --input-http :9000 --output-http staging.com
-input-raw=[]: Capture traffic from given port (use RAW sockets and require *sudo* access):
# Capture traffic from 8080 port
gor --input-raw :8080 --output-http staging.com
# Capture traffic from 8080 port
gor --input-raw :8080 --output-http staging.com
-input-tcp=[]: Used for internal communication between Gor instances. Example:
# Receive requests from other Gor instances on 28020 port, and redirect output to staging
gor --input-tcp :28020 --output-http staging.com
# Receive requests from other Gor instances on 28020 port, and redirect output to staging
gor --input-tcp :28020 --output-http staging.com
-memprofile="": write memory profile to this file
-output-dummy=[]: Used for testing inputs. Just prints data coming from inputs.
-output-file=[]: Write incoming requests to file:
gor --input-raw :80 --output-file ./requests.gor
gor --input-raw :80 --output-file ./requests.gor
-output-http=[]: Forwards incoming requests to given http address.
# Redirect all incoming requests to staging.com address
gor --input-raw :80 --output-http http://staging.com
# Redirect all incoming requests to staging.com address
gor --input-raw :80 --output-http http://staging.com
-output-http-elasticsearch="": Send request and response stats to ElasticSearch:
gor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'
gor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'
-output-http-header=[]: Inject additional headers to http reqest:
gor --input-raw :8080 --output-http staging.com --output-http-header 'User-Agent: Gor'
gor --input-raw :8080 --output-http staging.com --output-http-header 'User-Agent: Gor'
-output-http-header-filter=[]: A regexp to match a specific header against. Requests with non-matching headers will be dropped:
gor --input-raw :8080 --output-http staging.com --output-http-header-filter api-version:^v1
gor --input-raw :8080 --output-http staging.com --output-http-header-filter api-version:^v1
-output-http-header-hash-filter=[]: Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific header. The fraction must have a denominator that is a power of two:
gor --input-raw :8080 --output-http staging.com --output-http-header-hash-filter user-id:1/4
gor --input-raw :8080 --output-http staging.com --output-http-header-hash-filter user-id:1/4
-output-http-method=[]: Whitelist of HTTP methods to replay. Anything else will be dropped:
gor --input-raw :8080 --output-http staging.com --output-http-method GET --output-http-method OPTIONS
-output-http-redirects=0: Enable how often redirects should be followed.
-output-http-rewrite-url=[]: Rewrite the requst url based on a mapping:
gor --input-raw :8080 --output-http staging.com --output-http-rewrite-url /xml_test/interface.php:/api/service.do
-output-http-stats=false: Report http output queue stats to console every 5 seconds.
-output-http-url-regexp=: A regexp to match requests against. Anything else will be dropped:
gor --input-raw :8080 --output-http staging.com --output-http-url-regexp ^www.
-output-http-workers=-1: Number of http output workers desired. Use default -1 for dynamic worker scaling. Gor will add http workers if its work queue starts getting too full and kill them .
-output-http-stats=false: If set to `true` it gives out queuing stats for the HTTP output every 5 seconds in the form latest,mean,max,count,count/second.
gor --input-raw :8080 --output-http staging.com --output-http-url-regexp ^www.
-output-http-workers=-1: Gor uses dynamic worker scaling by default. Enter a number to run a set number of workers.
-output-tcp=[]: Used for internal communication between Gor instances. Example:
# Listen for requests on 80 port and forward them to other Gor instance on 28020 port
gor --input-raw :80 --output-tcp replay.local:28020
-output-tcp-stats=false: If set to `true` it gives out queuing stats for the TCP output every 5 seconds in the form latest,mean,max,count,count/second.
# Listen for requests on 80 port and forward them to other Gor instance on 28020 port
gor --input-raw :80 --output-tcp replay.local:28020
-output-tcp-stats=false: Report TCP output queue stats to console every 5 seconds.
-split-output=false: By default each output gets same traffic. If set to `true` it splits traffic equally among all outputs.
-output-http-rewrite-url=[]: Rewrites the url in the request based on a mapping
gor --input-raw :8080 --output-http staging.com --output-http-rewrite-url /xml_test/interface.php:/api/service.do
-stats=false: Turn on queue stats output
-verbose=false: Turn on verbose/debug output
```
## Building from source
+6 -5
View File
@@ -62,6 +62,12 @@ func (i *TCPInput) handleConnection(conn net.Conn) {
for {
buf, err := reader.ReadBytes('¶')
if err == io.EOF {
return
} else if err != nil {
log.Println("Unexpected error in input tcp connection", err)
return
}
buf_len := len(buf)
if buf_len > 0 {
new_buf_len := len(buf) - 2
@@ -69,11 +75,6 @@ func (i *TCPInput) handleConnection(conn net.Conn) {
new_buf := make([]byte, new_buf_len)
copy(new_buf, buf[:new_buf_len])
i.data <- new_buf
if err != nil {
if err != io.EOF {
log.Printf("error: %s\n", err)
}
}
}
}
}
+25 -5
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
@@ -19,8 +20,8 @@ func (e *RedirectNotAllowed) Error() string {
}
// customCheckRedirect disables redirects https://github.com/buger/gor/pull/15
func customCheckRedirect(req *http.Request, via []*http.Request) error {
if len(via) >= 0 {
func (o *HTTPOutput) customCheckRedirect(req *http.Request, via []*http.Request) error {
if len(via) >= o.redirectLimit {
return new(RedirectNotAllowed)
}
return nil
@@ -33,17 +34,34 @@ func ParseRequest(data []byte) (request *http.Request, err error) {
request, err = http.ReadRequest(reader)
if err != nil {
return
}
if request.Method == "POST" {
body, _ := ioutil.ReadAll(reader)
bodyBuf := bytes.NewBuffer(body)
request.Body = ioutil.NopCloser(bodyBuf)
request.ContentLength = int64(bodyBuf.Len())
}
return
}
const InitialDynamicWorkers = 10
type HTTPOutput struct {
// Keep this as first element of struct because it guarantees 64bit
// alignment. atomic.* functions crash on 32bit machines if operand is not
// aligned at 64bit. See https://github.com/golang/go/issues/599
activeWorkers int64
address string
limit int
queue chan []byte
activeWorkers int64
redirectLimit int
needWorker chan int
urlRegexp HTTPUrlRegexp
@@ -59,7 +77,7 @@ type HTTPOutput struct {
queueStats *GorStat
}
func NewHTTPOutput(address string, headers HTTPHeaders, methods HTTPMethods, urlRegexp HTTPUrlRegexp, headerFilters HTTPHeaderFilters, headerHashFilters HTTPHeaderHashFilters, elasticSearchAddr string, outputHTTPUrlRewrite UrlRewriteMap) io.Writer {
func NewHTTPOutput(address string, headers HTTPHeaders, methods HTTPMethods, urlRegexp HTTPUrlRegexp, headerFilters HTTPHeaderFilters, headerHashFilters HTTPHeaderHashFilters, elasticSearchAddr string, outputHTTPUrlRewrite UrlRewriteMap, outputHTTPRedirects int) io.Writer {
o := new(HTTPOutput)
@@ -71,6 +89,8 @@ func NewHTTPOutput(address string, headers HTTPHeaders, methods HTTPMethods, url
o.headers = headers
o.methods = methods
o.redirectLimit = Settings.outputHTTPRedirects
o.urlRegexp = urlRegexp
o.headerFilters = headerFilters
o.headerHashFilters = headerHashFilters
@@ -116,7 +136,7 @@ func (o *HTTPOutput) WorkerMaster() {
func (o *HTTPOutput) Worker() {
client := &http.Client{
CheckRedirect: customCheckRedirect,
CheckRedirect: o.customCheckRedirect,
}
death_count := 0
+16 -3
View File
@@ -2,8 +2,11 @@ package main
import (
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
_ "strings"
"sync"
"testing"
"time"
@@ -11,7 +14,7 @@ import (
func startHTTP(cb func(*http.Request)) net.Listener {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
go cb(r)
cb(r)
})
listener, _ := net.Listen("tcp", ":0")
@@ -60,10 +63,20 @@ func TestHTTPOutput(t *testing.T) {
t.Error("Wrong method")
}
if req.Method == "POST" {
defer req.Body.Close()
body, _ := ioutil.ReadAll(req.Body)
if string(body) != "a=1&b=2\r\n\r\n" {
buf, _ := httputil.DumpRequest(req, true)
t.Error("Wrong POST body:", string(buf))
}
}
wg.Done()
})
output := NewHTTPOutput(listener.Addr().String(), headers, methods, HTTPUrlRegexp{}, HTTPHeaderFilters{}, HTTPHeaderHashFilters{}, "", UrlRewriteMap{})
output := NewHTTPOutput(listener.Addr().String(), headers, methods, HTTPUrlRegexp{}, HTTPHeaderFilters{}, HTTPHeaderHashFilters{}, "", UrlRewriteMap{}, 0)
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
@@ -96,7 +109,7 @@ func BenchmarkHTTPOutput(b *testing.B) {
wg.Done()
})
output := NewHTTPOutput(listener.Addr().String(), headers, methods, HTTPUrlRegexp{}, HTTPHeaderFilters{}, HTTPHeaderHashFilters{}, "", UrlRewriteMap{})
output := NewHTTPOutput(listener.Addr().String(), headers, methods, HTTPUrlRegexp{}, HTTPHeaderFilters{}, HTTPHeaderHashFilters{}, "", UrlRewriteMap{}, 0)
Plugins.Inputs = []io.Reader{input}
Plugins.Outputs = []io.Writer{output}
+12 -2
View File
@@ -5,6 +5,7 @@ import (
"io"
"log"
"net"
"time"
)
type TCPOutput struct {
@@ -32,11 +33,20 @@ func NewTCPOutput(address string) io.Writer {
}
func (o *TCPOutput) worker() {
conn, _ := o.connect(o.address)
conn, err := o.connect(o.address)
for ; err != nil; conn, err = o.connect(o.address) {
time.Sleep(2 * time.Second)
}
defer conn.Close()
for {
conn.Write(<-o.buf)
_, err := conn.Write(<-o.buf)
if err != nil {
log.Println("Worker failed on write, exitings and starting new worker")
go o.worker()
break
}
}
}
+1 -1
View File
@@ -96,6 +96,6 @@ func InitPlugins() {
}
for _, options := range Settings.outputHTTP {
registerPlugin(NewHTTPOutput, options, Settings.outputHTTPHeaders, Settings.outputHTTPMethods, Settings.outputHTTPUrlRegexp, Settings.outputHTTPHeaderFilters, Settings.outputHTTPHeaderHashFilters, Settings.outputHTTPElasticSearch, Settings.outputHTTPUrlRewrite)
registerPlugin(NewHTTPOutput, options, Settings.outputHTTPHeaders, Settings.outputHTTPMethods, Settings.outputHTTPUrlRegexp, Settings.outputHTTPHeaderFilters, Settings.outputHTTPHeaderHashFilters, Settings.outputHTTPElasticSearch, Settings.outputHTTPUrlRewrite, Settings.outputHTTPRedirects)
}
}
+2 -1
View File
@@ -61,12 +61,13 @@ func (t *Listener) listen() {
func (t *Listener) readRAWSocket() {
conn, e := net.ListenPacket("ip4:tcp", t.addr)
defer conn.Close()
if e != nil {
log.Fatal(e)
}
defer conn.Close()
buf := make([]byte, 4096*2)
for {
+2
View File
@@ -40,6 +40,7 @@ type AppSettings struct {
outputHTTPElasticSearch string
outputHTTPWorkers int
outputHTTPStats bool
outputHTTPRedirects int
}
var Settings AppSettings = AppSettings{}
@@ -83,6 +84,7 @@ func init() {
flag.StringVar(&Settings.outputHTTPElasticSearch, "output-http-elasticsearch", "", "Send request and response stats to ElasticSearch:\n\tgor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'")
flag.Var(&Settings.outputHTTPUrlRewrite, "output-http-rewrite-url", "Rewrite the requst url based on a mapping:\n\tgor --input-raw :8080 --output-http staging.com --output-http-rewrite-url /xml_test/interface.php:/api/service.do")
flag.IntVar(&Settings.outputHTTPRedirects, "output-http-redirects", 0, "Enable how often redirects should be followed.")
}
func Debug(args ...interface{}) {
+2 -4
View File
@@ -39,10 +39,8 @@ func (h *HTTPHeaderHashFilters) Set(value string) error {
panic("need positive numerators and denominators, with the former less than the latter.")
}
for test := den; test != 1; test /= 2 {
if test%2 == 1 {
return errors.New("must have a denominator which is a power of two.")
}
if den & (den - 1) != 0 {
return errors.New("must have a denominator which is a power of two.")
}
var f headerHashFilter
+5
View File
@@ -23,6 +23,11 @@ func TestHTTPHeaderHashFilters(t *testing.T) {
t.Error("Should error on HeaderIrrelevant:1/3")
}
err = filters.Set("Pow2Denom:1/31")
if err == nil {
t.Error("Should error on Pow2Denom:1/31")
}
req := http.Request{}
req.Header = make(map[string][]string)
req.Header.Add("Header1", "test3414")
+9 -4
View File
@@ -3,11 +3,12 @@ package main
import (
"errors"
"fmt"
"regexp"
"strings"
)
type urlRewrite struct {
src string
src *regexp.Regexp
target string
}
@@ -22,14 +23,18 @@ func (r *UrlRewriteMap) Set(value string) error {
if len(valArr) < 2 {
return errors.New("need both src and target, colon-delimited (ex. /a:/b).")
}
*r = append(*r, urlRewrite{src: valArr[0], target: valArr[1]})
regexp, err := regexp.Compile(valArr[0])
if err != nil {
return err
}
*r = append(*r, urlRewrite{src: regexp, target: valArr[1]})
return nil
}
func (r *UrlRewriteMap) Rewrite(path string) string {
for _, f := range *r {
if f.src == path {
return f.target
if f.src.MatchString(path) {
path = f.src.ReplaceAllString(path, f.target)
}
}
return path
+27 -2
View File
@@ -4,7 +4,7 @@ import (
"testing"
)
func TestUrlRewriteMap(t *testing.T) {
func TestUrlRewriteMap_1(t *testing.T) {
var url string
rewrites := UrlRewriteMap{}
@@ -15,7 +15,6 @@ func TestUrlRewriteMap(t *testing.T) {
}
url = "/abc"
if rewrites.Rewrite(url) == url {
t.Error("Request url should have been rewritten, wasn't")
}
@@ -25,3 +24,29 @@ func TestUrlRewriteMap(t *testing.T) {
t.Error("Request url should not have been rewritten, was")
}
}
func TestUrlRewriteMap_2(t *testing.T) {
var url string
rewrites := UrlRewriteMap{}
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")
}
url = "/v1/user/joe/ping"
if rewrites.Rewrite(url) == url {
t.Error("Request url should have been rewritten, wasn't")
}
url = "/v1/user/joe/ping"
if rewrites.Rewrite(url) != "/v2/user/joe/ping" {
t.Error("Request url should have been rewritten, wasn't")
}
url = "/v1/user/ping"
if rewrites.Rewrite(url) != url {
t.Error("Request url should not have been rewritten, was")
}
}