From fd5118b76da2fe83008dc510dfa1319a03bab104 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Mon, 19 Dec 2016 14:45:18 +0300 Subject: [PATCH 01/19] fix test: should not add port to host for https --- http_client_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http_client_test.go b/http_client_test.go index 00f9db3..917c7c9 100644 --- a/http_client_test.go +++ b/http_client_test.go @@ -24,8 +24,8 @@ func TestHTTPClientURLPort(t *testing.T) { } c2 := NewHTTPClient("https://example.com", &HTTPClientConfig{}) - if c2.baseURL != "https://example.com:443" { - t.Error("Sould add 443 port for https:", c2.baseURL) + if c2.baseURL != "https://example.com" { + t.Error("Sould not add 443 port for https:", c2.baseURL) } c3 := NewHTTPClient("https://example.com:1", &HTTPClientConfig{}) From 1f19e550bbf4e59c9780fb1a66f70173e74dbb1c Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Thu, 22 Dec 2016 09:59:48 +0100 Subject: [PATCH 02/19] Create kafka input --- input_kafka.go | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ kafka.go | 42 ++++++++++++++++++++++ output_kafka.go | 21 ++--------- plugins.go | 4 +++ settings.go | 8 +++-- 5 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 input_kafka.go create mode 100644 kafka.go diff --git a/input_kafka.go b/input_kafka.go new file mode 100644 index 0000000..5420340 --- /dev/null +++ b/input_kafka.go @@ -0,0 +1,93 @@ +package main + +import ( + "encoding/json" + "log" + "time" + + "github.com/Shopify/sarama" +) + +// KafkaInput should make consumer client. +type KafkaInput struct { + config *KafkaConfig + consumers []sarama.PartitionConsumer + messages chan *sarama.ConsumerMessage +} + +// NewKafkaInput constructor for KafkaInput +func NewKafkaInput(address string, config *KafkaConfig) *KafkaInput { + c := sarama.NewConfig() + // c.Consumer. + + con, err := sarama.NewConsumer([]string{config.host}, c) + if err != nil { + log.Fatalln("Failed to start Sarama(Kafka) consumer:", err) + } + + partitions, err := con.Partitions(config.topic) + if err != nil { + log.Fatalln("Failed to collect Sarama(Kafka) partitions:", err) + } + + i := &KafkaInput{ + config: config, + consumers: make([]sarama.PartitionConsumer, len(partitions)), + messages: make(chan *sarama.ConsumerMessage, 256), + } + + for index, partition := range partitions { + consumer, err := con.ConsumePartition(config.topic, partition, sarama.OffsetNewest) + if err != nil { + log.Fatalln("Failed to start Sarama(Kafka) partition consumer:", err) + } + + go func(consumer sarama.PartitionConsumer) { + defer consumer.Close() + + for message := range consumer.Messages() { + i.messages <- message + } + }(consumer) + + if Settings.verbose { + // Start infinite loop for tracking errors for kafka producer. + go i.ErrorHandler(consumer) + } + + i.consumers[index] = consumer + } + + return i +} + +// ErrorHandler should receive errors +func (i *KafkaInput) ErrorHandler(consumer sarama.PartitionConsumer) { + for err := range consumer.Errors() { + log.Println("Failed to read access log entry:", err) + } +} + +func (i *KafkaInput) Read(data []byte) (int, error) { + message := <-i.messages + + var kafkaMessage KafkaMessage + json.Unmarshal(message.Value, &kafkaMessage) + + buf, err := kafkaMessage.Dump() + if err != nil { + log.Println("Failed to decode access log entry:", err) + return 0, err + } + + header := payloadHeader(RequestPayload, uuid(), time.Now().UnixNano(), -1) + + copy(data[0:len(header)], header) + copy(data[len(header):], buf) + + return len(buf) + len(header), nil +} + +func (i *KafkaInput) String() string { + return "Kafka Input: " + i.config.host + "/" + i.config.topic +} diff --git a/kafka.go b/kafka.go new file mode 100644 index 0000000..ef0849d --- /dev/null +++ b/kafka.go @@ -0,0 +1,42 @@ +package main + +import ( + "bytes" + "fmt" + + "github.com/buger/gor/proto" +) + +// KafkaConfig should contains required information to +// build producers. +type KafkaConfig struct { + host string + topic string +} + +// KafkaMessage should contains catched request information that should be +// passed as Json to Apache Kafka. +type KafkaMessage struct { + ReqURL string `json:"Req_URL"` + ReqMethod string `json:"Req_Method"` + ReqBody string `json:"Req_Body,omitempty"` + ReqHeaders map[string]string `json:"Req_Headers,omitempty"` +} + +// Dump returns the given request in its HTTP/1.x wire +// representation. +func (m KafkaMessage) Dump() ([]byte, error) { + var b bytes.Buffer + + b.WriteString(fmt.Sprintf("%s %s HTTP/1.1", m.ReqMethod, m.ReqURL)) + b.Write(proto.CLRF) + for key, value := range m.ReqHeaders { + b.WriteString(fmt.Sprintf("%s: %s", key, value)) + b.Write(proto.CLRF) + } + + b.Write(proto.CLRF) + b.WriteString(m.ReqBody) + + return b.Bytes(), nil +} diff --git a/output_kafka.go b/output_kafka.go index 4ebfc4c..77c4a35 100644 --- a/output_kafka.go +++ b/output_kafka.go @@ -2,20 +2,14 @@ package main import ( "encoding/json" - "github.com/Shopify/sarama" - "github.com/buger/gor/proto" "io" "log" "strings" "time" -) -// KafkaConfig should contains required information to -// build producers. -type KafkaConfig struct { - host string - topic string -} + "github.com/Shopify/sarama" + "github.com/buger/gor/proto" +) // KafkaOutput should make producer client. type KafkaOutput struct { @@ -23,15 +17,6 @@ type KafkaOutput struct { producer sarama.AsyncProducer } -// KafkaMessage should contains catched request information that should be -// passed as Json to Apache Kafka. -type KafkaMessage struct { - ReqURL string `json:"Req_URL"` - ReqMethod string `json:"Req_Method"` - ReqBody string `json:"Req_Body,omitempty"` - ReqHeaders map[string]string `json:"Req_Headers,omitempty"` -} - // KafkaOutputFrequency in milliseconds const KafkaOutputFrequency = 500 diff --git a/plugins.go b/plugins.go index 6ee1516..a5a9113 100644 --- a/plugins.go +++ b/plugins.go @@ -146,4 +146,8 @@ func InitPlugins() { if Settings.outputKafkaConfig.host != "" && Settings.outputKafkaConfig.topic != "" { registerPlugin(NewKafkaOutput, "", &Settings.outputKafkaConfig) } + + if Settings.inputKafkaConfig.host != "" && Settings.inputKafkaConfig.topic != "" { + registerPlugin(NewKafkaInput, "", &Settings.inputKafkaConfig) + } } diff --git a/settings.go b/settings.go index ac62f7c..274ffff 100644 --- a/settings.go +++ b/settings.go @@ -59,6 +59,7 @@ type AppSettings struct { outputHTTPConfig HTTPOutputConfig modifierConfig HTTPModifierConfig + inputKafkaConfig KafkaConfig outputKafkaConfig KafkaConfig } @@ -128,8 +129,11 @@ func init() { 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.StringVar(&Settings.outputKafkaConfig.host, "output-kafka-host", "", "Send request and response stats to Kafka:\n\tgor --input-raw :8080 --output-kafka-host '192.168.0.1:9092,192.168.0.2:9092'") - flag.StringVar(&Settings.outputKafkaConfig.topic, "output-kafka-topic", "", "Send request and response stats to Kafka:\n\tgor --input-raw :8080 --output-kafka-topic 'kafka-log'") + flag.StringVar(&Settings.outputKafkaConfig.host, "output-kafka-host", "", "Read request and response stats from Kafka:\n\tgor --input-raw :8080 --output-kafka-host '192.168.0.1:9092,192.168.0.2:9092'") + flag.StringVar(&Settings.outputKafkaConfig.topic, "output-kafka-topic", "", "Read request and response stats from Kafka:\n\tgor --input-raw :8080 --output-kafka-topic 'kafka-log'") + + flag.StringVar(&Settings.inputKafkaConfig.host, "input-kafka-host", "", "Send request and response stats to Kafka:\n\tgor --output-stdout --input-kafka-host '192.168.0.1:9092,192.168.0.2:9092'") + flag.StringVar(&Settings.inputKafkaConfig.topic, "input-kafka-topic", "", "Send request and response stats to Kafka:\n\tgor --output-stdout --input-kafka-topic 'kafka-log'") 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") From 5b5c3f33f4e5781bb0e09edef9e144ec5d726ebb Mon Sep 17 00:00:00 2001 From: Sergey Mostovoy Date: Thu, 22 Dec 2016 11:25:29 +0200 Subject: [PATCH 03/19] Filter response if request is filtered #388 --- Makefile | 4 ++-- emitter.go | 52 ++++++++++++++++++++++++++++++++++++------------- emitter_test.go | 46 +++++++++++++++++++++++++++++++++++++++++++ test_input.go | 17 +++++++++++++--- 4 files changed, 100 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index fb1e399..614ff1e 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ -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_null.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 = 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_null.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 output_kafka.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 +RUN = docker run -v `pwd`:$(SOURCE_PATH) -p 0.0.0.0:$(PORT):$(PORT) -t -i gor:go BENCHMARK = BenchmarkRAWInput TEST = TestRawListenerBench VERSION = DEV-$(shell date +%s) diff --git a/emitter.go b/emitter.go index f085a2e..e22595a 100644 --- a/emitter.go +++ b/emitter.go @@ -44,12 +44,16 @@ func CopyMulty(src io.Reader, writers ...io.Writer) (err error) { buf := make([]byte, 5*1024*1024) wIndex := 0 modifier := NewHTTPModifier(&Settings.modifierConfig) + filteredRequests := make(map[string]time.Time) + filteredRequestsLastCleanTime := time.Now() for { nr, er := src.Read(buf) if nr > 0 && len(buf) > nr { payload := buf[:nr] + meta := payloadMeta(payload) + requestID := string(meta[1]) _maxN := nr if nr > 500 { @@ -60,23 +64,31 @@ func CopyMulty(src io.Reader, writers ...io.Writer) (err error) { 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 != nil { + if 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 modifier tells to skip request + if len(body) == 0 { + filteredRequests[requestID] = time.Now() + continue + } - if originalBodyLen != len(body) { - payload = append(payload[:headSize], body...) - } + 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.debug { + Debug("[EMITTER] Rewritten input:", len(payload), "First 500 bytes:", string(payload[0:_maxN])) + } + } else { + if _, ok := filteredRequests[requestID]; ok { + delete(filteredRequests, requestID); + continue + } } } @@ -104,5 +116,17 @@ func CopyMulty(src io.Reader, writers ...io.Writer) (err error) { break } } + + // Clean up filtered requests for which we didn't get a response to filter + now := time.Now() + if now.Sub(filteredRequestsLastCleanTime) > 60 * time.Second { + for k, v := range filteredRequests { + if now.Sub(v) > 60 * time.Second { + delete(filteredRequests, k) + } + } + filteredRequestsLastCleanTime = time.Now() + } + return err } diff --git a/emitter_test.go b/emitter_test.go index aec1e30..500cecd 100644 --- a/emitter_test.go +++ b/emitter_test.go @@ -5,6 +5,7 @@ import ( "sync" "sync/atomic" "testing" + "time" ) func TestEmitter(t *testing.T) { @@ -31,6 +32,51 @@ func TestEmitter(t *testing.T) { close(quit) } +func TestEmitterFiltered(t *testing.T) { + wg := new(sync.WaitGroup) + quit := make(chan int) + + input := NewTestInput() + input.skipHeader = true + + output := NewTestOutput(func(data []byte) { + wg.Done() + }) + + Plugins.Inputs = []io.Reader{input} + Plugins.Outputs = []io.Writer{output} + methods := HTTPMethods{[]byte("GET")} + Settings.modifierConfig = HTTPModifierConfig{methods: methods} + + go Start(quit) + + wg.Add(2) + + id := uuid() + reqh := payloadHeader(RequestPayload, id, time.Now().UnixNano(), -1) + reqb := append(reqh, []byte("GET / HTTP/1.1\r\nHost: www.w3.org\r\nUser-Agent: Go 1.1 package http\r\nAccept-Encoding: gzip\r\n\r\n")...) + + resh := payloadHeader(ResponsePayload, id, time.Now().UnixNano()+1, 1) + respb := append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...) + + input.EmitBytes(reqb) + input.EmitBytes(respb) + + id = uuid() + reqh = payloadHeader(RequestPayload, id, time.Now().UnixNano(), -1) + reqb = append(reqh, []byte("POST / HTTP/1.1\r\nHost: www.w3.org\r\nUser-Agent: Go 1.1 package http\r\nAccept-Encoding: gzip\r\n\r\n")...) + + resh = payloadHeader(ResponsePayload, id, time.Now().UnixNano()+1, 1) + respb = append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...) + + input.EmitBytes(reqb) + input.EmitBytes(respb) + + wg.Wait() + + close(quit) +} + func TestEmitterRoundRobin(t *testing.T) { wg := new(sync.WaitGroup) quit := make(chan int) diff --git a/test_input.go b/test_input.go index 0f990e7..a69debf 100644 --- a/test_input.go +++ b/test_input.go @@ -9,6 +9,7 @@ import ( // TestInput used for testing purpose, it allows emitting requests on demand type TestInput struct { data chan []byte + skipHeader bool } // NewTestInput constructor for TestInput @@ -22,13 +23,23 @@ func NewTestInput() (i *TestInput) { func (i *TestInput) Read(data []byte) (int, error) { buf := <-i.data - header := payloadHeader(RequestPayload, uuid(), time.Now().UnixNano(), -1) - copy(data[0:len(header)], header) - copy(data[len(header):], buf) + var header []byte + + if !i.skipHeader { + header = payloadHeader(RequestPayload, uuid(), time.Now().UnixNano(), -1) + copy(data[0:len(header)], header) + copy(data[len(header):], buf) + } else { + copy(data, buf) + } return len(buf) + len(header), nil } +func (i *TestInput) EmitBytes(data []byte) { + i.data <- data +} + // EmitGET emits GET request without headers func (i *TestInput) EmitGET() { i.data <- []byte("GET / HTTP/1.1\r\n\r\n") From dfd5fa0c4ff09ea5f3e6d005dcd7d9899591cf63 Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Thu, 22 Dec 2016 10:28:35 +0100 Subject: [PATCH 04/19] Update documentation --- input_kafka.go | 7 ++++--- output_kafka.go | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/input_kafka.go b/input_kafka.go index 5420340..48c890f 100644 --- a/input_kafka.go +++ b/input_kafka.go @@ -8,17 +8,18 @@ import ( "github.com/Shopify/sarama" ) -// KafkaInput should make consumer client. +// KafkaInput is used for recieving Kafka messages and +// transforming them into HTTP payloads. type KafkaInput struct { config *KafkaConfig consumers []sarama.PartitionConsumer messages chan *sarama.ConsumerMessage } -// NewKafkaInput constructor for KafkaInput +// NewKafkaInput creates instance of kafka consumer client. func NewKafkaInput(address string, config *KafkaConfig) *KafkaInput { c := sarama.NewConfig() - // c.Consumer. + // Configuration options go here con, err := sarama.NewConsumer([]string{config.host}, c) if err != nil { diff --git a/output_kafka.go b/output_kafka.go index 77c4a35..35a0d67 100644 --- a/output_kafka.go +++ b/output_kafka.go @@ -11,7 +11,7 @@ import ( "github.com/buger/gor/proto" ) -// KafkaOutput should make producer client. +// KafkaOutput is used for sending payloads to kafka in JSON format. type KafkaOutput struct { config *KafkaConfig producer sarama.AsyncProducer From be49c7e7ac7dbcdc06ea1e7ba72c86af5678570c Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 23 Dec 2016 20:25:09 +0300 Subject: [PATCH 05/19] Run GC on each 1000th request --- Makefile | 5 ++++- emitter.go | 23 +++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 614ff1e..cd1c5e5 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ SOURCE = emitter.go gor.go gor_stat.go input_dummy.go input_file.go input_raw.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:go +RUN = docker run -v `pwd`:$(SOURCE_PATH) -p 0.0.0.0:$(PORT):$(PORT) -i -t gor BENCHMARK = BenchmarkRAWInput TEST = TestRawListenerBench VERSION = DEV-$(shell date +%s) @@ -12,6 +12,9 @@ FADDR = ":8000" release: release-x64 release-mac +release-bin: + docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i gor go build -tags netgo $(LDFLAGS) + release-x64: docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i gor go build -tags netgo $(LDFLAGS) && tar -czf gor_$(VERSION)_x64.tar.gz gor && rm gor diff --git a/emitter.go b/emitter.go index e22595a..5741345 100644 --- a/emitter.go +++ b/emitter.go @@ -47,6 +47,8 @@ func CopyMulty(src io.Reader, writers ...io.Writer) (err error) { filteredRequests := make(map[string]time.Time) filteredRequestsLastCleanTime := time.Now() + i := 0 + for { nr, er := src.Read(buf) @@ -115,17 +117,22 @@ func CopyMulty(src io.Reader, writers ...io.Writer) (err error) { err = er break } - } - // Clean up filtered requests for which we didn't get a response to filter - now := time.Now() - if now.Sub(filteredRequestsLastCleanTime) > 60 * time.Second { - for k, v := range filteredRequests { - if now.Sub(v) > 60 * time.Second { - delete(filteredRequests, k) + // Run GC on each 1000 request + if i % 1000 == 0 { + // Clean up filtered requests for which we didn't get a response to filter + now := time.Now() + if now.Sub(filteredRequestsLastCleanTime) > 60 * time.Second { + for k, v := range filteredRequests { + if now.Sub(v) > 60 * time.Second { + delete(filteredRequests, k) + } + } + filteredRequestsLastCleanTime = time.Now() } } - filteredRequestsLastCleanTime = time.Now() + + i++ } return err From 5909dd71c54eec8060f788328effb5db613bf33a Mon Sep 17 00:00:00 2001 From: Sergey Mostovoy Date: Tue, 27 Dec 2016 12:18:49 +0200 Subject: [PATCH 06/19] Support %r - request id - output file pattern #381 --- output_file.go | 44 ++++++++++++++++++++++++++++---------------- output_file_test.go | 27 +++++++++++++++++++++++++++ settings.go | 2 +- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/output_file.go b/output_file.go index a50b905..1ac00dc 100644 --- a/output_file.go +++ b/output_file.go @@ -16,14 +16,15 @@ import ( "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()) }, +var dateFileNameFuncs = map[string]func(*FileOutput) string{ + "%Y": func(o *FileOutput) string { return time.Now().Format("2006") }, + "%m": func(o *FileOutput) string { return time.Now().Format("01") }, + "%d": func(o *FileOutput) string { return time.Now().Format("02") }, + "%H": func(o *FileOutput) string { return time.Now().Format("15") }, + "%M": func(o *FileOutput) string { return time.Now().Format("04") }, + "%S": func(o *FileOutput) string { return time.Now().Format("05") }, + "%NS": func(o *FileOutput) string { return fmt.Sprint(time.Now().Nanosecond()) }, + "%r": func(o *FileOutput) string { return o.currentID }, } type FileOutputConfig struct { @@ -35,13 +36,15 @@ type FileOutputConfig struct { // FileOutput output plugin type FileOutput struct { - mu sync.Mutex - pathTemplate string - currentName string - file *os.File - queueLength int - chunkSize int - writer io.Writer + mu sync.Mutex + pathTemplate string + currentName string + file *os.File + queueLength int + chunkSize int + writer io.Writer + requestPerFile bool + currentID string config *FileOutputConfig } @@ -53,6 +56,10 @@ func NewFileOutput(pathTemplate string, config *FileOutputConfig) *FileOutput { o.config = config o.updateName() + if strings.Contains(pathTemplate, "%r") { + o.requestPerFile = true + } + go func() { for { time.Sleep(time.Second) @@ -123,7 +130,7 @@ func (o *FileOutput) filename() string { path := o.pathTemplate for name, fn := range dateFileNameFuncs { - path = strings.Replace(path, name, fn(), -1) + path = strings.Replace(path, name, fn(o), -1) } if !o.config.append { @@ -167,6 +174,11 @@ func (o *FileOutput) updateName() { } func (o *FileOutput) Write(data []byte) (n int, err error) { + if o.requestPerFile { + o.currentID = string(payloadMeta(data)[1]) + o.updateName() + } + if !isOriginPayload(data) { return len(data), nil } diff --git a/output_file_test.go b/output_file_test.go index 04601f8..22531a4 100644 --- a/output_file_test.go +++ b/output_file_test.go @@ -106,6 +106,33 @@ func TestFileOutputMultipleFiles(t *testing.T) { os.Remove(name3) } +func TestFileOutputFilePerRequest(t *testing.T) { + output := NewFileOutput("/tmp/log-%Y-%m-%d-%S-%r", &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\ntest")) + name1 := output.file.Name() + + output.Write([]byte("1 2 1\ntest")) + name2 := output.file.Name() + + time.Sleep(time.Second) + output.updateName() + + output.Write([]byte("1 3 1\ntest")) + name3 := output.file.Name() + + if name3 == name2 || name2 == name1 || 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}) diff --git a/settings.go b/settings.go index ac62f7c..f012867 100644 --- a/settings.go +++ b/settings.go @@ -96,7 +96,7 @@ func init() { 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.DurationVar(&Settings.outputFileConfig.flushInterval, "output-file-flush-interval", time.Second, "Interval for forcing buffer flush to the file, default: 1s.") flag.BoolVar(&Settings.outputFileConfig.append, "output-file-append", false, "The flushed chunk is appended to existence file or not. ") // Set default From 826cc45d44438189401227366cfb4193d0dcaf62 Mon Sep 17 00:00:00 2001 From: Sergey Mostovoy Date: Tue, 27 Dec 2016 12:46:47 +0200 Subject: [PATCH 07/19] clean files in test [#381] --- output_file_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/output_file_test.go b/output_file_test.go index 22531a4..98e7e5f 100644 --- a/output_file_test.go +++ b/output_file_test.go @@ -107,7 +107,7 @@ func TestFileOutputMultipleFiles(t *testing.T) { } func TestFileOutputFilePerRequest(t *testing.T) { - output := NewFileOutput("/tmp/log-%Y-%m-%d-%S-%r", &FileOutputConfig{append: true, flushInterval: time.Minute}) + output := NewFileOutput("/tmp/log-%Y-%m-%d-%S-%r", &FileOutputConfig{append: true}) if output.file != nil { t.Error("Should not initialize file if no writes") @@ -130,6 +130,7 @@ func TestFileOutputFilePerRequest(t *testing.T) { } os.Remove(name1) + os.Remove(name2) os.Remove(name3) } From f5e8e26e9624e7b65c6938184a13651578ee66f3 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Tue, 27 Dec 2016 17:16:29 +0300 Subject: [PATCH 08/19] Fix #408 --- .gitignore | 4 ++++ proto/fuzz.go | 12 ++++++++++++ proto/proto.go | 11 ++++++----- proto/proto_test.go | 13 +++++++++++++ 4 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 proto/fuzz.go diff --git a/.gitignore b/.gitignore index 6bc8d66..a6a9269 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ gor *.pcap .DS_Store + +corpus +crashers +suppressions diff --git a/proto/fuzz.go b/proto/fuzz.go new file mode 100644 index 0000000..f72bfbc --- /dev/null +++ b/proto/fuzz.go @@ -0,0 +1,12 @@ +// +build gofuzz + +package proto + +func Fuzz(data []byte) int { + + ParseHeaders([][]byte{data}, func(header []byte, value []byte) bool { + return true + }) + + return 1 +} diff --git a/proto/proto.go b/proto/proto.go index e63ac9f..18c6f29 100644 --- a/proto/proto.go +++ b/proto/proto.go @@ -181,11 +181,10 @@ func HeadersEqual(h1 []byte, h2 []byte) bool { // Parsing headers from multiple payloads func ParseHeaders(payloads [][]byte, cb func(header []byte, value []byte) bool) { - - hS := [2]int{0, 0} - hE := [2]int{-1, -1} - vS := [2]int{-1, -1} - vE := [2]int{-1, -1} + hS := [2]int{0, 0} // header start + hE := [2]int{-1, -1} // header end + vS := [2]int{-1, -1} // value start + vE := [2]int{-1, -1} // value end i := 0 pIdx := 0 @@ -260,11 +259,13 @@ func ParseHeaders(payloads [][]byte, cb func(header []byte, value []byte) bool) hE = [2]int{pIdx, i} newLineBreak = false } + lineBreaks = 0 default: lineBreaks = 0 if hS[1] == -1 { hS = [2]int{pIdx, i} + hE = [2]int{-1, -1} } else { if hE[1] == -1 { break diff --git a/proto/proto_test.go b/proto/proto_test.go index 800799b..c685931 100644 --- a/proto/proto_test.go +++ b/proto/proto_test.go @@ -145,6 +145,19 @@ func TestParseHeaders(t *testing.T) { } } +// See https://github.com/dvyukov/go-fuzz and fuzz.go +func TestFuzzCrashers(t *testing.T) { + var crashers = []string{ + "\n:00\n", + } + + for _, f := range crashers { + ParseHeaders([][]byte{[]byte(f)}, func(header []byte, value []byte) bool { + return true + }) + } +} + func TestParseHeadersWithComplexUserAgent(t *testing.T) { // User-Agent could contain inside ':' // Parser should wait for \r\n From 8e5a2562266ed483f2f3cec7cc4536869b393fea Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Tue, 27 Dec 2016 18:48:59 +0300 Subject: [PATCH 09/19] Allow spaces inside filters --- http_modifier_settings.go | 13 ++++++++----- http_modifier_test.go | 12 ++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/http_modifier_settings.go b/http_modifier_settings.go index bf16fb2..3bea9f9 100644 --- a/http_modifier_settings.go +++ b/http_modifier_settings.go @@ -43,7 +43,8 @@ func (h *HTTPHeaderFilters) Set(value string) error { if len(valArr) < 2 { return errors.New("need both header and value, colon-delimited (ex. user_id:^169$).") } - r, err := regexp.Compile(valArr[1]) + val := strings.TrimSpace(valArr[1]) + r, err := regexp.Compile(val) if err != nil { return err } @@ -75,14 +76,16 @@ func (h *HTTPHashFilters) Set(value string) error { f := hashFilter{name: []byte(valArr[0])} - if strings.Contains(valArr[1], "%") { - p, _ := strconv.ParseInt(valArr[1][:len(valArr[1])-1], 0, 0) + val := strings.TrimSpace(valArr[1]) + + if strings.Contains(val, "%") { + p, _ := strconv.ParseInt(val[:len(val)-1], 0, 0) f.percent = uint32(p) - } else if strings.Contains(valArr[1], "/") { + } else if strings.Contains(val, "/") { // DEPRECATED format var num, den uint64 - fracArr := strings.Split(valArr[1], "/") + fracArr := strings.Split(val, "/") num, _ = strconv.ParseUint(fracArr[0], 10, 64) den, _ = strconv.ParseUint(fracArr[1], 10, 64) diff --git a/http_modifier_test.go b/http_modifier_test.go index 939e77e..dc53250 100644 --- a/http_modifier_test.go +++ b/http_modifier_test.go @@ -64,6 +64,18 @@ func TestHTTPModifierHeaderNegativeFilters(t *testing.T) { if len(modifier.Rewrite(payload)) != 0 { t.Error("Request should not pass filters") } + + filters = HTTPHeaderFilters{} + // Setting filter that not match our header + filters.Set("Host: www*") + + modifier = NewHTTPModifier(&HTTPModifierConfig{ + headerNegativeFilters: filters, + }) + + if len(modifier.Rewrite(payload)) != 0 { + t.Error("Request should not pass filters") + } } func TestHTTPModifierURLRewrite(t *testing.T) { From b0b697cba6780b0e6182659c71dcaf6f2b220d71 Mon Sep 17 00:00:00 2001 From: Sean Lewis Date: Wed, 4 Jan 2017 14:58:36 -0700 Subject: [PATCH 10/19] flush the output buffer when it should be flushed fixes #409 --- output_file.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/output_file.go b/output_file.go index 1ac00dc..5804214 100644 --- a/output_file.go +++ b/output_file.go @@ -36,15 +36,15 @@ type FileOutputConfig struct { // FileOutput output plugin type FileOutput struct { - mu sync.Mutex - pathTemplate string - currentName string - file *os.File - queueLength int - chunkSize int - writer io.Writer - requestPerFile bool - currentID string + mu sync.Mutex + pathTemplate string + currentName string + file *os.File + queueLength int + chunkSize int + writer io.Writer + requestPerFile bool + currentID string config *FileOutputConfig } @@ -62,8 +62,9 @@ func NewFileOutput(pathTemplate string, config *FileOutputConfig) *FileOutput { go func() { for { - time.Sleep(time.Second) + time.Sleep(config.flushInterval) o.updateName() + o.flush() } }() From 962fd70d1a9665c3bca9a4a4284d2d984bcda16d Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Thu, 5 Jan 2017 20:18:19 +0300 Subject: [PATCH 11/19] Add docs to master --- docs/Capturing-and-replaying-traffic.md | 49 ++++++++ docs/Commercial-FAQ.md | 58 ++++++++++ docs/Commercial-Support.md | 11 ++ docs/Commercial-collaboration.md | 19 ++++ docs/Compilation.md | 30 +++++ docs/Development-Setup.md | 39 +++++++ docs/Distributed-configuration.md | 26 +++++ docs/Exporting-to-ElasticSearch.md | 38 +++++++ docs/FAQ.md | 32 ++++++ docs/Getting-Started.md | 44 ++++++++ docs/Home.md | 29 +++++ docs/Middleware.md | 74 ++++++++++++ docs/Rate-limiting.md | 37 ++++++ docs/Replaying-HTTP-traffic.md | 46 ++++++++ docs/Request-filtering.md | 35 ++++++ docs/Request-rewriting.md | 38 +++++++ docs/Running-as-non-root-user.md | 17 +++ docs/Saving-and-Replaying-from-file.md | 105 ++++++++++++++++++ docs/The-Basics.md | 17 +++ docs/Troubleshooting.md | 90 +++++++++++++++ ...g-and-replaying-keep-alive-TCP-sessions.md | 11 ++ docs/[PRO]-Replaying-Binary-protocols.md | 14 +++ docs/_Footer.md | 1 + 23 files changed, 860 insertions(+) create mode 100644 docs/Capturing-and-replaying-traffic.md create mode 100644 docs/Commercial-FAQ.md create mode 100644 docs/Commercial-Support.md create mode 100644 docs/Commercial-collaboration.md create mode 100644 docs/Compilation.md create mode 100644 docs/Development-Setup.md create mode 100644 docs/Distributed-configuration.md create mode 100644 docs/Exporting-to-ElasticSearch.md create mode 100644 docs/FAQ.md create mode 100644 docs/Getting-Started.md create mode 100644 docs/Home.md create mode 100644 docs/Middleware.md create mode 100644 docs/Rate-limiting.md create mode 100644 docs/Replaying-HTTP-traffic.md create mode 100644 docs/Request-filtering.md create mode 100644 docs/Request-rewriting.md create mode 100644 docs/Running-as-non-root-user.md create mode 100644 docs/Saving-and-Replaying-from-file.md create mode 100644 docs/The-Basics.md create mode 100644 docs/Troubleshooting.md create mode 100644 docs/[PRO]-Recording-and-replaying-keep-alive-TCP-sessions.md create mode 100644 docs/[PRO]-Replaying-Binary-protocols.md create mode 100644 docs/_Footer.md diff --git a/docs/Capturing-and-replaying-traffic.md b/docs/Capturing-and-replaying-traffic.md new file mode 100644 index 0000000..0e37fd9 --- /dev/null +++ b/docs/Capturing-and-replaying-traffic.md @@ -0,0 +1,49 @@ +Think about Gor more like a network analyzer or tcpdump on steroids, it is not a proxy and does not affect your app anyhow. You specify application port, and it will capture and replay incoming data. + +Simplest setup will be: +```bash +# Run on servers where you want to catch traffic. You can run it on every `web` machine. +sudo gor --input-raw :80 --output-http http://staging.com +``` +It will record and replay traffic from the same machine. However, it is possible to use [[Aggregator-forwarder setup]], when Gor on your web machines forward traffic to Gor aggregator instance running on the separate server. + +> You may notice that it require `sudo`: to analyze network Gor need permissions which available only to root users. However, it is possible to configure Gor [beign run for non-root users](Running as a non-root user). + + +### Forwarding to multiple addresses + +You can forward traffic to multiple endpoints. +``` +gor --input-tcp :28020 --output-http "http://staging.com" --output-http "http://dev.com" +``` + +### Splitting traffic +By default, it will send same traffic to all outputs, but you have options to equally split it (round-robin) using `--split-output` option. + +``` +gor --input-raw :80 --output-http "http://staging.com" --output-http "http://dev.com" --split-output true +``` + +### Tracking responses +By default `input-raw` does not intercept responses, only requests. You can turn response tracking using `--input-raw-track-response` option. When enable you will be able to access response information in middleware and `output-file`. + + +### Traffic interception engine +By default, Gor will use `libpcap` for intercepting traffic, it should work in most cases. If you have any troubles with it, you may try alternative engine: `raw_socket`. + +``` +sudo gor --input-raw :80 --input-raw-engine "raw_socket" --output-http "http://staging.com" +``` + +You can read more about [[Replaying HTTP traffic]]. + + +### Tracking original IP addresses +You can use `--input-raw-realip-header` option to specify header name: If not blank, injects header with given name and real IP value to the request payload. Usually, this header should be named: `X-Real-IP`, but you can specify any name. + +`gor --input-raw :80 --input-raw-realip-header "X-Real-IP" ...` + + +*** + +Also you may want to know about [[Rate limiting]], [[Request rewriting]] and [[Request filtering]] \ No newline at end of file diff --git a/docs/Commercial-FAQ.md b/docs/Commercial-FAQ.md new file mode 100644 index 0000000..9070f0a --- /dev/null +++ b/docs/Commercial-FAQ.md @@ -0,0 +1,58 @@ +### What are GoReplay PRO and GoReplay Enterprise? + +[GoReplay PRO](https://goreplay.org/pro.html) and GoReplay Enterprise are extensions to GoReplay which add more functionality and provide additional support options for customers. + +### Is there a trial version? + +There's no free trial but we do offer a 14 day period with full refund if it does not work for you. + +### What is the license? + +See [COMM-LICENSE](https://github.com/buger/gor/blob/master/COMM-LICENSE) in the root of the GoReplay repo. + +### How does PRO licensing work? + +Every organization running GoReplay Pro on its own servers must have a license. There's no limit to the amount of servers or environments used by that organization. + +### How does Enterprise licensing work? + +Every organization running Gor Enterprise on its own servers must have a license. There's **no limit** to the amount of servers or environments used by that organization. + +### What happens if my subscription lapses? + +You must have an active subscription to run GoReplay Pro or Enterprise. After a one week grace period, you'll lose access to binaries and priority support. You won't get any more updates or bug fixes. + +### How do I buy GoReplay Enterprise? + +Send email to [support@gortool.com](mailto:support@gortool.com) with your info. A PDF quote will be emailed to you with the price. Reply to that email with your purchase order or just "Sounds good" and we will send an invoice which can be paid with a credit card, ACH bank transfer or a paper check. + +### Can I upgrade from GoReplay Pro? + +Yes! Current subscribers can upgrade by [requesting a quote](mailto:support@gortool.com). **Please note that you are an existing Pro subscriber.** We will add a one-time discount on your first invoice to reflect any remaining Pro subscription credit. If you purchased GoReplay Pro 6 months ago for $950, you'll get a $475 discount. + +### Can I distribute GoReplay PRO or Enterprise to my customers? + +This is a common requirement for "on-site installs" or "appliances" sold to large corporations. + +The standard license is appropriate for SaaS usage as it does not allow distribution. GoReplay PRO and Enterprise have an Appliance license option which **does** allow you to distribute them. The Appliance license is $9,500/yr for Pro and $19,500/yr for Enterprise. It allows you to distribute the Pro or Enterprise binaries as part of your application and each of your customers to run GoReplay Pro or Enterprise. Email [support@gortool.com](mailto:support@gortool.com) to purchase. + +### Can you transfer a license? + +Licenses are **not** transferable to another company. We will transfer the license from a user-specific email to a group email address (e.g. john_smith@example.com -> tech@example.com) but only for **the same domain**. It is strongly recommended that you buy the license using a group email address so the license is not attached to any one employee's email address. + +### What does the license require me to do? + +Your purchase gets you a unique access URL for downloading the Pro and/or Enterprise binaries. The license agreement requires you to keep this access URL private. If we find your access URL is ever publicized: + +1. We'll send you a warning email with details. You need to remove the content and send a new email address so we can generate a new access URL for you. The old access URL will stop working immediately so you'll need to update your apps. +2. If your access URL is publicized a second time, we reserve the right to permanently remove access. + +### Can I get a refund? + +Yes, up to two weeks after purchase. Let us know the reason and maybe we can help but either way it's not a problem. Email [support@gortool.com](mailto:support@gortool.com). + +### How do I update my credit card info? + +If you purchased GoReplay Enterprise, there's nothing to do. Each annual invoice is paid separately. + +If you purchased GoReplay PRO, log into [Gumroad](https://gumroad.com) with your email address, click the Billing tab and enter your new card. I can't provide support for the Gumroad website and don't have the ability to edit customer info - if you can't log in or change your credit card, you can always let your current subscription expire and purchase a new subscription. \ No newline at end of file diff --git a/docs/Commercial-Support.md b/docs/Commercial-Support.md new file mode 100644 index 0000000..105fb58 --- /dev/null +++ b/docs/Commercial-Support.md @@ -0,0 +1,11 @@ +Gor offers only community support. Gor Pro and Enterprise offer priority support via email. + +## Priority Support + +Covers 1 incident per quarter, with a max response time of 2 working days. Scope is limited to Gor and Gor Pro and Enterprise features and APIs, not the application or infrastructure. For support, email **support** AT **gortool.com**. Please email using the same domain as the original license email or explain your connection to the licensed company. + +More aggressive support contracts (phone, quicker response time) are available separately, email with your needs. + +## Onboarding + +Enterprise customers may request a one hour video chat session with @buger to discuss their application(s), requirements and how best to leverage the various Gor features. Contact support to set up your session. \ No newline at end of file diff --git a/docs/Commercial-collaboration.md b/docs/Commercial-collaboration.md new file mode 100644 index 0000000..977cbcf --- /dev/null +++ b/docs/Commercial-collaboration.md @@ -0,0 +1,19 @@ +Collaboration is difficult with commercial closed source but I do want to keep as much of the OSS ethos as possible available to customers who want to fix it themselves. + +## Legal + +In order to unambiguously own and sell Gor commercial products, I must have the copyright associated with the entire codebase. Any code you create which is merged must be owned by me. That's not me trying to be a jerk, that's just the way it works. + +## Application + +If you wish access to the product repository so you can send a PR, just open a new Gor issue and include the following info: + +1. the email address that bought the license, a max of one collaborator per license +1. the following statement "I assign all rights, including copyright, to any future Gor work by myself to Leonid Bugaev" + +You should be granted access to the private repo soon after. + +## Notes + +1. You should **never** work on the master branch. Only I may merge changes. +1. I may revoke access for any reason at any time. Access is not guaranteed with purchase. \ No newline at end of file diff --git a/docs/Compilation.md b/docs/Compilation.md new file mode 100644 index 0000000..8eb38fd --- /dev/null +++ b/docs/Compilation.md @@ -0,0 +1,30 @@ +We provide pre-compiled binaries for Mac and Linux, but you are free to compile Gor by yourself. + +Gor is written using Go, so first you need to download it from here https://golang.org/, use the latest stable version. + +The only Gor dependency is [libpcap](https://github.com/the-tcpdump-group/libpcap), which is the interface to various kernel packet capture mechanisms, and https://github.com/google/gopacket, which is a Go wrapper around libpcap. Latest libpcap version can be obtained at http://www.tcpdump.org/release/. Libpcap itself depend on `flex` and `bison` packages, many operating systems already have them installed. + +```bash +# Fetch libpcap dependencies. Depending on your OS, instead of `apt` you will use `yum` or `rpm`, or `brew` on Mac. +sudo apt-get install flex bison -y + +# Download latest stable release, compile and install it +wget http://www.tcpdump.org/release/libpcap-1.7.4.tar.gz && tar xzf libpcap-1.7.4.tar.gz +cd libpcap-1.7.4 +./configure && make install + + +# Lets fetch Gor source code +mkdir $HOME/gocode +# See more information about GOPATH https://github.com/golang/go/wiki/GOPATH +export GOPATH=$HOME/gocode +# Fetch code from the Github +go get github.com/buger/gor + +# Compile from source +cd $HOME/gocode/src/github.com/buger/gor +go build LDFLAGS = -ldflags "-extldflags \"-static\"" +``` + +After you finished, you should see `gor` binary in current directory. + diff --git a/docs/Development-Setup.md b/docs/Development-Setup.md new file mode 100644 index 0000000..8e3d954 --- /dev/null +++ b/docs/Development-Setup.md @@ -0,0 +1,39 @@ +## STEP 1: Install Docker +For local development we recommend to use Docker. + +If you don’t have it you can read how to install it here: +https://docs.docker.com/engine/getstarted/step_one/#step-3-verify-your-installation + +## STEP 2: Download repository + +`git clone git@github.com:buger/goreplay.git` + + +## STEP 3: Setup container + +``` +cd ./goreplay +make build + +``` + +## Testing +To run tests execute next command: + +``` +make test +``` + +You can copy the command that is produced and modify it. For example, if you need to run one test copy the command and add `-run TestName`, e.g.: + +``` +docker run -v `pwd`:/go/src/github.com/buger/gor/ -p 0.0.0.0:8000:8000 -t -i gor:go go test ./. -run TestEmitterFiltered -timeout 60s -ldflags "-X main.VERSION=DEV-1482398347 -extldflags \"-static\"" -v +``` + + +## Building +To get a binary file run + +``` +make release-bin +``` diff --git a/docs/Distributed-configuration.md b/docs/Distributed-configuration.md new file mode 100644 index 0000000..2795bf8 --- /dev/null +++ b/docs/Distributed-configuration.md @@ -0,0 +1,26 @@ +Sometimes it makes sense to use separate Gor instance for replaying traffic and performing things like load testing, so your production machines do not spend precious resources. It is possible to configure Gor on your web machines forward traffic to Gor aggregator instance running on the separate server. + +```bash +# Run on servers where you want to catch traffic. You can run it on each `web` machine. +sudo gor --input-raw :80 --output-tcp replay.local:28020 + +# Replay server (replay.local). +gor --input-tcp replay.local:28020 --output-http http://staging.com +``` + +If you have multiple replay machines you can split traffic among them using `--split-output` option: it will equally split all incoming traffic to all outputs using round robin algorithm. +``` +gor --input-raw :80 --split-output --output-tcp replay1.local:28020 --output-tcp replay2.local:28020 +``` + +[GoReplay PRO](https://goreplay.com/pro.html) support accurate recording and replaying of tcp sessions, and when `--recognize-tcp-sessions` option is passed, instead of round-robin it will use a smarter algorithm which ensures that same sessions will be sent to the same replay instance. + + +In case if you are planning a large load testing, you may consider use separate master instance which will control Gor slaves which actually replay traffic. For example: +``` +# This command will read multiple log files, replay them on 10x speed and loop them if needed for 30 seconds, and will distributed traffic (tcp session aware) among multiple workers +gor --input-file logs_from_multiple_machines.*|1000% --input-file-loop --exit-after 30s --recognize-tcp-sessions --split-output --output-tcp worker1.local --output-tcp worker2.local:27017 --output-tcp worker3.local:27017 ... --output-tcp workerN.local:27017 + +# worker +gor --input-tcp :27017 --ouput-http load_test.target +``` diff --git a/docs/Exporting-to-ElasticSearch.md b/docs/Exporting-to-ElasticSearch.md new file mode 100644 index 0000000..228ccdb --- /dev/null +++ b/docs/Exporting-to-ElasticSearch.md @@ -0,0 +1,38 @@ +Gor can export requests and replayed response data to ElasticSearch: + +``` +./gor --input-raw :8000 --output-http http://staging.com --output-http-elasticsearch localhost:9200/gor +``` + +You don't have to create the index upfront. That will be done for you automatically. + +### Format + +Following structure represents ES format: + +``` +type ESRequestResponse struct { + ReqURL string `json:"Req_URL"` + ReqMethod string `json:"Req_Method"` + ReqUserAgent string `json:"Req_User-Agent"` + ReqAcceptLanguage string `json:"Req_Accept-Language,omitempty"` + ReqAccept string `json:"Req_Accept,omitempty"` + ReqAcceptEncoding string `json:"Req_Accept-Encoding,omitempty"` + ReqIfModifiedSince string `json:"Req_If-Modified-Since,omitempty"` + ReqConnection string `json:"Req_Connection,omitempty"` + ReqCookies string `json:"Req_Cookies,omitempty"` + RespStatus string `json:"Resp_Status"` + RespStatusCode string `json:"Resp_Status-Code"` + RespProto string `json:"Resp_Proto,omitempty"` + RespContentLength string `json:"Resp_Content-Length,omitempty"` + RespContentType string `json:"Resp_Content-Type,omitempty"` + RespTransferEncoding string `json:"Resp_Transfer-Encoding,omitempty"` + RespContentEncoding string `json:"Resp_Content-Encoding,omitempty"` + RespExpires string `json:"Resp_Expires,omitempty"` + RespCacheControl string `json:"Resp_Cache-Control,omitempty"` + RespVary string `json:"Resp_Vary,omitempty"` + RespSetCookie string `json:"Resp_Set-Cookie,omitempty"` + Rtt int64 `json:"RTT"` + Timestamp time.Time +} +``` \ No newline at end of file diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000..a30c1c8 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,32 @@ +### What OS are supported? +Gor will run everywhere where [libpcap](http://www.tcpdump.org/) works, and it works on most of the platforms. However, currently, we test it on Linux and Mac. See more about [[Compilation]]. + +### Why does the `--input-raw` requires sudo or root access? +Listener works by sniffing traffic from a given port. It's accessible +only by using sudo or root access. But it is possible to [[Running as non root user]]. + +### How do you deal with user session to replay the traffic correctly? +You can rewrite session related headers/params to match your staging environment. If you require custom logic (e.g random token based auth) follow this discussion: https://github.com/buger/gor/issues/154 + +### Can I use Gor to intercept SSL traffic? +Basic idea is that SSL was made to protect itself from traffic interception. There 2 options: +1. Move SSL handling to proxy like Nginx or Amazon ELB. And allow Gor to listen on upstreams. +2. Use `--input-http` so you can duplicate request payload directly from your app to Gor, but it will require your app modifications. + +More can be find here: https://github.com/buger/gor/issues/85 + +### Is there a limit for size of HTTP request when using output-http? +Due to the fact that Gor can't guarantee interception of all packets, for large payloads > 200kb there is chance of missing some packets and corrupting body. Treat it as a feature and chance to test broken bodies handling :) +The only way to guarantee delivery is using `--input-http`, but you will miss some features. + +### I'm getting 'too many open files' error +Typical Linux shell has a small open files soft limit at 1024. You can easily raise that when you do this before starting your gor replay process: + + ulimit -n 64000 + +More about ulimit: http://www.thecodingmachine.com/solving-the-too-many-open-files-exception-in-red5-or-any-other-application/ + +### The CPU average across my load-balanced targets is higher than the source +If you are replaying traffic from multiple listeners to a load-balanced target and you use sticky sessions, you may observe that the target servers have a higher CPU load than the listener servers. This may be because the sticky session cookie of the original load balancer is not honored by the target load balancer thus resulting in requests that would normally hit the same target server hitting different servers on the backend thus reducing some caching benefits gained via the load balancing. Try running just one listener against one replay target and see if the CPU utilization comparison is more accurate. + +Also see [[Troubleshooting]]. \ No newline at end of file diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md new file mode 100644 index 0000000..6d16f63 --- /dev/null +++ b/docs/Getting-Started.md @@ -0,0 +1,44 @@ +### Dependencies +To start working with Gor, you need to have a web server running on your machine, and a terminal to run commands. If you are just poking around, you can quickly start the server by calling `gor file-server :8000`, this will start a simple file server of the current directory on port `8000`. + +### Installing Gor +Download the latest Gor binary from https://github.com/buger/gor/releases (we provide precompiled binaries for Windows, Linux x64 and Mac OS), or you can compile by yourself [[Compilation]]. + +Once the archive is downloaded and uncompressed, you can run Gor from the current directory, or you may want to copy binary to your PATH (for Linux and Mac OS it can be `/usr/local/bin`). + +### Capturing web traffic +Now run this command in terminal: `sudo ./gor --input-raw :8000 --output-stdout` + +This command says to listen for all network activity happening on port 8000 and log it to stdout. +If you are familiar with `tcpdump`, we are going to implement similar functionality. + +> You may notice that it uses `sudo` and asks for the password: to analyze network, Gor needs permissions which are available only to super users. +> However, it is possible to configure Gor [being run for non-root users](Running-as-non-root-user). + + +Make a few requests by opening `http://localhost:8000` in your browser, or just by calling curl in terminal `curl http://localhost:8000`. You should see that `gor` outputs all the HTTP requests and responses right to the terminal window where it is running. + + +**Gor is not a proxy:** you do not need to put 3-rd party tool to your critical path. Instead Gor just silently analyzes the traffic of your application and does not affect it anyhow. + +### Replaying + +Now it's time to replay your original traffic to another environment. Let's start the same file web server but on a different port: `gor file-server :8001`. + +Instead of `--output-stdout` we will use `--output-http` and provide URL of second server: `sudo ./gor --input-raw :8000 --output-http="http://localhost:8001"` + +Make few requests to first server. You should see them replicated to the second one, voila! + +### Saving requests to file and replaying them later +Sometimes it's not possible to replay requests in real time; Gor allows you to save requests to the file and replay them later. + +First use `--output-file` to save them: `sudo ./gor --input-raw :8000 --output-file=requests.gor`. This will create new file and continuously write all captured requests to it. + +Let's re-run Gor, but now to replay requests from file: `./gor --input-file requests.gor --output-http="http://localhost:8001"`. You should see all the recorded requests coming to the second server, and they will be replayed in the same order and with exactly same timing as they were recorded. + +Next: [[The Basics]] + +### Watch an overview: + + +[![Foo](http://img.youtube.com/vi/CxuKZcMKaW4/0.jpg)](https://www.youtube.com/watch?v=CxuKZcMKaW4) \ No newline at end of file diff --git a/docs/Home.md b/docs/Home.md new file mode 100644 index 0000000..88a7bc9 --- /dev/null +++ b/docs/Home.md @@ -0,0 +1,29 @@ +Gor is an open-source tool for capturing and replaying live HTTP traffic into a test environment in order to continuously test your system with real data. It can be used to increase confidence in code deployments, configuration changes and infrastructure changes. + +Read for more info: + +* [[Getting Started]] +* [[The Basics]] +* [[Capturing and replaying traffic]] +* [[Replaying HTTP traffic]] +* [[[PRO] Replaying Binary protocols]] +* [[[PRO] Recording and replaying keep alive TCP sessions]] +* [[Saving and Replaying from file]] +* [Performance testing](https://github.com/buger/gor/wiki/Saving-and-Replaying-from-file#performance-testing) +* [[Rate limiting]] +* [[Request filtering]] +* [[Request rewriting]] +* [[Middleware]] +* [[Distributed configuration]] +* [[Exporting to ElasticSearch]] +* [[FAQ]] +* [[Troubleshooting]] + +## Commercial Aspects + +* [[Commercial Support]] +* [[Commercial FAQ]] +* [[Commercial collaboration]] + + +Next: [Getting Started](Getting-Started) \ No newline at end of file diff --git a/docs/Middleware.md b/docs/Middleware.md new file mode 100644 index 0000000..7c98cbe --- /dev/null +++ b/docs/Middleware.md @@ -0,0 +1,74 @@ +#### Overview +Middleware is a program that accepts request and response payload at STDIN and emits modified requests at STDOUT. You can implement any custom logic like stripping private data, advanced rewriting, support for oAuth and etc. Check examples [included into our repo](https://github.com/buger/gor/tree/master/examples/middleware). + + +``` + Original request +--------------+ ++-------------+----------STDIN---------->+ | +| Gor input | | Middleware | ++-------------+----------STDIN---------->+ | + Original response (1) +------+---+---+ + | ^ ++-------------+ Modified request v | +| Gor output +<---------STDOUT-----------------+ | ++-----+-------+ | + | | + | Replayed response | + +------------------STDIN----------------->----+ +``` + +(1): Original responses will only be sent to the middleware if the `--input-raw-track-response` option is specified. + +Middleware can be written in any language, see `examples/middleware` folder for examples. +Middleware program should accept the fact that all communication with Gor is asynchronous, there is no guarantee that original request and response messages will come one after each other. Your app should take care of the state if logic depends on original or replayed response, see `examples/middleware/token_modifier.go` as example. + +Simple bash echo middleware (returns same request) will look like this: +```bash +while read line; do + echo $line +end +``` + +Middleware can be enabled using `--middleware` option, by specifying path to executable file: +``` +gor --input-raw :80 --middleware "/opt/middleware_executable" --output-http "http://staging.server" +``` + +#### Communication protocol +All messages should be hex encoded, new line character specifieds the end of the message, eg. new message per line. + +Decoded payload consist of 2 parts: header and HTTP payload, separated by new line character. + +Example request payload: + +``` +1 932079936fa4306fc308d67588178d17d823647c 1439818823587396305 +GET /a HTTP/1.1 +Host: 127.0.0.1 + +``` + +Example response payload (note: you will only receive this if you specify `--input-raw-track-response`) + +``` +2 8e091765ae902fef8a2b7d9dd960e9d52222bd8c 1439818823587996305 2782013 +HTTP/1.1 200 OK +Date: Mon, 17 Aug 2015 13:40:23 GMT +Content-Length: 0 +Content-Type: text/plain; charset=utf-8 + +``` + +Header contains request meta information separated by spaces. First value is payload type, possible values: `1` - request, `2` - original response, `3` - replayed response. +Next goes request id: unique among all requests (sha1 of time and Ack), but remain same for original and replayed response, so you can create associations between request and responses. The third argument is the time when request/response was initiated/received. Forth argument is populated only for responses and means latency. + +HTTP payload is unmodified HTTP requests/responses intercepted from network. You can read more about request format [here](http://www.jmarshall.com/easy/http/), [here](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol) and [here](http://www.w3.org/Protocols/rfc2616/rfc2616.html). You can operate with payload as you want, add headers, change path, and etc. Basically you just editing a string, just ensure that it is RCF compliant. + +At the end modified (or untouched) request should be emitted back to STDOUT, keeping original header, and hex-encoded. If you want to filter request, just not send it. Emitting responses back is required, even if you did not touch them. + +#### Advanced example +Imagine that you have auth system that randomly generate access tokens, which used later for accessing secure content. Since there is no pre-defined token value, naive approach without middleware (or if middleware use only request payloads) will fail, because replayed server have own tokens, not synced with origin. To fix this, our middleware should take in account responses of replayed and origin server, store `originalToken -> replayedToken` aliases and rewrite all requests using this token to use replayed alias. See [examples/middleware/token_modifier.go](https://github.com/buger/gor/tree/master/examples/middleware/token_modifier.go) and [middleware_test.go#TestTokenMiddleware](https://github.com/buger/gor/tree/master/middleware_test.go) as example of described scheme. + +*** + +You may also read about [[Request filtering]], [[Rate limiting]] and [[Request rewriting]]. \ No newline at end of file diff --git a/docs/Rate-limiting.md b/docs/Rate-limiting.md new file mode 100644 index 0000000..ce0003b --- /dev/null +++ b/docs/Rate-limiting.md @@ -0,0 +1,37 @@ +Rate limiting can be useful if you only want to forward parts of incoming traffic, for example, to not overload your test environment. There are two strategies: dropping random requests or dropping fractions of requests based on Header or URL param value. + +### Dropping random requests +Every input and output support random rate limiting. +There are two limiting algorithms: absolute or percentage based. + +**Absolute**: If for current second it reached specified requests limit - disregard the rest, on next second counter reset. + +**Percentage**: For input-file it will slowdown or speedup request execution, for the rest it will use the random generator to decide if request pass or not based on the chance you specified. + +You can specify your desired limit using the "|" operator after the server address, see examples below. + +#### Limiting replay using absolute number +``` +# staging.server will not get more than ten requests per second +gor --input-tcp :28020 --output-http "http://staging.com|10" +``` + +#### Limiting listener using percentage based limiter +``` +# replay server will not get more than 10% of requests +# useful for high-load environments +gor --input-raw :80 --output-tcp "replay.local:28020|10%" +``` + +### Consistent limiting based on Header or URL param value +If you have unique user id (like API key) stored in header or URL you can consistently forward specified percent of traffic only for the fraction of this users. +Basic formula looks like this: `FNV32-1A_hashing(value) % 100 >= chance`. Examples: +``` +# Limit based on header value +gor --input-raw :80 --output-tcp "replay.local:28020|10%" --http-header-limiter "X-API-KEY: 10%" + +# Limit based on header value +gor --input-raw :80 --output-tcp "replay.local:28020|10%" --http-param-limiter "api_key: 10%" +``` + +When limiting based on header or param only percentage based limiting supported. \ No newline at end of file diff --git a/docs/Replaying-HTTP-traffic.md b/docs/Replaying-HTTP-traffic.md new file mode 100644 index 0000000..7004323 --- /dev/null +++ b/docs/Replaying-HTTP-traffic.md @@ -0,0 +1,46 @@ +Gor can replay HTTP traffic using `--output-http` option: + +```bash +sudo ./gor --input-raw :8000 --output-http="http://staging.env" +``` + +You can [filter](Request filtering), [rate limit](Rate limiting) and [rewrite](Request rewriting) requests on the fly. + +### HTTP output workers +By default Gor creates a dynamic pool of workers: it starts with 10 and creates more HTTP output workers when the HTTP output queue length is greater than 10. The number of workers created (N) is equal to the queue length at the time which it is checked and found to have a length greater than 10. The queue length is checked every time a message is written to the HTTP output queue. No more workers will be spawned until that request to spawn N workers is satisfied. If a dynamic worker cannot process a message at that time, it will sleep for 100 milliseconds. If a dynamic worker cannot process a message for 2 seconds it dies. +You may specify fixed number of workers using `--output-http-workers=20` option. + +### Following redirects +By default Gor will ignore all redirects since they are handled by clients using your app, but in scenarios where your replayed environment introduces new redirects, you can enable them like this: +``` +gor --input-tcp replay.local:28020 --output-http http://staging.com --output-http-redirects 2 +``` +The given example will follow up to 2 redirects per request. + +### HTTP timeouts +By default http timeout for both request and response is 5 seconds. You can override it like this: +``` +gor --input-tcp replay.local:28020 --output-http http://staging.com --output-http-timeout 30s +``` + +### Response buffer +By default, to reduce memory consumption, internal HTTP client will fetch max 200kb of the response body (used if you use middleware), by you can increase limit using `--output-http-response-buffer` option (accepts number of bytes). + +### Basic Auth + +If your development or staging environment is protected by Basic Authentication then those credentials can be injected in during the replay: + +``` +gor --input-raw :80 --output-http "http://user:pass@staging.com" +``` + +Note: This will overwrite any Authorization headers in the original request. + + +### Multiple domains support + +If you app accepts traffic from multiple domains, and you want to keep original headers, there is specific `--http-original-host` with tells Gor do not touch Host header at all. + + +*** +You may also read about [[Saving and Replaying from file]] \ No newline at end of file diff --git a/docs/Request-filtering.md b/docs/Request-filtering.md new file mode 100644 index 0000000..07d2285 --- /dev/null +++ b/docs/Request-filtering.md @@ -0,0 +1,35 @@ +Filtering is useful when you need to capture only specific part of traffic, like API requests. It is possible to filter by URL, HTTP header or HTTP method. + +#### Allow url regexp +``` +# only forward requests being sent to the /api endpoint +gor --input-raw :8080 --output-http staging.com --http-allow-url /api +``` + +#### Disallow url regexp +``` +# only forward requests NOT being sent to the /api... endpoint +gor --input-raw :8080 --output-http staging.com --http-disallow-url /api +``` +#### Filter based on regexp of header + +``` +# only forward requests with an api version of 1.0x +gor --input-raw :8080 --output-http staging.com --http-allow-header api-version:^1\.0\d + +# only forward requests NOT containing User-Agent header value "Replayed by Gor" +gor --input-raw :8080 --output-http staging.com --http-disallow-header "User-Agent: Replayed by Gor" +``` + +#### Filter based on HTTP method +Requests not matching a specified whitelist can be filtered out. For example to strip non-nullipotent requests: + +``` +gor --input-raw :80 --output-http "http://staging.server" \ + --http-allow-method GET \ + --http-allow-method OPTIONS +``` + + +----- +You may also read about [[Request rewriting]], [[Rate limiting]] and [[Middleware]] \ No newline at end of file diff --git a/docs/Request-rewriting.md b/docs/Request-rewriting.md new file mode 100644 index 0000000..adc68a4 --- /dev/null +++ b/docs/Request-rewriting.md @@ -0,0 +1,38 @@ +Gor supports rewriting of URLs, URL params and headers, see below. + +Rewriting may be useful if you test environment does not have the same data as your production, and you want to perform all actions in the context of `test` user: for example rewrite all API tokens to some test value. Other possible use cases are toggling features on/off using custom headers or rewriting URL's if they changed in the new environment. + +For more complex logic you can use [[Middleware]]. + +#### Rewrite URL based on a mapping +`--http-rewrite-url` expects value in ":" format: ":" is a dilimiter. In `` section you may use captured regexp group values. This works similar to `replace` method in Javascript or `gsub` in Ruby. + +``` +# Rewrites all `/v1/user//ping` requests to `/v2/user//ping` +gor --input-raw :8080 --output-http staging.com --http-rewrite-url /v1/user/([^\\/]+)/ping:/v2/user/$1/ping +``` + +#### Set URL param +Set request url param, if param already exists it will be overwritten. +``` +gor --input-raw :8080 --output-http staging.com --http-set-param api_key=1 +``` + +#### Set Header +Set request header, if header already exists it will be overwritten. May be useful if you need to identify requests generated by Gor or enable feature flagged functionality in an application: + +``` +gor --input-raw :80 --output-http "http://staging.server" \ + --http-header "User-Agent: Replayed by Gor" \ + --http-header "Enable-Feature-X: true" +``` + +#### Host header +Host header gets special treatment. By default Host get set to the value specified in --output-http. If you manually set --http-header "Host: anonther.com", Gor will not override Host value. + +If you app accepts traffic from multiple domains, and you want to keep original headers, there is specific `--http-original-host` with tells Gor do not touch Host header at all. + + +*** + +You may also read about [[Request filtering]], [[Rate limiting]] and [[Middleware]] \ No newline at end of file diff --git a/docs/Running-as-non-root-user.md b/docs/Running-as-non-root-user.md new file mode 100644 index 0000000..66c9512 --- /dev/null +++ b/docs/Running-as-non-root-user.md @@ -0,0 +1,17 @@ +You can enable Gor for non-root users in a secure method by using the following commands + +``` +# Following commands assume that you put `gor` binary to /usr/local/bin +add gor +addgroup gor +chgrp gor /usr/local/bin/gor +chmod 0750 /usr/local/bin/gor +setcap "cap_net_raw,cap_net_admin+eip" /usr/local/bin/gor +``` + +As a brief explanation of the above. +* We create a group called gor. +* We then add the user you want to the new group so they will be able to use gor without sudo +* We then change the user/group of gor binary the new group. +* We then make sure the permissions are set on gor binary so that members of the group can execute it but other normal users cannot. +* We then use `setcap` to give the CAP_NET_RAW and CAP_NET_ADMIN privilege to the executable when it runs. This is so that Gor can open its raw socket which is not normally permitted unless you are root. \ No newline at end of file diff --git a/docs/Saving-and-Replaying-from-file.md b/docs/Saving-and-Replaying-from-file.md new file mode 100644 index 0000000..568c7d5 --- /dev/null +++ b/docs/Saving-and-Replaying-from-file.md @@ -0,0 +1,105 @@ +You can save requests to file, and replay them later. While replaying it will preserve the original time differences between requests. If you apply [percentage based limiting](Rate Limiting) timing between requests will be reduced or increased appropriately: this approach opens possibilities like load testing, see below. + +```bash +# write to file +gor --input-raw :80 --output-file requests.log + +# read from file +gor --input-file requests.gor --output-http "http://staging.com" +``` + +By default Gor writes files in chunks. This configurable using `--output-file-append` option: the flushed chunk is appended to existence file or not. The default is **false**. By default, `--output-file` flushes each chunk to a different path. + +```bash +gor ... --output-file %Y%m%d.log +# append false +20140608_0.log +20140608_1.log +20140609_0.log +20140609_1.log +``` + +This makes parallel file processing easy. But if you want to disable this behavior, you can disable it by adding `--output-file-append` option: + +```bash +gor ... --output-file %Y%m%d.log --output-file-append +# append true +20140608.log +20140609.log +``` + +If you run gor multiple times, and it finds existing files, it will continue from last known index. + +### Chunk size + +You can set chunk limits using `--output-file-size-limit` and `--output-file-queue-limit` options. +The length of the chunk queue and the size of each chunk, respectively. The default values are 256 and 32mb, respectively. The suffixes “k” (KB), “m” (MB), and “g” (GB) can be used for `output-file-size-limit`. +If you want to have only size constraint, you can set `--output-file-queue-limit` to 0, and vice versa. + +```bash +gor --input-raw :80 --output-file %Y-%m-%d.gz --output-file-size-limit 256m --output-file-queue-limit 0 +``` + +### Using date variables in file names +For example, you can tell to create new file each hour: `--output-file /mnt/logs/requests-%Y-%m-%d-%H.log` +It will create new file for each hour: requests-2016-06-01-12.log, requests-2016-06-01-13.log, ... + +The time format used as part of the file name. The following characters are replaced with actual values when the file is created: + +* `%Y`: year including the century (at least 4 digits) +* `%m`: month of the year (01..12) +* `%d`: Day of the month (01..31) +* `%H`: Hour of the day, 24-hour clock (00..23) +* `%M`: Minute of the hour (00..59) +* `%S`: Second of the minute (00..60) + +The default format is `%Y%m%d%H`, which creates one file per hour. + + +### GZIP compression +To read or write GZIP compressed files ensure that file extension ends with ".gz": `--output-file log.gz` + +### Replaying from multiple files + +`--input-file` accepts file pattern, for example: `--input-file logs-2016-05-*`: it will replay all the files, sorting them in lexicographical order. + +### Buffered file output +Gor has memory buffer when it writes to file, and continuously flush changes to the file. Flushing to file happens if the buffer is filled, forced flush every 1 second, or if Gor is closed. You can change it using `--output-file-flush-interval` option. It most cases it should not be touched. + +### File format +HTTP requests stored as it is, plain text: headers and bodies. Requests separated by `\n🐵🙈🙉\n` line (using such sequence for uniqueness and fun). Before each request goes single line with meta information containing payload type (1 - request, 2 - response, 3 - replayed response), unique request ID (request and response have the same) and timestamp when request was made. An example of 2 requests: + +``` +1 d7123dasd913jfd21312dasdhas31 127345969\n +GET / HTTP/1.1\r\n +\r\n +\n +🐵🙈🙉 +\n +POST /upload HTTP/1.1\r\n +Content-Length: 7\r\n +Host: www.w3.org\r\n +\r\n +a=1&b=2 +``` +Note that technically \r and \n symbols are invisible, and indicate new lines. I made them visible in example just to show how it looks on byte level. + +Making it text friendly allows writing simple parsers and use console tools like `grep` to do an analysis. You can even edit them manually, but be sure that your file editor does not change line endings. + +## Performance testing + +Currently, this functionality supported only by `input-file` and only when using percentage based limiter. Unlike default limiter for `input-file` instead of dropping requests it will slowdown or speedup request emitting. Note that **limiter is applied to input**: + +``` +# Replay from file on 2x speed +gor --input-file "requests.gor|200%" --output-http "staging.com" +``` + +Use `--stats --output-http-stats` to see latency stats. + +### Looping files for replaying indefinitely +You can loop the same set of files, so when the last one replays all the requests, it will not stop, and will start from first one again. Having the only small amount of requests you can do extensive performance testing. +Pass `--input-file-loop` to make it work. + +*** +You may also read about [[Capturing and replaying traffic]] and [[Rate limiting]] \ No newline at end of file diff --git a/docs/The-Basics.md b/docs/The-Basics.md new file mode 100644 index 0000000..799c48c --- /dev/null +++ b/docs/The-Basics.md @@ -0,0 +1,17 @@ +### Overview +Gor architecture tries to follow UNIX philosophy: everything made of pipes, various inputs multiplexing data to outputs. + +You can [rate limit](Rate limiting), [filter](Request filtering), [rewrite](Request rewriting) requests or even use your own [middleware](Middleware) to implement custom logic. Also, it is possible to replay requests at the higher rate for [load testing](Saving and Replaying from file). + +### Available input and output plugins + +Available inputs: + * `--input-raw` - used to capture HTTP traffic, you should specify IP address or interface and application port. More about [[Capturing and replaying traffic]]. + * `--input-file` - accepts file which previously was recorded using `--output-file`. More about [[Saving and Replaying from file]] + * `--input-tcp` - used by Gor aggregation instance if you decided forward traffic from multiple forwarder Gor instances to it. Read about using [[Aggregator-forwarder setup]]. + +Available outputs: + * `--output-http` - replay HTTP traffic to given endpoint, accepts base url. Read [more about it](Replaying HTTP traffic) + * `--output-file` - records incoming traffic to the file. More about [[Saving and Replaying from file]] + * `--output-tcp` - forward incoming data to another Gor instance, used in conjunction with `--input-tcp`. Read more about [[Aggregator-forwarder setup]]. + * `--output-stdout` - used for debugging, outputs all data to stdout. \ No newline at end of file diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md new file mode 100644 index 0000000..e473071 --- /dev/null +++ b/docs/Troubleshooting.md @@ -0,0 +1,90 @@ +Gor can report stats on the `output-tcp` and `output-http` request queues. Stats are reported to the console every 5 seconds in the form `latest,mean,max,count,count/second` by using the `--output-http-stats` and `--output-tcp-stats` options. + +Examples: + +``` +2014/04/23 21:17:50 output_tcp:latest,mean,max,count,count/second +2014/04/23 21:17:50 output_tcp:0,0,0,0,0 +2014/04/23 21:17:55 output_tcp:1,1,2,68,13 +2014/04/23 21:18:00 output_tcp:1,1,2,92,18 +2014/04/23 21:18:05 output_tcp:1,1,2,119,23 +``` + +``` +2014/04/23 21:19:46 output_http:latest,mean,max,count,count/second +2014/04/23 21:19:46 output_http:0,0,0,0,0 +2014/04/23 21:19:51 output_http:0,0,0,0,0 +2014/04/23 21:19:56 output_http:0,0,0,0,0 +2014/04/23 21:20:01 output_http:1,0,1,50,10 +2014/04/23 21:20:06 output_http:1,1,4,72,14 +2014/04/23 21:20:11 output_http:1,0,1,179,35 +2014/04/23 21:20:16 output_http:1,0,1,148,29 +2014/04/23 21:20:21 output_http:1,1,2,91,18 +2014/04/23 21:20:26 output_http:1,1,2,150,30 +2014/04/23 21:18:15 output_http:100,99,100,70,14 +2014/04/23 21:18:21 output_http:100,99,100,55,11 +``` + +### How can I tell if I have bottlenecks? +Key areas that sometimes experience bottlenecks are the output-tcp and output-http functions which have internal queues for requests. Each queue has an upper limit of 100. Enable stats reporting to see if any queues are experiencing bottleneck behavior. + +#### Output HTTP bottlenecks +When running a Gor replay the output-http feature may bottleneck if: + + * the replay has inadequate bandwidth. If the replay is receiving or sending more messages than its network adapter can handle the output-http-stats may report that the output-http queue is filling up. See if there is a way to upgrade the replay's bandwidth. + * with `--output-http-workers` set to anything other than `-1` the `-output-http` target is unable to respond to messages in a timely manner. The http output workers which take messages off the output-http queue, process the request, and ensure that the request did not result in an error may not be able to keep up with the number of incoming requests. If the replay is not using dynamic worker scaling (`--output-http-workers=-1`) The optimal number of output-http-workers can be determined with the formula `output-workers = (Average number of requests per second)/(Average target response time per second)`. + +#### Output TCP bottlenecks +When using the Gor listener the output-tcp feature may bottleneck if: + + * the replay is unable to accept and process more requests than the listener is able generate. Prior to troubleshooting the output-tcp bottleneck, ensure that the replay target is not experiencing any bottlenecks. + * the replay target has inadequate bandwidth to handle all its incoming requests. If a replay target's incoming bandwidth is maxed out the output-tcp-stats may report that the output-tcp queue is filling up. See if there is a way to upgrade the replay's bandwidth. + + +#### Tuning + +To achieve the top most performance you should tune the source server system limits: + + net.ipv4.tcp_max_tw_buckets = 65536 + net.ipv4.tcp_tw_recycle = 1 + net.ipv4.tcp_tw_reuse = 0 + net.ipv4.tcp_max_syn_backlog = 131072 + net.ipv4.tcp_syn_retries = 3 + net.ipv4.tcp_synack_retries = 3 + net.ipv4.tcp_retries1 = 3 + net.ipv4.tcp_retries2 = 8 + net.ipv4.tcp_rmem = 16384 174760 349520 + net.ipv4.tcp_wmem = 16384 131072 262144 + net.ipv4.tcp_mem = 262144 524288 1048576 + net.ipv4.tcp_max_orphans = 65536 + net.ipv4.tcp_fin_timeout = 10 + net.ipv4.tcp_low_latency = 1 + net.ipv4.tcp_syncookies = 0 +*** + +### Gor is crashing with following stacktrace +``` +fatal error: unexpected signal during runtime execution +[signal 0xb code=0x1 addr=0x63 pc=0x7ffcdfdf8b2c] + +runtime stack: +runtime.throw(0xad8380, 0x2a) + /usr/local/go/src/runtime/panic.go:547 +0x90 +runtime.sigpanic() + /usr/local/go/src/runtime/sigpanic_unix.go:12 +0x5a + +goroutine 103 [syscall, locked to thread]: +runtime.cgocall(0x7b35a0, 0xc82121f1e8, 0x0) + /usr/local/go/src/runtime/cgocall.go:123 +0x11b fp=0xc82121f188 sp=0xc82121f158 +net._C2func_getaddrinfo(0x7ffcec0008c0, 0x0, 0xc821b221e0, 0xc8217b2b18, 0x0, 0x0, 0x0) + ??:0 +0x55 fp=0xc82121f1e8 sp=0xc82121f188 +net.cgoLookupIPCNAME(0x7fffb17208ab, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb17200) +``` + +There is a chance that you hit Go bug. The crash comes from the CGO version of DNS resolver. +By default Go based version used, but ins some cases [it switches to CGO based](https://golang.org/pkg/net/#hdr-Name_Resolution). It is possible to force Go based DNS resolver using GODEBUG environment variable: +`sudo GODEBUG="netdns=go" ./gor --input-raw :80 --output-http staging.env` + + + +Also, see [[FAQ]] \ No newline at end of file diff --git a/docs/[PRO]-Recording-and-replaying-keep-alive-TCP-sessions.md b/docs/[PRO]-Recording-and-replaying-keep-alive-TCP-sessions.md new file mode 100644 index 0000000..cea7702 --- /dev/null +++ b/docs/[PRO]-Recording-and-replaying-keep-alive-TCP-sessions.md @@ -0,0 +1,11 @@ +> **This feature available only in PRO version. See https://goreplay.org/pro.html for details.** + +By default, GoReplay does not guarantee that when you record keep-alive TCP session, it will be replayed in the same TCP connection as well. This is ok for most of the cases, but it does not give an accurate number of TCP sessions while replaying, also may cause issues if your application state depends on TCP session (do not mess with HTTP session). + +[GoReplay PRO](https://goreplay.org/pro.html) extension adds support for accurate recording and replaying of keep-alive TCP sessions. Separate connection to your server is created per original session and it makes benchmarks and tests incredibly accurate. To enable session recognition you just need to pass `--recognize-tcp-sessions` option. + +``` +gor --input-raw :80 --recognize-tcp-sessions --output-http http://test.target +``` + +Note that enabling this option also change algorithm of distributing traffic when using `--split-output`, see [Distributed configuration]. \ No newline at end of file diff --git a/docs/[PRO]-Replaying-Binary-protocols.md b/docs/[PRO]-Replaying-Binary-protocols.md new file mode 100644 index 0000000..7709d9a --- /dev/null +++ b/docs/[PRO]-Replaying-Binary-protocols.md @@ -0,0 +1,14 @@ +> **This feature available only in PRO version. See https://goreplay.org/pro.html for details.** + +Gor includes basic support for working with binary formats like `thrift` or `protocol-buffers`. To start set `--input-raw-protocol` to 'binary' (by default 'http'). For replaying, you should use `--output-binary`, example: + +``` +gor --input-raw :80 --input-raw-protocol binary --output-binary staging:8081 +``` + +While working with `--input-raw` you may notice a 2-second delay before messages are emitted to the outputs. This behaviour is expected and happening because for general binary protocol it is impossible to know when TCP message ends, so Gor has to set inactivity timeout. Each protocol has own rules (for example write message length as first bytes), and require individual handling to know message length. We consider improving detailed protocol support for `thrift`, `protocol-buffer` and etc. + +Note that you can use all load testing features for binary protocols. For example, the following command will loop and replay recorded payload on 10x speed for 30 seconds: +``` +gor --input-file './binary*.gor|1000%' --output-binary staging:9091 --input-file-loop --exit-after 30s +``` \ No newline at end of file diff --git a/docs/_Footer.md b/docs/_Footer.md new file mode 100644 index 0000000..3b97cd2 --- /dev/null +++ b/docs/_Footer.md @@ -0,0 +1 @@ +[Website](https://goreplay.org) | [PRO version](https://goreplay.org/pro.html) | [[Getting started]] | [[FAQ]] | [Join newsletter](https://www.getdrip.com/forms/89690474/submissions/new) \ No newline at end of file From ea8d3f7299fdd7ab25f703404e3a6b7aa42e496a Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Thu, 5 Jan 2017 20:21:20 +0300 Subject: [PATCH 12/19] Set theme jekyll-theme-cayman --- docs/_config.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/_config.yml diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..c419263 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file From 17c1abfc5e5cf69979da0ffaba617080b7e36a69 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Thu, 5 Jan 2017 20:25:09 +0300 Subject: [PATCH 13/19] Create CNAME --- docs/CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/CNAME diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 0000000..d8d891f --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +docs.goreplay.org \ No newline at end of file From a519c09770b1ea0c3f6b0ad04cf99b6f47af73c6 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Thu, 5 Jan 2017 20:28:45 +0300 Subject: [PATCH 14/19] Set theme jekyll-theme-slate --- docs/_config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_config.yml b/docs/_config.yml index c419263..c741881 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1 +1 @@ -theme: jekyll-theme-cayman \ No newline at end of file +theme: jekyll-theme-slate \ No newline at end of file From e5935a001de74c4800b635948b1f1c865ad2cdcb Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Thu, 5 Jan 2017 20:31:56 +0300 Subject: [PATCH 15/19] Set theme jekyll-theme-cayman --- docs/_config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_config.yml b/docs/_config.yml index c741881..c419263 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1 +1 @@ -theme: jekyll-theme-slate \ No newline at end of file +theme: jekyll-theme-cayman \ No newline at end of file From 8421ddb6ffc23d2ba8299e6af07baf401d1981f7 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Thu, 5 Jan 2017 20:35:21 +0300 Subject: [PATCH 16/19] Rename Home.md to index.md --- docs/{Home.md => index.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/{Home.md => index.md} (95%) diff --git a/docs/Home.md b/docs/index.md similarity index 95% rename from docs/Home.md rename to docs/index.md index 88a7bc9..1539924 100644 --- a/docs/Home.md +++ b/docs/index.md @@ -26,4 +26,4 @@ Read for more info: * [[Commercial collaboration]] -Next: [Getting Started](Getting-Started) \ No newline at end of file +Next: [Getting Started](Getting-Started) From 3b51158163592fb28f22f6cd1a10630e5a4f5d80 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 20 Jan 2017 16:09:00 +0300 Subject: [PATCH 17/19] continue work on new docs --- .request | 5 + docs/Home.md | 29 - docs/Request-rewriting.md | 2 +- docs/The-Basics.md | 17 - .../collaboration.md} | 0 docs/{Commercial-FAQ.md => commercial/faq.md} | 0 .../support.md} | 0 docs/css/breadcrumbs.css | 39 + docs/css/code.css | 355 ++ docs/css/fabric.css | 3442 +++++++++++++++++ docs/css/goreplay.css | 171 + docs/css/sidenav.css | 435 +++ docs/getting-started/basics.md | 17 + .../tutorial.md} | 2 +- docs/index.md | 33 + docs/js/base.js | 7 + docs/js/turbolinks.js | 6 + ...-and-replaying-keep-alive-tcp-sessions.md} | 0 .../replaying-binary-protocols.md} | 0 mkdocs.yml | 2 + sidenav.css | 435 +++ site/.gitignore | 3 + site/Gemfile | 24 + site/Gemfile.lock | 56 + site/_config.yml | 34 + .../2017-01-06-welcome-to-jekyll.markdown | 25 + site/about.md | 15 + site/index.md | 6 + 28 files changed, 5112 insertions(+), 48 deletions(-) create mode 100644 .request delete mode 100644 docs/Home.md delete mode 100644 docs/The-Basics.md rename docs/{Commercial-collaboration.md => commercial/collaboration.md} (100%) rename docs/{Commercial-FAQ.md => commercial/faq.md} (100%) rename docs/{Commercial-Support.md => commercial/support.md} (100%) create mode 100644 docs/css/breadcrumbs.css create mode 100644 docs/css/code.css create mode 100644 docs/css/fabric.css create mode 100644 docs/css/goreplay.css create mode 100644 docs/css/sidenav.css create mode 100644 docs/getting-started/basics.md rename docs/{Getting-Started.md => getting-started/tutorial.md} (96%) create mode 100644 docs/index.md create mode 100644 docs/js/base.js create mode 100644 docs/js/turbolinks.js rename docs/{[PRO]-Recording-and-replaying-keep-alive-TCP-sessions.md => pro/recording-and-replaying-keep-alive-tcp-sessions.md} (100%) rename docs/{[PRO]-Replaying-Binary-protocols.md => pro/replaying-binary-protocols.md} (100%) create mode 100644 mkdocs.yml create mode 100644 sidenav.css create mode 100644 site/.gitignore create mode 100644 site/Gemfile create mode 100644 site/Gemfile.lock create mode 100644 site/_config.yml create mode 100644 site/_posts/2017-01-06-welcome-to-jekyll.markdown create mode 100644 site/about.md create mode 100644 site/index.md diff --git a/.request b/.request new file mode 100644 index 0000000..ed6250c --- /dev/null +++ b/.request @@ -0,0 +1,5 @@ +POST /post HTTP/1.1 +Content-Length: 7 +Host: www.w3.org + +a=1&b=2 diff --git a/docs/Home.md b/docs/Home.md deleted file mode 100644 index 88a7bc9..0000000 --- a/docs/Home.md +++ /dev/null @@ -1,29 +0,0 @@ -Gor is an open-source tool for capturing and replaying live HTTP traffic into a test environment in order to continuously test your system with real data. It can be used to increase confidence in code deployments, configuration changes and infrastructure changes. - -Read for more info: - -* [[Getting Started]] -* [[The Basics]] -* [[Capturing and replaying traffic]] -* [[Replaying HTTP traffic]] -* [[[PRO] Replaying Binary protocols]] -* [[[PRO] Recording and replaying keep alive TCP sessions]] -* [[Saving and Replaying from file]] -* [Performance testing](https://github.com/buger/gor/wiki/Saving-and-Replaying-from-file#performance-testing) -* [[Rate limiting]] -* [[Request filtering]] -* [[Request rewriting]] -* [[Middleware]] -* [[Distributed configuration]] -* [[Exporting to ElasticSearch]] -* [[FAQ]] -* [[Troubleshooting]] - -## Commercial Aspects - -* [[Commercial Support]] -* [[Commercial FAQ]] -* [[Commercial collaboration]] - - -Next: [Getting Started](Getting-Started) \ No newline at end of file diff --git a/docs/Request-rewriting.md b/docs/Request-rewriting.md index adc68a4..825610b 100644 --- a/docs/Request-rewriting.md +++ b/docs/Request-rewriting.md @@ -2,7 +2,7 @@ Gor supports rewriting of URLs, URL params and headers, see below. Rewriting may be useful if you test environment does not have the same data as your production, and you want to perform all actions in the context of `test` user: for example rewrite all API tokens to some test value. Other possible use cases are toggling features on/off using custom headers or rewriting URL's if they changed in the new environment. -For more complex logic you can use [[Middleware]]. +For more complex logic you can use [Middleware](middleware.md). #### Rewrite URL based on a mapping `--http-rewrite-url` expects value in ":" format: ":" is a dilimiter. In `` section you may use captured regexp group values. This works similar to `replace` method in Javascript or `gsub` in Ruby. diff --git a/docs/The-Basics.md b/docs/The-Basics.md deleted file mode 100644 index 799c48c..0000000 --- a/docs/The-Basics.md +++ /dev/null @@ -1,17 +0,0 @@ -### Overview -Gor architecture tries to follow UNIX philosophy: everything made of pipes, various inputs multiplexing data to outputs. - -You can [rate limit](Rate limiting), [filter](Request filtering), [rewrite](Request rewriting) requests or even use your own [middleware](Middleware) to implement custom logic. Also, it is possible to replay requests at the higher rate for [load testing](Saving and Replaying from file). - -### Available input and output plugins - -Available inputs: - * `--input-raw` - used to capture HTTP traffic, you should specify IP address or interface and application port. More about [[Capturing and replaying traffic]]. - * `--input-file` - accepts file which previously was recorded using `--output-file`. More about [[Saving and Replaying from file]] - * `--input-tcp` - used by Gor aggregation instance if you decided forward traffic from multiple forwarder Gor instances to it. Read about using [[Aggregator-forwarder setup]]. - -Available outputs: - * `--output-http` - replay HTTP traffic to given endpoint, accepts base url. Read [more about it](Replaying HTTP traffic) - * `--output-file` - records incoming traffic to the file. More about [[Saving and Replaying from file]] - * `--output-tcp` - forward incoming data to another Gor instance, used in conjunction with `--input-tcp`. Read more about [[Aggregator-forwarder setup]]. - * `--output-stdout` - used for debugging, outputs all data to stdout. \ No newline at end of file diff --git a/docs/Commercial-collaboration.md b/docs/commercial/collaboration.md similarity index 100% rename from docs/Commercial-collaboration.md rename to docs/commercial/collaboration.md diff --git a/docs/Commercial-FAQ.md b/docs/commercial/faq.md similarity index 100% rename from docs/Commercial-FAQ.md rename to docs/commercial/faq.md diff --git a/docs/Commercial-Support.md b/docs/commercial/support.md similarity index 100% rename from docs/Commercial-Support.md rename to docs/commercial/support.md diff --git a/docs/css/breadcrumbs.css b/docs/css/breadcrumbs.css new file mode 100644 index 0000000..16cd8cf --- /dev/null +++ b/docs/css/breadcrumbs.css @@ -0,0 +1,39 @@ +.wy-breadcrumbs li { + display: inline-block; +} + +.wy-breadcrumbs li.wy-breadcrumbs-aside { + float: right; +} + +.wy-breadcrumbs li a { + display: inline-block; + padding: 5px; +} + +.wy-breadcrumbs li a:first-child { + padding-left: 0; +} + +.wy-breadcrumbs-extra { + margin-bottom: 0; + color: #b3b3b3; + font-size: 80%; + display: inline-block; +} + +@media screen and (max-width: 480px) { + .wy-breadcrumbs-extra { + display: none; + } + + .wy-breadcrumbs li.wy-breadcrumbs-aside { + display: none; + } +} + +@media print { + .wy-breadcrumbs li.wy-breadcrumbs-aside { + display: none; + } +} \ No newline at end of file diff --git a/docs/css/code.css b/docs/css/code.css new file mode 100644 index 0000000..a8a920d --- /dev/null +++ b/docs/css/code.css @@ -0,0 +1,355 @@ +.codeblock-example { + border: 1px solid #e1e4e5; + border-bottom: none; + padding: 24px; + padding-top: 48px; + font-weight: 500; + background: #fff; + position: relative; +} + +.codeblock-example:after { + content: "Example"; + position: absolute; + top: 0; + left: 0; + background: #9B59B6; + color: #fff; + padding: 6px 12px; +} + +.codeblock-example.prettyprint-example-only { + border: 1px solid #e1e4e5; + margin-bottom: 24px; +} + +.codeblock, +pre.literal-block, +.rst-content .literal-block, +.rst-content pre.literal-block, +div[class^='highlight'] { + border: 1px solid #e1e4e5; + padding: 0; + overflow-x: auto; + background: #fff; + margin: 1px 0 24px; +} + +.codeblock div[class^='highlight'], +pre.literal-block div[class^='highlight'], +.rst-content .literal-block div[class^='highlight'], +div[class^='highlight'] div[class^='highlight'] { + border: none; + background: none; + margin: 0; +} + +div[class^='highlight'] td.code { + width: 100%; +} + +.linenodiv pre { + border-right: solid 1px #e6e9ea; + margin: 0; + padding: 12px; + font-family: "Source Code Pro","Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace; + font-size: 12px; + line-height: 1.5; + color: #d9d9d9; +} + +div[class^='highlight'] pre { + white-space: pre; + margin: 0; + padding: 12px; + font-family: "Source Code Pro","Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace; + font-size: 12px; + line-height: 1.5; + display: block; + overflow: auto; + color: #404040; +} + +@media print { + .codeblock, + pre.literal-block, + .rst-content .literal-block, + .rst-content pre.literal-block, + div[class^='highlight'], + div[class^='highlight'] pre { + white-space: pre-wrap; + } + +} + +.hll { + background-color: #ffc; + margin: 0 -12px; + padding: 0 12px; + display: block; +} + +.c { + color: #998; + font-style: italic; +} + +.err { + color: #a61717; + background-color: #e3d2d2; +} + +.k { + font-weight: 700; +} + +.o { + font-weight: 700; +} + +.cm { + color: #998; + font-style: italic; +} + +.cp { + color: #999; + font-weight: 700; +} + +.c1 { + color: #998; + font-style: italic; +} + +.cs { + color: #999; + font-weight: 700; + font-style: italic; +} + +.gd { + color: #000; + background-color: #fdd; +} + +.gd .x { + color: #000; + background-color: #faa; +} + +.ge { + font-style: italic; +} + +.gr { + color: #a00 +} + +.gh { + color: #999; +} + +.gi { + color: #000; + background-color: #dfd; +} + +.gi .x { + color: #000; + background-color: #afa; +} + +.go { + color: #888; +} + +.gp { + color: #555; +} + +.gs { + font-weight: 700; +} + +.gu { + color: purple; + font-weight: 700; +} + +.gt { + color: #a00; +} + +.kc { + font-weight: 700; +} + +.kd { + font-weight: 700; +} + +.kn { + font-weight: 700; +} + +.kp { + font-weight: 700; +} + +.kr { + font-weight: 700; +} + +.kt { + color: #458; + font-weight: 700; +} + +.m { + color: #099; +} + +.s { + color: #d14; +} + +.n { + color: #333; +} + +.na { + color: teal; +} + +.nb { + color: #0086b3; +} + +.nc { + color: #458; + font-weight: 700; +} + +.no { + color: teal; +} + +.ni { + color: purple; +} + +.ne { + color: #900; + font-weight: 700; +} + +.nf { + color: #900; + font-weight: 700; +} + +.nn { + color: #555; +} + +.nt { + color: navy; +} + +.nv { + color: teal; +} + +.ow { + font-weight: 700; +} + +.w { + color: #bbb; +} + +.mf { + color: #099; +} + +.mh { + color: #099; +} + +.mi { + color: #099; +} + +.mo { + color: #099; +} + +.sb { + color: #d14; +} + +.sc { + color: #d14; +} + +.sd { + color: #d14; +} + +.s2 { + color: #d14 +} + +.se { + color: #d14; +} + +.sh { + color: #d14; +} + +.si { + color: #d14; +} + +.sx { + color: #d14; +} + +.sr { + color: #009926; +} + +.s1 { + color: #d14; +} + +.ss { + color: #990073; +} + +.bp { + color: #999; +} + +.vc { + color: teal; +} + +.vg { + color: teal; +} + +.vi { + color: teal; +} + +.il { + color: #099; +} + +.gc { + color: #999; + background-color: #EAF2F5; +} \ No newline at end of file diff --git a/docs/css/fabric.css b/docs/css/fabric.css new file mode 100644 index 0000000..6cc8e5e --- /dev/null +++ b/docs/css/fabric.css @@ -0,0 +1,3442 @@ +/* Taken from https://docs.fabric.io/apple/fabric/overview.html */ + +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block +} + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1 +} + +audio:not([controls]) { + display: none +} + +[hidden] { + display: none +} + +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100% +} + +body { + margin: 0 +} + +a:hover, +a:active { + outline: 0 +} + +abbr[title] { + border-bottom: 1px dotted +} + +b, +strong { + font-weight: 700 +} + +blockquote { + margin: 0 +} + +dfn { + font-style: italic +} + +ins { + background: #ff9; + color: #000; + text-decoration: none +} + +mark { + background: #ff0; + color: #000; + font-style: italic; + font-weight: 700 +} + +pre, +code, +.rst-content tt, +.rst-content code, +kbd, +samp { + font-family: monospace,serif; + _font-family: "courier new",monospace; + font-size: 1em +} + +pre { + white-space: pre +} + +q { + quotes: none +} + +q:before, +q:after { + content: ""; + content: none +} + +small { + font-size: 85% +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline +} + +sup { + top: -.5em +} + +sub { + bottom: -.25em +} + +ul, +ol, +dl { + margin: 0; + padding: 0; + list-style: none; + list-style-image: none +} + +li { + list-style: none +} + +dd { + margin: 0 +} + +img { + border: 0; + -ms-interpolation-mode: bicubic; + vertical-align: middle; + max-width: 100% +} + +svg:not(:root) { + overflow: hidden +} + +figure { + margin: 0 +} + +form { + margin: 0 +} + +fieldset { + border: 0; + margin: 0; + padding: 0 +} + +label { + cursor: pointer +} + +legend { + border: 0; + *margin-left: -7px; + padding: 0; + white-space: normal +} + +button, +input, +select, +textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle +} + +button, +input { + line-height: normal +} + +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; + *overflow: visible +} + +button[disabled], +input[disabled] { + cursor: default +} + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; + *width: 13px; + *height: 13px +} + +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0 +} + +textarea { + overflow: auto; + vertical-align: top; + resize: vertical +} + +table { + border-collapse: collapse; + border-spacing: 0 +} + +td { + vertical-align: top +} + +.chromeframe { + margin: .2em 0; + background: #ccc; + color: #000; + padding: .2em 0 +} + +.ir { + display: block; + border: 0; + text-indent: -999em; + overflow: hidden; + background-color: transparent; + background-repeat: no-repeat; + text-align: left; + direction: ltr; + *line-height: 0 +} + +.ir br { + display: none +} + +.hidden { + display: none!important; + visibility: hidden +} + +.visuallyhidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px +} + +.visuallyhidden.focusable:active, +.visuallyhidden.focusable:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto +} + +.invisible { + visibility: hidden +} + +.relative { + position: relative +} + +big, +small { + font-size: 100% +} + +@media print { + html, + body, + section { + background: none!important + } + + * { + box-shadow: none!important; + text-shadow: none!important; + filter: none!important; + -ms-filter: none!important + } + + a, + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: "" + } + + pre, + blockquote { + page-break-inside: avoid + } + + thead { + display: table-header-group + } + + tr, + img { + page-break-inside: avoid + } + + img { + max-width: 100%!important + } + + @page { + margin: .5cm + } + + p, + h2, + h3 { + orphans: 3; + widows: 3 + } + + h2, + h3 { + page-break-after: avoid + } + +} + +.fa:before, +.wy-menu-vertical li span.toctree-expand:before, +.wy-menu-vertical li.on a span.toctree-expand:before, +.wy-menu-vertical li.current>a span.toctree-expand:before, +.rst-content .admonition-title:before, +.rst-content h1 .headerlink:before, +.rst-content h2 .headerlink:before, +.rst-content h3 .headerlink:before, +.rst-content h4 .headerlink:before, +.rst-content h5 .headerlink:before, +.rst-content h6 .headerlink:before, +.rst-content dl dt .headerlink:before, +.rst-content p.caption .headerlink:before, +.rst-content tt.download span:first-child:before, +.rst-content code.download span:first-child:before, +.icon:before, +.wy-dropdown .caret:before, +.wy-inline-validate.wy-inline-validate-success .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-info .wy-input-context:before, +.wy-alert, +.rst-content .note, +.rst-content .attention, +.rst-content .caution, +.rst-content .danger, +.rst-content .error, +.rst-content .hint, +.rst-content .important, +.rst-content .tip, +.rst-content .warning, +.rst-content .seealso, +.rst-content .admonition-todo, +.btn, +input[type="text"], +input[type="password"], +input[type="email"], +input[type="url"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="datetime"], +input[type="datetime-local"], +input[type="week"], +input[type="number"], +input[type="search"], +input[type="tel"], +input[type="color"], +select, +textarea, +.wy-menu-vertical li.on a, +.wy-menu-vertical li.current>a, +.wy-side-nav-search>a, +.wy-side-nav-search .wy-dropdown>a, +.wy-nav-top a { + -webkit-font-smoothing: antialiased +} + +.clearfix { + *zoom: 1 +} + +.clearfix:before, +.clearfix:after { + display: table; + content: "" +} + +.clearfix:after { + clear: both +} + +@font-face { + font-family: 'FontAwesome'; + src: url(../fonts/fontawesome-webfont.eot?v=4.2.0); + src: url(../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0) format("embedded-opentype"),url(../fonts/fontawesome-webfont.woff?v=4.2.0) format("woff"),url(../fonts/fontawesome-webfont.ttf?v=4.2.0) format("truetype"),url(../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular) format("svg"); + font-weight: 400; + font-style: normal +} + +.fa, +.wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand, +.rst-content .admonition-title, +.rst-content h1 .headerlink, +.rst-content h2 .headerlink, +.rst-content h3 .headerlink, +.rst-content h4 .headerlink, +.rst-content h5 .headerlink, +.rst-content h6 .headerlink, +.rst-content dl dt .headerlink, +.rst-content p.caption .headerlink, +.rst-content tt.download span:first-child, +.rst-content code.download span:first-child, +.icon { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} + +.fa-lg { + font-size: 1.33333em; + line-height: .75em; + vertical-align: -15% +} + +.fa-2x { + font-size: 2em +} + +.fa-3x { + font-size: 3em +} + +.fa-4x { + font-size: 4em +} + +.fa-5x { + font-size: 5em +} + +.fa-fw { + width: 1.28571em; + text-align: center +} + +.fa-ul { + padding-left: 0; + margin-left: 2.14286em; + list-style-type: none +} + +.fa-ul>li { + position: relative +} + +.fa-li { + position: absolute; + left: -2.14286em; + width: 2.14286em; + top: .14286em; + text-align: center +} + +.fa-li.fa-lg { + left: -1.85714em +} + +.fa-border { + padding: .2em .25em .15em; + border: solid .08em #eee; + border-radius: .1em +} + +.pull-right { + float: right +} + +.pull-left { + float: left +} + +.fa.pull-left, +.wy-menu-vertical li span.pull-left.toctree-expand, +.wy-menu-vertical li.on a span.pull-left.toctree-expand, +.wy-menu-vertical li.current>a span.pull-left.toctree-expand, +.rst-content .pull-left.admonition-title, +.rst-content h1 .pull-left.headerlink, +.rst-content h2 .pull-left.headerlink, +.rst-content h3 .pull-left.headerlink, +.rst-content h4 .pull-left.headerlink, +.rst-content h5 .pull-left.headerlink, +.rst-content h6 .pull-left.headerlink, +.rst-content dl dt .pull-left.headerlink, +.rst-content p.caption .pull-left.headerlink, +.rst-content tt.download span.pull-left:first-child, +.rst-content code.download span.pull-left:first-child, +.pull-left.icon { + margin-right: .3em +} + +.fa.pull-right, +.wy-menu-vertical li span.pull-right.toctree-expand, +.wy-menu-vertical li.on a span.pull-right.toctree-expand, +.wy-menu-vertical li.current>a span.pull-right.toctree-expand, +.rst-content .pull-right.admonition-title, +.rst-content h1 .pull-right.headerlink, +.rst-content h2 .pull-right.headerlink, +.rst-content h3 .pull-right.headerlink, +.rst-content h4 .pull-right.headerlink, +.rst-content h5 .pull-right.headerlink, +.rst-content h6 .pull-right.headerlink, +.rst-content dl dt .pull-right.headerlink, +.rst-content p.caption .pull-right.headerlink, +.rst-content tt.download span.pull-right:first-child, +.rst-content code.download span.pull-right:first-child, +.pull-right.icon { + margin-left: .3em +} + +.fa, +.wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand, +.rst-content .admonition-title, +.rst-content h1 .headerlink, +.rst-content h2 .headerlink, +.rst-content h3 .headerlink, +.rst-content h4 .headerlink, +.rst-content h5 .headerlink, +.rst-content h6 .headerlink, +.rst-content dl dt .headerlink, +.rst-content p.caption .headerlink, +.rst-content tt.download span:first-child, +.rst-content code.download span:first-child, +.icon, +.wy-dropdown .caret, +.wy-inline-validate.wy-inline-validate-success .wy-input-context, +.wy-inline-validate.wy-inline-validate-danger .wy-input-context, +.wy-inline-validate.wy-inline-validate-warning .wy-input-context, +.wy-inline-validate.wy-inline-validate-info .wy-input-context { + font-family: inherit +} + +.fa:before, +.wy-menu-vertical li span.toctree-expand:before, +.wy-menu-vertical li.on a span.toctree-expand:before, +.wy-menu-vertical li.current>a span.toctree-expand:before, +.rst-content .admonition-title:before, +.rst-content h1 .headerlink:before, +.rst-content h2 .headerlink:before, +.rst-content h3 .headerlink:before, +.rst-content h4 .headerlink:before, +.rst-content h5 .headerlink:before, +.rst-content h6 .headerlink:before, +.rst-content dl dt .headerlink:before, +.rst-content p.caption .headerlink:before, +.rst-content tt.download span:first-child:before, +.rst-content code.download span:first-child:before, +.icon:before, +.wy-dropdown .caret:before, +.wy-inline-validate.wy-inline-validate-success .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-info .wy-input-context:before { + font-family: "FontAwesome"; + display: inline-block; + font-style: normal; + font-weight: 400; + line-height: 1; + text-decoration: inherit +} + +a .fa, +a .wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li a span.toctree-expand, +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand, +a .rst-content .admonition-title, +.rst-content a .admonition-title, +a .rst-content h1 .headerlink, +.rst-content h1 a .headerlink, +a .rst-content h2 .headerlink, +.rst-content h2 a .headerlink, +a .rst-content h3 .headerlink, +.rst-content h3 a .headerlink, +a .rst-content h4 .headerlink, +.rst-content h4 a .headerlink, +a .rst-content h5 .headerlink, +.rst-content h5 a .headerlink, +a .rst-content h6 .headerlink, +.rst-content h6 a .headerlink, +a .rst-content dl dt .headerlink, +.rst-content dl dt a .headerlink, +a .rst-content p.caption .headerlink, +.rst-content p.caption a .headerlink, +a .rst-content tt.download span:first-child, +.rst-content tt.download a span:first-child, +a .rst-content code.download span:first-child, +.rst-content code.download a span:first-child, +a .icon { + display: inline-block; + text-decoration: inherit +} + +.btn .fa, +.btn .wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li .btn span.toctree-expand, +.btn .wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.on a .btn span.toctree-expand, +.btn .wy-menu-vertical li.current>a span.toctree-expand, +.wy-menu-vertical li.current>a .btn span.toctree-expand, +.btn .rst-content .admonition-title, +.rst-content .btn .admonition-title, +.btn .rst-content h1 .headerlink, +.rst-content h1 .btn .headerlink, +.btn .rst-content h2 .headerlink, +.rst-content h2 .btn .headerlink, +.btn .rst-content h3 .headerlink, +.rst-content h3 .btn .headerlink, +.btn .rst-content h4 .headerlink, +.rst-content h4 .btn .headerlink, +.btn .rst-content h5 .headerlink, +.rst-content h5 .btn .headerlink, +.btn .rst-content h6 .headerlink, +.rst-content h6 .btn .headerlink, +.btn .rst-content dl dt .headerlink, +.rst-content dl dt .btn .headerlink, +.btn .rst-content p.caption .headerlink, +.rst-content p.caption .btn .headerlink, +.btn .rst-content tt.download span:first-child, +.rst-content tt.download .btn span:first-child, +.btn .rst-content code.download span:first-child, +.rst-content code.download .btn span:first-child, +.btn .icon, +.nav .fa, +.nav .wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li .nav span.toctree-expand, +.nav .wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.on a .nav span.toctree-expand, +.nav .wy-menu-vertical li.current>a span.toctree-expand, +.wy-menu-vertical li.current>a .nav span.toctree-expand, +.nav .rst-content .admonition-title, +.rst-content .nav .admonition-title, +.nav .rst-content h1 .headerlink, +.rst-content h1 .nav .headerlink, +.nav .rst-content h2 .headerlink, +.rst-content h2 .nav .headerlink, +.nav .rst-content h3 .headerlink, +.rst-content h3 .nav .headerlink, +.nav .rst-content h4 .headerlink, +.rst-content h4 .nav .headerlink, +.nav .rst-content h5 .headerlink, +.rst-content h5 .nav .headerlink, +.nav .rst-content h6 .headerlink, +.rst-content h6 .nav .headerlink, +.nav .rst-content dl dt .headerlink, +.rst-content dl dt .nav .headerlink, +.nav .rst-content p.caption .headerlink, +.rst-content p.caption .nav .headerlink, +.nav .rst-content tt.download span:first-child, +.rst-content tt.download .nav span:first-child, +.nav .rst-content code.download span:first-child, +.rst-content code.download .nav span:first-child, +.nav .icon { + display: inline +} + +.btn .fa.fa-large, +.btn .wy-menu-vertical li span.fa-large.toctree-expand, +.wy-menu-vertical li .btn span.fa-large.toctree-expand, +.btn .rst-content .fa-large.admonition-title, +.rst-content .btn .fa-large.admonition-title, +.btn .rst-content h1 .fa-large.headerlink, +.rst-content h1 .btn .fa-large.headerlink, +.btn .rst-content h2 .fa-large.headerlink, +.rst-content h2 .btn .fa-large.headerlink, +.btn .rst-content h3 .fa-large.headerlink, +.rst-content h3 .btn .fa-large.headerlink, +.btn .rst-content h4 .fa-large.headerlink, +.rst-content h4 .btn .fa-large.headerlink, +.btn .rst-content h5 .fa-large.headerlink, +.rst-content h5 .btn .fa-large.headerlink, +.btn .rst-content h6 .fa-large.headerlink, +.rst-content h6 .btn .fa-large.headerlink, +.btn .rst-content dl dt .fa-large.headerlink, +.rst-content dl dt .btn .fa-large.headerlink, +.btn .rst-content p.caption .fa-large.headerlink, +.rst-content p.caption .btn .fa-large.headerlink, +.btn .rst-content tt.download span.fa-large:first-child, +.rst-content tt.download .btn span.fa-large:first-child, +.btn .rst-content code.download span.fa-large:first-child, +.rst-content code.download .btn span.fa-large:first-child, +.btn .fa-large.icon, +.nav .fa.fa-large, +.nav .wy-menu-vertical li span.fa-large.toctree-expand, +.wy-menu-vertical li .nav span.fa-large.toctree-expand, +.nav .rst-content .fa-large.admonition-title, +.rst-content .nav .fa-large.admonition-title, +.nav .rst-content h1 .fa-large.headerlink, +.rst-content h1 .nav .fa-large.headerlink, +.nav .rst-content h2 .fa-large.headerlink, +.rst-content h2 .nav .fa-large.headerlink, +.nav .rst-content h3 .fa-large.headerlink, +.rst-content h3 .nav .fa-large.headerlink, +.nav .rst-content h4 .fa-large.headerlink, +.rst-content h4 .nav .fa-large.headerlink, +.nav .rst-content h5 .fa-large.headerlink, +.rst-content h5 .nav .fa-large.headerlink, +.nav .rst-content h6 .fa-large.headerlink, +.rst-content h6 .nav .fa-large.headerlink, +.nav .rst-content dl dt .fa-large.headerlink, +.rst-content dl dt .nav .fa-large.headerlink, +.nav .rst-content p.caption .fa-large.headerlink, +.rst-content p.caption .nav .fa-large.headerlink, +.nav .rst-content tt.download span.fa-large:first-child, +.rst-content tt.download .nav span.fa-large:first-child, +.nav .rst-content code.download span.fa-large:first-child, +.rst-content code.download .nav span.fa-large:first-child, +.nav .fa-large.icon { + line-height: .9em +} + +.btn .fa.fa-spin, +.btn .wy-menu-vertical li span.fa-spin.toctree-expand, +.wy-menu-vertical li .btn span.fa-spin.toctree-expand, +.btn .rst-content .fa-spin.admonition-title, +.rst-content .btn .fa-spin.admonition-title, +.btn .rst-content h1 .fa-spin.headerlink, +.rst-content h1 .btn .fa-spin.headerlink, +.btn .rst-content h2 .fa-spin.headerlink, +.rst-content h2 .btn .fa-spin.headerlink, +.btn .rst-content h3 .fa-spin.headerlink, +.rst-content h3 .btn .fa-spin.headerlink, +.btn .rst-content h4 .fa-spin.headerlink, +.rst-content h4 .btn .fa-spin.headerlink, +.btn .rst-content h5 .fa-spin.headerlink, +.rst-content h5 .btn .fa-spin.headerlink, +.btn .rst-content h6 .fa-spin.headerlink, +.rst-content h6 .btn .fa-spin.headerlink, +.btn .rst-content dl dt .fa-spin.headerlink, +.rst-content dl dt .btn .fa-spin.headerlink, +.btn .rst-content p.caption .fa-spin.headerlink, +.rst-content p.caption .btn .fa-spin.headerlink, +.btn .rst-content tt.download span.fa-spin:first-child, +.rst-content tt.download .btn span.fa-spin:first-child, +.btn .rst-content code.download span.fa-spin:first-child, +.rst-content code.download .btn span.fa-spin:first-child, +.btn .fa-spin.icon, +.nav .fa.fa-spin, +.nav .wy-menu-vertical li span.fa-spin.toctree-expand, +.wy-menu-vertical li .nav span.fa-spin.toctree-expand, +.nav .rst-content .fa-spin.admonition-title, +.rst-content .nav .fa-spin.admonition-title, +.nav .rst-content h1 .fa-spin.headerlink, +.rst-content h1 .nav .fa-spin.headerlink, +.nav .rst-content h2 .fa-spin.headerlink, +.rst-content h2 .nav .fa-spin.headerlink, +.nav .rst-content h3 .fa-spin.headerlink, +.rst-content h3 .nav .fa-spin.headerlink, +.nav .rst-content h4 .fa-spin.headerlink, +.rst-content h4 .nav .fa-spin.headerlink, +.nav .rst-content h5 .fa-spin.headerlink, +.rst-content h5 .nav .fa-spin.headerlink, +.nav .rst-content h6 .fa-spin.headerlink, +.rst-content h6 .nav .fa-spin.headerlink, +.nav .rst-content dl dt .fa-spin.headerlink, +.rst-content dl dt .nav .fa-spin.headerlink, +.nav .rst-content p.caption .fa-spin.headerlink, +.rst-content p.caption .nav .fa-spin.headerlink, +.nav .rst-content tt.download span.fa-spin:first-child, +.rst-content tt.download .nav span.fa-spin:first-child, +.nav .rst-content code.download span.fa-spin:first-child, +.rst-content code.download .nav span.fa-spin:first-child, +.nav .fa-spin.icon { + display: inline-block +} + +.btn.fa:before, +.wy-menu-vertical li span.btn.toctree-expand:before, +.rst-content .btn.admonition-title:before, +.rst-content h1 .btn.headerlink:before, +.rst-content h2 .btn.headerlink:before, +.rst-content h3 .btn.headerlink:before, +.rst-content h4 .btn.headerlink:before, +.rst-content h5 .btn.headerlink:before, +.rst-content h6 .btn.headerlink:before, +.rst-content dl dt .btn.headerlink:before, +.rst-content p.caption .btn.headerlink:before, +.rst-content tt.download span.btn:first-child:before, +.rst-content code.download span.btn:first-child:before, +.btn.icon:before { + opacity: .5; + -webkit-transition: opacity .05s ease-in; + -moz-transition: opacity .05s ease-in; + transition: opacity .05s ease-in +} + +.btn.fa:hover:before, +.wy-menu-vertical li span.btn.toctree-expand:hover:before, +.rst-content .btn.admonition-title:hover:before, +.rst-content h1 .btn.headerlink:hover:before, +.rst-content h2 .btn.headerlink:hover:before, +.rst-content h3 .btn.headerlink:hover:before, +.rst-content h4 .btn.headerlink:hover:before, +.rst-content h5 .btn.headerlink:hover:before, +.rst-content h6 .btn.headerlink:hover:before, +.rst-content dl dt .btn.headerlink:hover:before, +.rst-content p.caption .btn.headerlink:hover:before, +.rst-content tt.download span.btn:first-child:hover:before, +.rst-content code.download span.btn:first-child:hover:before, +.btn.icon:hover:before { + opacity: 1 +} + +.btn-mini .fa:before, +.btn-mini .wy-menu-vertical li span.toctree-expand:before, +.wy-menu-vertical li .btn-mini span.toctree-expand:before, +.btn-mini .rst-content .admonition-title:before, +.rst-content .btn-mini .admonition-title:before, +.btn-mini .rst-content h1 .headerlink:before, +.rst-content h1 .btn-mini .headerlink:before, +.btn-mini .rst-content h2 .headerlink:before, +.rst-content h2 .btn-mini .headerlink:before, +.btn-mini .rst-content h3 .headerlink:before, +.rst-content h3 .btn-mini .headerlink:before, +.btn-mini .rst-content h4 .headerlink:before, +.rst-content h4 .btn-mini .headerlink:before, +.btn-mini .rst-content h5 .headerlink:before, +.rst-content h5 .btn-mini .headerlink:before, +.btn-mini .rst-content h6 .headerlink:before, +.rst-content h6 .btn-mini .headerlink:before, +.btn-mini .rst-content dl dt .headerlink:before, +.rst-content dl dt .btn-mini .headerlink:before, +.btn-mini .rst-content p.caption .headerlink:before, +.rst-content p.caption .btn-mini .headerlink:before, +.btn-mini .rst-content tt.download span:first-child:before, +.rst-content tt.download .btn-mini span:first-child:before, +.btn-mini .rst-content code.download span:first-child:before, +.rst-content code.download .btn-mini span:first-child:before, +.btn-mini .icon:before { + font-size: 14px; + vertical-align: -15% +} + +.wy-alert, +.rst-content .note, +.rst-content .attention, +.rst-content .caution, +.rst-content .danger, +.rst-content .error, +.rst-content .hint, +.rst-content .important, +.rst-content .tip, +.rst-content .warning, +.rst-content .seealso, +.rst-content .admonition-todo { + padding: 12px; + line-height: 24px; + margin-bottom: 24px; + background: #e7f2fa +} + +.wy-alert-title, +.rst-content .admonition-title { + color: #fff; + font-weight: 700; + display: block; + color: #fff; + background: #6ab0de; + margin: -12px; + padding: 6px 12px; + margin-bottom: 12px +} + +.wy-alert.wy-alert-danger, +.rst-content .wy-alert-danger.note, +.rst-content .wy-alert-danger.attention, +.rst-content .wy-alert-danger.caution, +.rst-content .danger, +.rst-content .error, +.rst-content .wy-alert-danger.hint, +.rst-content .wy-alert-danger.important, +.rst-content .wy-alert-danger.tip, +.rst-content .wy-alert-danger.warning, +.rst-content .wy-alert-danger.seealso, +.rst-content .wy-alert-danger.admonition-todo { + background: #fdf3f2 +} + +.wy-alert.wy-alert-danger .wy-alert-title, +.rst-content .wy-alert-danger.note .wy-alert-title, +.rst-content .wy-alert-danger.attention .wy-alert-title, +.rst-content .wy-alert-danger.caution .wy-alert-title, +.rst-content .danger .wy-alert-title, +.rst-content .error .wy-alert-title, +.rst-content .wy-alert-danger.hint .wy-alert-title, +.rst-content .wy-alert-danger.important .wy-alert-title, +.rst-content .wy-alert-danger.tip .wy-alert-title, +.rst-content .wy-alert-danger.warning .wy-alert-title, +.rst-content .wy-alert-danger.seealso .wy-alert-title, +.rst-content .wy-alert-danger.admonition-todo .wy-alert-title, +.wy-alert.wy-alert-danger .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-danger .admonition-title, +.rst-content .wy-alert-danger.note .admonition-title, +.rst-content .wy-alert-danger.attention .admonition-title, +.rst-content .wy-alert-danger.caution .admonition-title, +.rst-content .danger .admonition-title, +.rst-content .error .admonition-title, +.rst-content .wy-alert-danger.hint .admonition-title, +.rst-content .wy-alert-danger.important .admonition-title, +.rst-content .wy-alert-danger.tip .admonition-title, +.rst-content .wy-alert-danger.warning .admonition-title, +.rst-content .wy-alert-danger.seealso .admonition-title, +.rst-content .wy-alert-danger.admonition-todo .admonition-title { + background: #f29f97 +} + +.wy-alert.wy-alert-warning, +.rst-content .wy-alert-warning.note, +.rst-content .attention, +.rst-content .caution, +.rst-content .wy-alert-warning.danger, +.rst-content .wy-alert-warning.error, +.rst-content .wy-alert-warning.hint, +.rst-content .wy-alert-warning.important, +.rst-content .wy-alert-warning.tip, +.rst-content .warning, +.rst-content .wy-alert-warning.seealso, +.rst-content .admonition-todo { + background: #ffedcc +} + +.wy-alert.wy-alert-warning .wy-alert-title, +.rst-content .wy-alert-warning.note .wy-alert-title, +.rst-content .attention .wy-alert-title, +.rst-content .caution .wy-alert-title, +.rst-content .wy-alert-warning.danger .wy-alert-title, +.rst-content .wy-alert-warning.error .wy-alert-title, +.rst-content .wy-alert-warning.hint .wy-alert-title, +.rst-content .wy-alert-warning.important .wy-alert-title, +.rst-content .wy-alert-warning.tip .wy-alert-title, +.rst-content .warning .wy-alert-title, +.rst-content .wy-alert-warning.seealso .wy-alert-title, +.rst-content .admonition-todo .wy-alert-title, +.wy-alert.wy-alert-warning .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-warning .admonition-title, +.rst-content .wy-alert-warning.note .admonition-title, +.rst-content .attention .admonition-title, +.rst-content .caution .admonition-title, +.rst-content .wy-alert-warning.danger .admonition-title, +.rst-content .wy-alert-warning.error .admonition-title, +.rst-content .wy-alert-warning.hint .admonition-title, +.rst-content .wy-alert-warning.important .admonition-title, +.rst-content .wy-alert-warning.tip .admonition-title, +.rst-content .warning .admonition-title, +.rst-content .wy-alert-warning.seealso .admonition-title, +.rst-content .admonition-todo .admonition-title { + background: #f0b37e +} + +.wy-alert.wy-alert-info, +.rst-content .note, +.rst-content .wy-alert-info.attention, +.rst-content .wy-alert-info.caution, +.rst-content .wy-alert-info.danger, +.rst-content .wy-alert-info.error, +.rst-content .wy-alert-info.hint, +.rst-content .wy-alert-info.important, +.rst-content .wy-alert-info.tip, +.rst-content .wy-alert-info.warning, +.rst-content .seealso, +.rst-content .wy-alert-info.admonition-todo { + background: #e7f2fa +} + +.wy-alert.wy-alert-info .wy-alert-title, +.rst-content .note .wy-alert-title, +.rst-content .wy-alert-info.attention .wy-alert-title, +.rst-content .wy-alert-info.caution .wy-alert-title, +.rst-content .wy-alert-info.danger .wy-alert-title, +.rst-content .wy-alert-info.error .wy-alert-title, +.rst-content .wy-alert-info.hint .wy-alert-title, +.rst-content .wy-alert-info.important .wy-alert-title, +.rst-content .wy-alert-info.tip .wy-alert-title, +.rst-content .wy-alert-info.warning .wy-alert-title, +.rst-content .seealso .wy-alert-title, +.rst-content .wy-alert-info.admonition-todo .wy-alert-title, +.wy-alert.wy-alert-info .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-info .admonition-title, +.rst-content .note .admonition-title, +.rst-content .wy-alert-info.attention .admonition-title, +.rst-content .wy-alert-info.caution .admonition-title, +.rst-content .wy-alert-info.danger .admonition-title, +.rst-content .wy-alert-info.error .admonition-title, +.rst-content .wy-alert-info.hint .admonition-title, +.rst-content .wy-alert-info.important .admonition-title, +.rst-content .wy-alert-info.tip .admonition-title, +.rst-content .wy-alert-info.warning .admonition-title, +.rst-content .seealso .admonition-title, +.rst-content .wy-alert-info.admonition-todo .admonition-title { + background: #6ab0de +} + +.wy-alert.wy-alert-success, +.rst-content .wy-alert-success.note, +.rst-content .wy-alert-success.attention, +.rst-content .wy-alert-success.caution, +.rst-content .wy-alert-success.danger, +.rst-content .wy-alert-success.error, +.rst-content .hint, +.rst-content .important, +.rst-content .tip, +.rst-content .wy-alert-success.warning, +.rst-content .wy-alert-success.seealso, +.rst-content .wy-alert-success.admonition-todo { + background: #dbfaf4 +} + +.wy-alert.wy-alert-success .wy-alert-title, +.rst-content .wy-alert-success.note .wy-alert-title, +.rst-content .wy-alert-success.attention .wy-alert-title, +.rst-content .wy-alert-success.caution .wy-alert-title, +.rst-content .wy-alert-success.danger .wy-alert-title, +.rst-content .wy-alert-success.error .wy-alert-title, +.rst-content .hint .wy-alert-title, +.rst-content .important .wy-alert-title, +.rst-content .tip .wy-alert-title, +.rst-content .wy-alert-success.warning .wy-alert-title, +.rst-content .wy-alert-success.seealso .wy-alert-title, +.rst-content .wy-alert-success.admonition-todo .wy-alert-title, +.wy-alert.wy-alert-success .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-success .admonition-title, +.rst-content .wy-alert-success.note .admonition-title, +.rst-content .wy-alert-success.attention .admonition-title, +.rst-content .wy-alert-success.caution .admonition-title, +.rst-content .wy-alert-success.danger .admonition-title, +.rst-content .wy-alert-success.error .admonition-title, +.rst-content .hint .admonition-title, +.rst-content .important .admonition-title, +.rst-content .tip .admonition-title, +.rst-content .wy-alert-success.warning .admonition-title, +.rst-content .wy-alert-success.seealso .admonition-title, +.rst-content .wy-alert-success.admonition-todo .admonition-title { + background: #1abc9c +} + +.wy-alert.wy-alert-neutral, +.rst-content .wy-alert-neutral.note, +.rst-content .wy-alert-neutral.attention, +.rst-content .wy-alert-neutral.caution, +.rst-content .wy-alert-neutral.danger, +.rst-content .wy-alert-neutral.error, +.rst-content .wy-alert-neutral.hint, +.rst-content .wy-alert-neutral.important, +.rst-content .wy-alert-neutral.tip, +.rst-content .wy-alert-neutral.warning, +.rst-content .wy-alert-neutral.seealso, +.rst-content .wy-alert-neutral.admonition-todo { + background: #f3f6f6 +} + +.wy-alert.wy-alert-neutral .wy-alert-title, +.rst-content .wy-alert-neutral.note .wy-alert-title, +.rst-content .wy-alert-neutral.attention .wy-alert-title, +.rst-content .wy-alert-neutral.caution .wy-alert-title, +.rst-content .wy-alert-neutral.danger .wy-alert-title, +.rst-content .wy-alert-neutral.error .wy-alert-title, +.rst-content .wy-alert-neutral.hint .wy-alert-title, +.rst-content .wy-alert-neutral.important .wy-alert-title, +.rst-content .wy-alert-neutral.tip .wy-alert-title, +.rst-content .wy-alert-neutral.warning .wy-alert-title, +.rst-content .wy-alert-neutral.seealso .wy-alert-title, +.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title, +.wy-alert.wy-alert-neutral .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-neutral .admonition-title, +.rst-content .wy-alert-neutral.note .admonition-title, +.rst-content .wy-alert-neutral.attention .admonition-title, +.rst-content .wy-alert-neutral.caution .admonition-title, +.rst-content .wy-alert-neutral.danger .admonition-title, +.rst-content .wy-alert-neutral.error .admonition-title, +.rst-content .wy-alert-neutral.hint .admonition-title, +.rst-content .wy-alert-neutral.important .admonition-title, +.rst-content .wy-alert-neutral.tip .admonition-title, +.rst-content .wy-alert-neutral.warning .admonition-title, +.rst-content .wy-alert-neutral.seealso .admonition-title, +.rst-content .wy-alert-neutral.admonition-todo .admonition-title { + color: #404040; + background: #e1e4e5 +} + +.wy-alert.wy-alert-neutral a, +.rst-content .wy-alert-neutral.note a, +.rst-content .wy-alert-neutral.attention a, +.rst-content .wy-alert-neutral.caution a, +.rst-content .wy-alert-neutral.danger a, +.rst-content .wy-alert-neutral.error a, +.rst-content .wy-alert-neutral.hint a, +.rst-content .wy-alert-neutral.important a, +.rst-content .wy-alert-neutral.tip a, +.rst-content .wy-alert-neutral.warning a, +.rst-content .wy-alert-neutral.seealso a, +.rst-content .wy-alert-neutral.admonition-todo a { + color: #008BF3 +} + +.wy-alert p:last-child, +.rst-content .note p:last-child, +.rst-content .attention p:last-child, +.rst-content .caution p:last-child, +.rst-content .danger p:last-child, +.rst-content .error p:last-child, +.rst-content .hint p:last-child, +.rst-content .important p:last-child, +.rst-content .tip p:last-child, +.rst-content .warning p:last-child, +.rst-content .seealso p:last-child, +.rst-content .admonition-todo p:last-child { + margin-bottom: 0 +} + +.wy-tray-container { + position: fixed; + bottom: 0; + left: 0; + z-index: 600 +} + +.wy-tray-container li { + display: block; + width: 300px; + background: transparent; + color: #fff; + text-align: center; + box-shadow: 0 5px 5px 0 rgba(0,0,0,0.1); + padding: 0 24px; + min-width: 20%; + opacity: 0; + height: 0; + line-height: 56px; + overflow: hidden; + -webkit-transition: all .3s ease-in; + -moz-transition: all .3s ease-in; + transition: all .3s ease-in +} + +.wy-tray-container li.wy-tray-item-success { + background: #27AE60 +} + +.wy-tray-container li.wy-tray-item-info { + background: #2980B9 +} + +.wy-tray-container li.wy-tray-item-warning { + background: #E67E22 +} + +.wy-tray-container li.wy-tray-item-danger { + background: #E74C3C +} + +.wy-tray-container li.on { + opacity: 1; + height: 56px +} + +@media screen and (max-width: 768px) { + .wy-tray-container { + bottom: auto; + top: 0; + width: 100% + } + + .wy-tray-container li { + width: 100% + } + +} + +button { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle; + cursor: pointer; + line-height: normal; + -webkit-appearance: button; + *overflow: visible +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0 +} + +button[disabled] { + cursor: default +} + +.btn { + font-family: "Source Sans Pro","proxima-nova","Helvetica Neue",Arial,sans-serif; + display: inline-block; + padding: 10px 30px; + margin-bottom: 0; + font-weight: 400; + text-align: center; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + background-image: none; + background-color: #4a90e2; + border: 1px solid transparent; + white-space: nowrap; + color: rgba(255,255,255,0.95); + border-radius: 3px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + outline: none +} + +.btn-hover { + background: #2e8ece; + color: #fff +} + +.btn:disabled { + background-image: none; + filter: progid: DXImageTransform.Microsoft.gradient(enabled=false); + filter: alpha(opacity=40); + opacity: .4; + cursor: not-allowed; + box-shadow: none +} + +.btn-disabled { + background-image: none; + filter: progid: DXImageTransform.Microsoft.gradient(enabled=false); + filter: alpha(opacity=40); + opacity: .4; + cursor: not-allowed; + box-shadow: none +} + +.btn-disabled:hover, +.btn-disabled:focus, +.btn-disabled:active { + background-image: none; + filter: progid: DXImageTransform.Microsoft.gradient(enabled=false); + filter: alpha(opacity=40); + opacity: .4; + cursor: not-allowed; + box-shadow: none +} + +.btn::-moz-focus-inner { + padding: 0; + border: 0 +} + +.btn-small { + font-size: 80% +} + +.btn-info { + background-color: #2980B9!important +} + +.btn-info:hover { + background-color: #2e8ece!important +} + +.btn-neutral { + font-size: 14px; + color: #fff; + font-weight: 300; + /*background-color: #008bf3!important*/ +} + +.btn-success { + background-color: #27AE60!important +} + +.btn-success:hover { + background-color: #295!important +} + +.btn-danger { + background-color: #E74C3C!important +} + +.btn-danger:hover { + background-color: #ea6153!important +} + +.btn-warning { + background-color: #E67E22!important +} + +.btn-warning:hover { + background-color: #e98b39!important +} + +.btn-invert { + background-color: #222 +} + +.btn-invert:hover { + background-color: #2f2f2f!important +} + +.btn-link { + background-color: transparent!important; + color: #2980B9; + box-shadow: none; + border-color: transparent!important +} + +.btn-link:hover { + background-color: transparent!important; + color: #409ad5!important; + box-shadow: none +} + +.btn-link:active { + background-color: transparent!important; + color: #409ad5!important; + box-shadow: none +} + +.btn-link:visited { + color: #9B59B6 +} + +.wy-btn-group .btn, +.wy-control .btn { + vertical-align: middle +} + +.wy-btn-group { + margin-bottom: 24px; + *zoom: 1 +} + +.wy-btn-group:before, +.wy-btn-group:after { + display: table; + content: "" +} + +.wy-btn-group:after { + clear: both +} + +.wy-dropdown { + position: relative; + display: inline-block +} + +.wy-dropdown-active .wy-dropdown-menu { + display: block +} + +.wy-dropdown-menu { + position: absolute; + left: 0; + display: none; + float: left; + top: 100%; + min-width: 100%; + background: #fcfcfc; + z-index: 100; + border: solid 1px #cfd7dd; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.1); + padding: 12px +} + +.wy-dropdown-menu>dd>a { + display: block; + clear: both; + color: #404040; + white-space: nowrap; + font-size: 90%; + padding: 0 12px; + cursor: pointer +} + +.wy-dropdown-menu>dd>a:hover { + background: #2980B9; + color: #fff +} + +.wy-dropdown-menu>dd.divider { + border-top: solid 1px #cfd7dd; + margin: 6px 0 +} + +.wy-dropdown-menu>dd.search { + padding-bottom: 12px +} + +.wy-dropdown-menu>dd.search input[type="search"] { + width: 100% +} + +.wy-dropdown-menu>dd.call-to-action { + background: #e3e3e3; + text-transform: uppercase; + font-weight: 500; + font-size: 80% +} + +.wy-dropdown-menu>dd.call-to-action:hover { + background: #e3e3e3 +} + +.wy-dropdown-menu>dd.call-to-action .btn { + color: #fff +} + +.wy-dropdown.wy-dropdown-up .wy-dropdown-menu { + bottom: 100%; + top: auto; + left: auto; + right: 0 +} + +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu { + background: #fcfcfc; + margin-top: 2px +} + +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a { + padding: 6px 12px +} + +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover { + background: #2980B9; + color: #fff +} + +.wy-dropdown.wy-dropdown-left .wy-dropdown-menu { + right: 0; + left: auto; + text-align: right +} + +.wy-dropdown-arrow:before { + content: " "; + border-bottom: 5px solid #f5f5f5; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + position: absolute; + display: block; + top: -4px; + left: 50%; + margin-left: -3px +} + +.wy-dropdown-arrow.wy-dropdown-arrow-left:before { + left: 11px +} + +.wy-form-stacked select { + display: block +} + +.wy-form-aligned input, +.wy-form-aligned textarea, +.wy-form-aligned select, +.wy-form-aligned .wy-help-inline, +.wy-form-aligned label { + display: inline-block; + *display: inline; + *zoom: 1; + vertical-align: middle +} + +.wy-form-aligned .wy-control-group>label { + display: inline-block; + vertical-align: middle; + width: 10em; + margin: 6px 12px 0 0; + float: left +} + +.wy-form-aligned .wy-control { + float: left +} + +.wy-form-aligned .wy-control label { + display: block +} + +.wy-form-aligned .wy-control select { + margin-top: 6px +} + +fieldset { + border: 0; + margin: 0; + padding: 0 +} + +legend { + display: block; + width: 100%; + border: 0; + padding: 0; + white-space: normal; + margin-bottom: 24px; + font-size: 150%; + *margin-left: -7px +} + +label { + display: block; + margin: 0 0 .3125em; + color: #333; + font-size: 90% +} + +input, +select, +textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle +} + +.wy-control-group { + margin-bottom: 24px; + *zoom: 1; + max-width: 68em; + margin-left: auto; + margin-right: auto; + *zoom: 1 +} + +.wy-control-group:before, +.wy-control-group:after { + display: table; + content: "" +} + +.wy-control-group:after { + clear: both +} + +.wy-control-group:before, +.wy-control-group:after { + display: table; + content: "" +} + +.wy-control-group:after { + clear: both +} + +.wy-control-group.wy-control-group-required>label:after { + content: " *"; + color: #E74C3C +} + +.wy-control-group .wy-form-full, +.wy-control-group .wy-form-halves, +.wy-control-group .wy-form-thirds { + padding-bottom: 12px +} + +.wy-control-group .wy-form-full select, +.wy-control-group .wy-form-halves select, +.wy-control-group .wy-form-thirds select { + width: 100% +} + +.wy-control-group .wy-form-full input[type="text"], +.wy-control-group .wy-form-full input[type="password"], +.wy-control-group .wy-form-full input[type="email"], +.wy-control-group .wy-form-full input[type="url"], +.wy-control-group .wy-form-full input[type="date"], +.wy-control-group .wy-form-full input[type="month"], +.wy-control-group .wy-form-full input[type="time"], +.wy-control-group .wy-form-full input[type="datetime"], +.wy-control-group .wy-form-full input[type="datetime-local"], +.wy-control-group .wy-form-full input[type="week"], +.wy-control-group .wy-form-full input[type="number"], +.wy-control-group .wy-form-full input[type="search"], +.wy-control-group .wy-form-full input[type="tel"], +.wy-control-group .wy-form-full input[type="color"], +.wy-control-group .wy-form-halves input[type="text"], +.wy-control-group .wy-form-halves input[type="password"], +.wy-control-group .wy-form-halves input[type="email"], +.wy-control-group .wy-form-halves input[type="url"], +.wy-control-group .wy-form-halves input[type="date"], +.wy-control-group .wy-form-halves input[type="month"], +.wy-control-group .wy-form-halves input[type="time"], +.wy-control-group .wy-form-halves input[type="datetime"], +.wy-control-group .wy-form-halves input[type="datetime-local"], +.wy-control-group .wy-form-halves input[type="week"], +.wy-control-group .wy-form-halves input[type="number"], +.wy-control-group .wy-form-halves input[type="search"], +.wy-control-group .wy-form-halves input[type="tel"], +.wy-control-group .wy-form-halves input[type="color"], +.wy-control-group .wy-form-thirds input[type="text"], +.wy-control-group .wy-form-thirds input[type="password"], +.wy-control-group .wy-form-thirds input[type="email"], +.wy-control-group .wy-form-thirds input[type="url"], +.wy-control-group .wy-form-thirds input[type="date"], +.wy-control-group .wy-form-thirds input[type="month"], +.wy-control-group .wy-form-thirds input[type="time"], +.wy-control-group .wy-form-thirds input[type="datetime"], +.wy-control-group .wy-form-thirds input[type="datetime-local"], +.wy-control-group .wy-form-thirds input[type="week"], +.wy-control-group .wy-form-thirds input[type="number"], +.wy-control-group .wy-form-thirds input[type="search"], +.wy-control-group .wy-form-thirds input[type="tel"], +.wy-control-group .wy-form-thirds input[type="color"] { + width: 100% +} + +.wy-control-group .wy-form-full { + float: left; + display: block; + margin-right: 2.35765%; + width: 100%; + margin-right: 0 +} + +.wy-control-group .wy-form-full:last-child { + margin-right: 0 +} + +.wy-control-group .wy-form-halves { + float: left; + display: block; + margin-right: 2.35765%; + width: 48.82117% +} + +.wy-control-group .wy-form-halves:last-child { + margin-right: 0 +} + +.wy-control-group .wy-form-halves:nth-of-type(2n) { + margin-right: 0 +} + +.wy-control-group .wy-form-halves:nth-of-type(2n+1) { + clear: left +} + +.wy-control-group .wy-form-thirds { + float: left; + display: block; + margin-right: 2.35765%; + width: 31.76157% +} + +.wy-control-group .wy-form-thirds:last-child { + margin-right: 0 +} + +.wy-control-group .wy-form-thirds:nth-of-type(3n) { + margin-right: 0 +} + +.wy-control-group .wy-form-thirds:nth-of-type(3n+1) { + clear: left +} + +.wy-control-group.wy-control-group-no-input .wy-control { + margin: 6px 0 0; + font-size: 90% +} + +.wy-control-no-input { + display: inline-block; + margin: 6px 0 0; + font-size: 90% +} + +.wy-control-group.fluid-input input[type="text"], +.wy-control-group.fluid-input input[type="password"], +.wy-control-group.fluid-input input[type="email"], +.wy-control-group.fluid-input input[type="url"], +.wy-control-group.fluid-input input[type="date"], +.wy-control-group.fluid-input input[type="month"], +.wy-control-group.fluid-input input[type="time"], +.wy-control-group.fluid-input input[type="datetime"], +.wy-control-group.fluid-input input[type="datetime-local"], +.wy-control-group.fluid-input input[type="week"], +.wy-control-group.fluid-input input[type="number"], +.wy-control-group.fluid-input input[type="search"], +.wy-control-group.fluid-input input[type="tel"], +.wy-control-group.fluid-input input[type="color"] { + width: 100% +} + +.wy-form-message-inline { + display: inline-block; + padding-left: .3em; + color: #666; + vertical-align: middle; + font-size: 90% +} + +.wy-form-message { + display: block; + color: #999; + font-size: 70%; + margin-top: .3125em; + font-style: italic +} + +.wy-form-message p { + font-size: inherit; + font-style: italic; + margin-bottom: 6px +} + +.wy-form-message p:last-child { + margin-bottom: 0 +} + +input { + line-height: normal +} + +input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; + font-family: "Source Sans Pro","proxima-nova","Helvetica Neue",Arial,sans-serif; + *overflow: visible +} + +input[type="text"], +input[type="password"], +input[type="email"], +input[type="url"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="datetime"], +input[type="datetime-local"], +input[type="week"], +input[type="number"], +input[type="search"], +input[type="tel"], +input[type="color"] { + -webkit-appearance: none; + padding: 6px; + display: inline-block; + border: 1px solid #ccc; + font-size: 80%; + font-family: "Source Sans Pro","proxima-nova","Helvetica Neue",Arial,sans-serif; + box-shadow: inset 0 1px 3px #ddd; + border-radius: 0; + -webkit-transition: border .3s linear; + -moz-transition: border .3s linear; + transition: border .3s linear +} + +input[type="datetime-local"] { + padding: .34375em .625em +} + +input[disabled] { + cursor: default +} + +input[type="checkbox"], +input[type="radio"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; + margin-right: .3125em; + *height: 13px; + *width: 13px +} + +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none +} + +input[type="text"]:focus, +input[type="password"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus { + outline: 0; + outline: thin dotted \9; + border-color: #333 +} + +input.no-focus:focus { + border-color: #ccc!important +} + +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 1px auto #129FEA +} + +input[type="text"][disabled], +input[type="password"][disabled], +input[type="email"][disabled], +input[type="url"][disabled], +input[type="date"][disabled], +input[type="month"][disabled], +input[type="time"][disabled], +input[type="datetime"][disabled], +input[type="datetime-local"][disabled], +input[type="week"][disabled], +input[type="number"][disabled], +input[type="search"][disabled], +input[type="tel"][disabled], +input[type="color"][disabled] { + cursor: not-allowed; + background-color: #fafafa +} + +input:focus:invalid, +textarea:focus:invalid, +select:focus:invalid { + color: #E74C3C; + border: 1px solid #E74C3C +} + +input:focus:invalid:focus, +textarea:focus:invalid:focus, +select:focus:invalid:focus { + border-color: #E74C3C +} + +input[type="file"]:focus:invalid:focus, +input[type="radio"]:focus:invalid:focus, +input[type="checkbox"]:focus:invalid:focus { + outline-color: #E74C3C +} + +input.wy-input-large { + padding: 12px; + font-size: 100% +} + +textarea { + overflow: auto; + vertical-align: top; + width: 100%; + font-family: "Source Sans Pro","proxima-nova","Helvetica Neue",Arial,sans-serif +} + +select, +textarea { + padding: .5em .625em; + display: inline-block; + border: 1px solid #ccc; + font-size: 80%; + box-shadow: inset 0 1px 3px #ddd; + -webkit-transition: border .3s linear; + -moz-transition: border .3s linear; + transition: border .3s linear +} + +select { + border: 1px solid #ccc; + background-color: #fff +} + +select[multiple] { + height: auto +} + +select:focus, +textarea:focus { + outline: 0 +} + +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #fafafa +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled] { + cursor: not-allowed +} + +.wy-checkbox, +.wy-radio { + margin: 6px 0; + color: #404040; + display: block +} + +.wy-checkbox input, +.wy-radio input { + vertical-align: baseline +} + +.wy-form-message-inline { + display: inline-block; + *display: inline; + *zoom: 1; + vertical-align: middle +} + +.wy-input-prefix, +.wy-input-suffix { + white-space: nowrap; + padding: 6px +} + +.wy-input-prefix .wy-input-context, +.wy-input-suffix .wy-input-context { + line-height: 27px; + padding: 0 8px; + display: inline-block; + font-size: 80%; + background-color: #f3f6f6; + border: solid 1px #ccc; + color: #999 +} + +.wy-input-suffix .wy-input-context { + border-left: 0 +} + +.wy-input-prefix .wy-input-context { + border-right: 0 +} + +.wy-switch { + width: 36px; + height: 12px; + margin: 12px 0; + position: relative; + border-radius: 4px; + background: #ccc; + cursor: pointer; + -webkit-transition: all .2s ease-in-out; + -moz-transition: all .2s ease-in-out; + transition: all .2s ease-in-out +} + +.wy-switch:before { + position: absolute; + content: ""; + display: block; + width: 18px; + height: 18px; + border-radius: 4px; + background: #999; + left: -3px; + top: -3px; + -webkit-transition: all .2s ease-in-out; + -moz-transition: all .2s ease-in-out; + transition: all .2s ease-in-out +} + +.wy-switch:after { + content: "false"; + position: absolute; + left: 48px; + display: block; + font-size: 12px; + color: #ccc +} + +.wy-switch.active { + background: #1e8449 +} + +.wy-switch.active:before { + left: 24px; + background: #27AE60 +} + +.wy-switch.active:after { + content: "true" +} + +.wy-switch.disabled, +.wy-switch.active.disabled { + cursor: not-allowed +} + +.wy-control-group.wy-control-group-error .wy-form-message, +.wy-control-group.wy-control-group-error>label { + color: #E74C3C +} + +.wy-control-group.wy-control-group-error input[type="text"], +.wy-control-group.wy-control-group-error input[type="password"], +.wy-control-group.wy-control-group-error input[type="email"], +.wy-control-group.wy-control-group-error input[type="url"], +.wy-control-group.wy-control-group-error input[type="date"], +.wy-control-group.wy-control-group-error input[type="month"], +.wy-control-group.wy-control-group-error input[type="time"], +.wy-control-group.wy-control-group-error input[type="datetime"], +.wy-control-group.wy-control-group-error input[type="datetime-local"], +.wy-control-group.wy-control-group-error input[type="week"], +.wy-control-group.wy-control-group-error input[type="number"], +.wy-control-group.wy-control-group-error input[type="search"], +.wy-control-group.wy-control-group-error input[type="tel"], +.wy-control-group.wy-control-group-error input[type="color"] { + border: solid 1px #E74C3C +} + +.wy-control-group.wy-control-group-error textarea { + border: solid 1px #E74C3C +} + +.wy-inline-validate { + white-space: nowrap +} + +.wy-inline-validate .wy-input-context { + padding: .5em .625em; + display: inline-block; + font-size: 80% +} + +.wy-inline-validate.wy-inline-validate-success .wy-input-context { + color: #27AE60 +} + +.wy-inline-validate.wy-inline-validate-danger .wy-input-context { + color: #E74C3C +} + +.wy-inline-validate.wy-inline-validate-warning .wy-input-context { + color: #E67E22 +} + +.wy-inline-validate.wy-inline-validate-info .wy-input-context { + color: #2980B9 +} + +.rotate-90 { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + -o-transform: rotate(90deg); + transform: rotate(90deg) +} + +.rotate-180 { + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -ms-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg) +} + +.rotate-270 { + -webkit-transform: rotate(270deg); + -moz-transform: rotate(270deg); + -ms-transform: rotate(270deg); + -o-transform: rotate(270deg); + transform: rotate(270deg) +} + +.mirror { + -webkit-transform: scaleX(-1); + -moz-transform: scaleX(-1); + -ms-transform: scaleX(-1); + -o-transform: scaleX(-1); + transform: scaleX(-1) +} + +.mirror.rotate-90 { + -webkit-transform: scaleX(-1) rotate(90deg); + -moz-transform: scaleX(-1) rotate(90deg); + -ms-transform: scaleX(-1) rotate(90deg); + -o-transform: scaleX(-1) rotate(90deg); + transform: scaleX(-1) rotate(90deg) +} + +.mirror.rotate-180 { + -webkit-transform: scaleX(-1) rotate(180deg); + -moz-transform: scaleX(-1) rotate(180deg); + -ms-transform: scaleX(-1) rotate(180deg); + -o-transform: scaleX(-1) rotate(180deg); + transform: scaleX(-1) rotate(180deg) +} + +.mirror.rotate-270 { + -webkit-transform: scaleX(-1) rotate(270deg); + -moz-transform: scaleX(-1) rotate(270deg); + -ms-transform: scaleX(-1) rotate(270deg); + -o-transform: scaleX(-1) rotate(270deg); + transform: scaleX(-1) rotate(270deg) +} + +@media only screen and (max-width: 480px) { + .wy-form button[type="submit"] { + margin: .7em 0 0 + } + + .wy-form input[type="text"], + .wy-form input[type="password"], + .wy-form input[type="email"], + .wy-form input[type="url"], + .wy-form input[type="date"], + .wy-form input[type="month"], + .wy-form input[type="time"], + .wy-form input[type="datetime"], + .wy-form input[type="datetime-local"], + .wy-form input[type="week"], + .wy-form input[type="number"], + .wy-form input[type="search"], + .wy-form input[type="tel"], + .wy-form input[type="color"] { + margin-bottom: .3em; + display: block + } + + .wy-form label { + margin-bottom: .3em; + display: block + } + + .wy-form input[type="password"], + .wy-form input[type="email"], + .wy-form input[type="url"], + .wy-form input[type="date"], + .wy-form input[type="month"], + .wy-form input[type="time"], + .wy-form input[type="datetime"], + .wy-form input[type="datetime-local"], + .wy-form input[type="week"], + .wy-form input[type="number"], + .wy-form input[type="search"], + .wy-form input[type="tel"], + .wy-form input[type="color"] { + margin-bottom: 0 + } + + .wy-form-aligned .wy-control-group label { + margin-bottom: .3em; + text-align: left; + display: block; + width: 100% + } + + .wy-form-aligned .wy-control { + margin: 1.5em 0 0 + } + + .wy-form .wy-help-inline, + .wy-form-message-inline, + .wy-form-message { + display: block; + font-size: 80%; + padding: 6px 0 + } + +} + +@media screen and (max-width: 768px) { + .tablet-hide { + display: none + } + +} + +@media screen and (max-width: 480px) { + .mobile-hide { + display: none + } + +} + +.float-left { + float: left +} + +.float-right { + float: right +} + +.full-width { + width: 100% +} + +.wy-table, +.rst-content table.docutils, +.rst-content table.field-list { + border-collapse: collapse; + border-spacing: 0; + empty-cells: show; + margin-bottom: 24px +} + +.wy-table caption, +.rst-content table.docutils caption, +.rst-content table.field-list caption { + color: #000; + font: italic 85%/1 arial,sans-serif; + padding: 1em 0; + text-align: center +} + +.wy-table td, +.rst-content table.docutils td, +.rst-content table.field-list td, +.wy-table th, +.rst-content table.docutils th, +.rst-content table.field-list th { + font-size: 16px; + margin: 0; + overflow: visible; + padding: 6px 13px +} + +.wy-table td:first-child, +.rst-content table.docutils td:first-child, +.rst-content table.field-list td:first-child, +.wy-table th:first-child, +.rst-content table.docutils th:first-child, +.rst-content table.field-list th:first-child { + border-left-width: 0 +} + +.wy-table thead, +.rst-content table.docutils thead, +.rst-content table.field-list thead { + color: #000; + text-align: center; + vertical-align: bottom; + white-space: nowrap +} + +.wy-table thead th, +.rst-content table.docutils thead th, +.rst-content table.field-list thead th { + font-weight: 700; + border-bottom: solid 1px #ddd; +} + +.wy-table td, +.rst-content table.docutils td, +.rst-content table.field-list td { + background-color: transparent; + vertical-align: middle +} + +.wy-table td p, +.rst-content table.docutils td p, +.rst-content table.field-list td p { + line-height: 18px +} + +.wy-table td p:last-child, +.rst-content table.docutils td p:last-child, +.rst-content table.field-list td p:last-child { + margin-bottom: 0 +} + +.wy-table .wy-table-cell-min, +.rst-content table.docutils .wy-table-cell-min, +.rst-content table.field-list .wy-table-cell-min { + width: 1%; + padding-right: 0 +} + +.wy-table .wy-table-cell-min input[type=checkbox], +.rst-content table.docutils .wy-table-cell-min input[type=checkbox], +.rst-content table.field-list .wy-table-cell-min input[type=checkbox], +.wy-table .wy-table-cell-min input[type=checkbox], +.rst-content table.docutils .wy-table-cell-min input[type=checkbox], +.rst-content table.field-list .wy-table-cell-min input[type=checkbox] { + margin: 0 +} + +.wy-table-secondary { + color: gray; + font-size: 90% +} + +.wy-table-tertiary { + color: gray; + font-size: 80% +} + +.wy-table-odd td, +.wy-table-striped tr:nth-child(2n-1) td, +.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { + background-color: #fcfcfc; +} + +.wy-table-odd td, +.wy-table-striped tr:nth-child(2n) td, +.rst-content table.docutils:not(.field-list) tr:nth-child(2n) td { + background-color: #f8f8f8; +} + +.wy-table-backed { + background-color: #f8f8f8; +} + +.wy-table-bordered-all, +.rst-content table.docutils { + border: 1px solid #ddd; + line-height: 24px; +} + +.wy-table-bordered-all td, +.rst-content table.docutils td { + border-bottom: 1px solid #ddd; + border-left: 1px solid #ddd; +} + +.wy-table-bordered-all tbody>tr:last-child td, +.rst-content table.docutils tbody>tr:last-child td { + border-bottom-width: 0 +} + +.wy-table-bordered { + border: 1px solid #ddd; +} + +.wy-table-bordered-rows td { + border-bottom: 1px solid #ddd; +} + +.wy-table-bordered-rows tbody>tr:last-child td { + border-bottom-width: 0 +} + +.wy-table-horizontal tbody>tr:last-child td { + border-bottom-width: 0 +} + +.wy-table-horizontal td, +.wy-table-horizontal th { + border-width: 0 0 1px; + border-bottom: 1px solid #ddd; +} + +.wy-table-horizontal tbody>tr:last-child td { + border-bottom-width: 0 +} + +.wy-table-responsive { + margin-bottom: 24px; + max-width: 100%; + overflow: auto +} + +.wy-table-responsive table { + margin-bottom: 0!important +} + +.wy-table-responsive table td, +.wy-table-responsive table th { + white-space: nowrap +} + +a { + text-decoration: none; + cursor: pointer +} + +html { + height: 100%; + overflow-x: hidden +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-weight: 400; + color: #333; + min-height: 100%; + overflow-x: hidden; + background: #edf0f2 +} + +.wy-text-left { + text-align: left +} + +.wy-text-center { + text-align: center +} + +.wy-text-right { + text-align: right +} + +.wy-text-large { + font-size: 120% +} + +.wy-text-normal { + font-size: 100% +} + +.wy-text-small, +small { + font-size: 80% +} + +.wy-text-strike { + text-decoration: line-through +} + +.wy-text-warning { + color: #E67E22!important +} + +a.wy-text-warning:hover { + color: #eb9950!important +} + +.wy-text-info { + color: #2980B9!important +} + +a.wy-text-info:hover { + color: #409ad5!important +} + +.wy-text-success { + color: #27AE60!important +} + +a.wy-text-success:hover { + color: #36d278!important +} + +.wy-text-danger { + color: #E74C3C!important +} + +a.wy-text-danger:hover { + color: #ed7669!important +} + +.wy-text-neutral { + color: #404040!important +} + +a.wy-text-neutral:hover { + color: #595959!important +} + +.h1, +.h2, +.h3, +.h4, +.h5, +.h6, +h1, +h2, +h3, +h4, +h5, +h6, +legend { + margin-top: 0; + font-weight: 700; + font-family: "Source Sans Pro","ff-tisa-web-pro","Georgia",Arial,sans-serif; + font-style: normal +} + +p { + line-height: 24px; + margin: 0; + font-size: 16px; + margin-bottom: 24px +} + +h1, +.h1 { + font-size: 175% +} + +h2, +.h2 { + font-size: 150% +} + +h3, +.h3 { + font-size: 125% +} + +h4, +.h4 { + font-size: 115% +} + +h5, +.h5 { + font-size: 110% +} + +h6, +.h6 { + font-size: 100% +} + +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #e1e4e5; + margin: 24px 0; + padding: 0 +} + +code, +.rst-content tt, +.rst-content code { + max-width: 100%; + background: #fff; + border: solid 1px #e1e4e5; + font-size: 75%; + padding: 0 5px; + font-family: "Source Code Pro","Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace; + color: #E74C3C; + overflow-x: auto +} + +code.code-large, +.rst-content tt.code-large { + font-size: 90% +} + +.wy-plain-list-disc, +.rst-content .section ul, +.rst-content .toctree-wrapper ul, +article ul { + list-style: disc; + line-height: 24px; + margin-bottom: 24px +} + +.wy-plain-list-disc li, +.rst-content .section ul li, +.rst-content .toctree-wrapper ul li, +article ul li { + list-style: disc; + margin-left: 24px +} + +.wy-plain-list-disc li p:last-child, +.rst-content .section ul li p:last-child, +.rst-content .toctree-wrapper ul li p:last-child, +article ul li p:last-child { + margin-bottom: 0 +} + +.wy-plain-list-disc li ul, +.rst-content .section ul li ul, +.rst-content .toctree-wrapper ul li ul, +article ul li ul { + margin-bottom: 0 +} + +.wy-plain-list-disc li li, +.rst-content .section ul li li, +.rst-content .toctree-wrapper ul li li, +article ul li li { + list-style: circle +} + +.wy-plain-list-disc li li li, +.rst-content .section ul li li li, +.rst-content .toctree-wrapper ul li li li, +article ul li li li { + list-style: square +} + +.wy-plain-list-disc li ol li, +.rst-content .section ul li ol li, +.rst-content .toctree-wrapper ul li ol li, +article ul li ol li { + list-style: decimal +} + +.wy-plain-list-decimal, +.rst-content .section ol, +.rst-content ol.arabic, +article ol { + list-style: decimal; + line-height: 24px; + margin-bottom: 24px +} + +.wy-plain-list-decimal li, +.rst-content .section ol li, +.rst-content ol.arabic li, +article ol li { + list-style: decimal; + margin-left: 24px +} + +.wy-plain-list-decimal li p:last-child, +.rst-content .section ol li p:last-child, +.rst-content ol.arabic li p:last-child, +article ol li p:last-child { + margin-bottom: 0 +} + +.wy-plain-list-decimal li ul, +.rst-content .section ol li ul, +.rst-content ol.arabic li ul, +article ol li ul { + margin-bottom: 0 +} + +.wy-plain-list-decimal li ul li, +.rst-content .section ol li ul li, +.rst-content ol.arabic li ul li, +article ol li ul li { + list-style: disc +} + + +footer { + color: #999 +} + +footer p { + margin-bottom: 12px +} + +.rst-footer-buttons { + *zoom: 1 +} + +.rst-footer-buttons:before, +.rst-footer-buttons:after { + display: table; + content: "" +} + +.rst-footer-buttons:after { + clear: both +} + +#search-results .search li { + margin-bottom: 24px; + border-bottom: solid 1px #e1e4e5; + padding-bottom: 24px +} + +#search-results .search li:first-child { + border-top: solid 1px #e1e4e5; + padding-top: 24px +} + +#search-results .search li a { + font-size: 120%; + margin-bottom: 12px; + display: inline-block +} + +#search-results .context { + color: gray; + font-size: 90% +} + + + +@media print { + .rst-versions, + footer, + .wy-nav-side { + display: none + } + + .wy-nav-content-wrap { + margin-left: 0 + } + +} + +.rst-versions { + position: fixed; + bottom: 0; + left: 0; + width: 300px; + color: #fcfcfc; + background: #1f1d1d; + border-top: solid 10px #343131; + font-family: "Source Sans Pro","proxima-nova","Helvetica Neue",Arial,sans-serif; + z-index: 400 +} + +.rst-versions a { + color: #2980B9; + text-decoration: none +} + +.rst-versions .rst-badge-small { + display: none +} + +.rst-versions .rst-current-version { + padding: 12px; + background-color: #272525; + display: block; + text-align: right; + font-size: 90%; + cursor: pointer; + color: #27AE60; + *zoom: 1 +} + +.rst-versions .rst-current-version:before, +.rst-versions .rst-current-version:after { + display: table; + content: "" +} + +.rst-versions .rst-current-version:after { + clear: both +} + +.rst-versions .rst-current-version .fa, +.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand, +.rst-versions .rst-current-version .rst-content .admonition-title, +.rst-content .rst-versions .rst-current-version .admonition-title, +.rst-versions .rst-current-version .rst-content h1 .headerlink, +.rst-content h1 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h2 .headerlink, +.rst-content h2 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h3 .headerlink, +.rst-content h3 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h4 .headerlink, +.rst-content h4 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h5 .headerlink, +.rst-content h5 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h6 .headerlink, +.rst-content h6 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content dl dt .headerlink, +.rst-content dl dt .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content p.caption .headerlink, +.rst-content p.caption .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content tt.download span:first-child, +.rst-content tt.download .rst-versions .rst-current-version span:first-child, +.rst-versions .rst-current-version .rst-content code.download span:first-child, +.rst-content code.download .rst-versions .rst-current-version span:first-child, +.rst-versions .rst-current-version .icon { + color: #fcfcfc +} + +.rst-versions .rst-current-version .fa-book, +.rst-versions .rst-current-version .icon-book { + float: left +} + +.rst-versions .rst-current-version .icon-book { + float: left +} + +.rst-versions .rst-current-version.rst-out-of-date { + background-color: #E74C3C; + color: #fff +} + +.rst-versions .rst-current-version.rst-active-old-version { + background-color: #F1C40F; + color: #000 +} + +.rst-versions.shift-up .rst-other-versions { + display: block +} + +.rst-versions .rst-other-versions { + font-size: 90%; + padding: 12px; + color: gray; + display: none +} + +.rst-versions .rst-other-versions hr { + display: block; + height: 1px; + border: 0; + margin: 20px 0; + padding: 0; + border-top: solid 1px #413d3d +} + +.rst-versions .rst-other-versions dd { + display: inline-block; + margin: 0 +} + +.rst-versions .rst-other-versions dd a { + display: inline-block; + padding: 6px; + color: #fcfcfc +} + +.rst-versions.rst-badge { + width: auto; + bottom: 20px; + right: 20px; + left: auto; + border: none; + max-width: 300px +} + +.rst-versions.rst-badge .icon-book { + float: none +} + +.rst-versions.rst-badge .fa-book, +.rst-versions.rst-badge .icon-book { + float: none +} + +.rst-versions.rst-badge.shift-up .rst-current-version { + text-align: right +} + +.rst-versions.rst-badge.shift-up .rst-current-version .fa-book, +.rst-versions.rst-badge.shift-up .rst-current-version .icon-book { + float: left +} + +.rst-versions.rst-badge.shift-up .rst-current-version .icon-book { + float: left +} + +.rst-versions.rst-badge .rst-current-version { + width: auto; + height: 30px; + line-height: 30px; + padding: 0 6px; + display: block; + text-align: center +} + +@media screen and (max-width: 768px) { + .rst-versions { + width: 85%; + display: none + } + + .rst-versions.shift { + display: block + } + + img { + width: 100%; + height: auto + } + +} + +.rst-content img { + max-width: 100%; + height: auto!important +} + +.rst-content div.figure { + margin-bottom: 24px +} + +.rst-content div.figure.align-center { + text-align: center +} + +.rst-content .section>img, +.rst-content .section>a>img { + margin-bottom: 24px +} + +.rst-content blockquote { + margin-left: 24px; + line-height: 24px; + margin-bottom: 24px +} + +.rst-content .note .last, +.rst-content .attention .last, +.rst-content .caution .last, +.rst-content .danger .last, +.rst-content .error .last, +.rst-content .hint .last, +.rst-content .important .last, +.rst-content .tip .last, +.rst-content .warning .last, +.rst-content .seealso .last, +.rst-content .admonition-todo .last { + margin-bottom: 0 +} + +.rst-content .admonition-title:before { + margin-right: 4px +} + +.rst-content .admonition table { + border-color: rgba(0,0,0,0.1) +} + +.rst-content .admonition table td, +.rst-content .admonition table th { + background: transparent!important; + border-color: rgba(0,0,0,0.1)!important +} + +.rst-content .section ol.loweralpha, +.rst-content .section ol.loweralpha li { + list-style: lower-alpha +} + +.rst-content .section ol.upperalpha, +.rst-content .section ol.upperalpha li { + list-style: upper-alpha +} + +.rst-content .section ol p, +.rst-content .section ul p { + margin-bottom: 12px +} + +.rst-content .line-block { + margin-left: 24px +} + +.rst-content .topic-title { + font-weight: 700; + margin-bottom: 12px +} + +.rst-content .toc-backref { + color: #404040 +} + +.rst-content .align-right { + float: right; + margin: 0 0 24px 24px +} + +.rst-content .align-left { + float: left; + margin: 0 24px 24px 0 +} + +.rst-content .align-center { + margin: auto; + display: block +} + +.rst-content h1 .headerlink, +.rst-content h2 .headerlink, +.rst-content h3 .headerlink, +.rst-content h4 .headerlink, +.rst-content h5 .headerlink, +.rst-content h6 .headerlink, +.rst-content dl dt .headerlink, +.rst-content p.caption .headerlink { + display: none; + visibility: hidden; + font-size: 14px +} + +.rst-content h1 .headerlink:after, +.rst-content h2 .headerlink:after, +.rst-content h3 .headerlink:after, +.rst-content h4 .headerlink:after, +.rst-content h5 .headerlink:after, +.rst-content h6 .headerlink:after, +.rst-content dl dt .headerlink:after, +.rst-content p.caption .headerlink:after { + visibility: visible; + content: ""; + font-family: FontAwesome; + display: inline-block +} + +.rst-content h1:hover .headerlink, +.rst-content h2:hover .headerlink, +.rst-content h3:hover .headerlink, +.rst-content h4:hover .headerlink, +.rst-content h5:hover .headerlink, +.rst-content h6:hover .headerlink, +.rst-content dl dt:hover .headerlink, +.rst-content p.caption:hover .headerlink { + display: inline-block +} + +.rst-content .sidebar { + float: right; + width: 40%; + display: block; + margin: 0 0 24px 24px; + padding: 24px; + background: #f3f6f6; + border: solid 1px #e1e4e5 +} + +.rst-content .sidebar p, +.rst-content .sidebar ul, +.rst-content .sidebar dl { + font-size: 90% +} + +.rst-content .sidebar .last { + margin-bottom: 0 +} + +.rst-content .sidebar .sidebar-title { + display: block; + font-family: "Source Sans Pro","ff-tisa-web-pro","Georgia",Arial,sans-serif; + font-weight: 700; + background: #e1e4e5; + padding: 6px 12px; + margin: -24px; + margin-bottom: 24px; + font-size: 100% +} + +.rst-content .highlighted { + background: #F1C40F; + display: inline-block; + font-weight: 700; + padding: 0 6px +} + +.rst-content .footnote-reference, +.rst-content .citation-reference { + vertical-align: super; + font-size: 90% +} + +.rst-content table.docutils.citation, +.rst-content table.docutils.footnote { + background: none; + border: none; + color: #999 +} + +.rst-content table.docutils.citation td, +.rst-content table.docutils.citation tr, +.rst-content table.docutils.footnote td, +.rst-content table.docutils.footnote tr { + border: none; + background-color: transparent!important; + white-space: normal +} + +.rst-content table.docutils.citation td.label, +.rst-content table.docutils.footnote td.label { + padding-left: 0; + padding-right: 0; + vertical-align: top +} + +.rst-content table.field-list { + border: none +} + +.rst-content table.field-list td { + border: none; + padding-top: 5px +} + +.rst-content table.field-list td>strong { + display: inline-block; + margin-top: 3px +} + +.rst-content table.field-list .field-name { + padding-right: 10px; + text-align: left; + white-space: nowrap +} + +.rst-content table.field-list .field-body { + text-align: left; + padding-left: 0 +} + +.rst-content tt, +.rst-content tt, +.rst-content code { + color: #000 +} + +.rst-content tt big, +.rst-content tt em, +.rst-content tt big, +.rst-content code big, +.rst-content tt em, +.rst-content code em { + font-size: 100%!important; + line-height: normal +} + +.rst-content tt .xref, +a .rst-content tt, +.rst-content tt .xref, +.rst-content code .xref, +a .rst-content tt, +a .rst-content code { + font-weight: 700 +} + +.rst-content a tt, +.rst-content a tt, +.rst-content a code { + color: #2980B9 +} + +.rst-content dl { + margin-bottom: 24px +} + +.rst-content dl dt { + font-weight: 700 +} + +.rst-content dl p, +.rst-content dl table, +.rst-content dl ul, +.rst-content dl ol { + margin-bottom: 12px!important +} + +.rst-content dl dd { + margin: 0 0 12px 24px +} + +.rst-content dl:not(.docutils) { + margin-bottom: 24px +} + +.rst-content dl:not(.docutils) dt { + display: inline-block; + margin: 6px 0; + font-size: 90%; + line-height: normal; + background: #e7f2fa; + color: #2980B9; + border-top: solid 3px #6ab0de; + padding: 6px; + position: relative +} + +.rst-content dl:not(.docutils) dt:before { + color: #6ab0de +} + +.rst-content dl:not(.docutils) dt .headerlink { + color: #404040; + font-size: 100%!important +} + +.rst-content dl:not(.docutils) dl dt { + margin-bottom: 6px; + border: none; + border-left: solid 3px #ccc; + background: #f0f0f0; + color: gray +} + +.rst-content dl:not(.docutils) dl dt .headerlink { + color: #404040; + font-size: 100%!important +} + +.rst-content dl:not(.docutils) dt:first-child { + margin-top: 0 +} + +.rst-content dl:not(.docutils) tt, +.rst-content dl:not(.docutils) tt, +.rst-content dl:not(.docutils) code { + font-weight: 700 +} + +.rst-content dl:not(.docutils) tt.descname, +.rst-content dl:not(.docutils) tt.descclassname, +.rst-content dl:not(.docutils) tt.descname, +.rst-content dl:not(.docutils) code.descname, +.rst-content dl:not(.docutils) tt.descclassname, +.rst-content dl:not(.docutils) code.descclassname { + background-color: transparent; + border: none; + padding: 0; + font-size: 100%!important +} + +.rst-content dl:not(.docutils) tt.descname, +.rst-content dl:not(.docutils) tt.descname, +.rst-content dl:not(.docutils) code.descname { + font-weight: 700 +} + +.rst-content dl:not(.docutils) .optional { + display: inline-block; + padding: 0 4px; + color: #000; + font-weight: 700 +} + +.rst-content dl:not(.docutils) .property { + display: inline-block; + padding-right: 8px +} + +.rst-content .viewcode-link, +.rst-content .viewcode-back { + display: inline-block; + color: #27AE60; + font-size: 80%; + padding-left: 24px +} + +.rst-content .viewcode-back { + display: block; + float: right +} + +.rst-content p.rubric { + margin-bottom: 12px; + font-weight: 700 +} + +.rst-content tt.download, +.rst-content code.download { + background: inherit; + padding: inherit; + font-family: inherit; + font-size: inherit; + color: inherit; + border: inherit; + white-space: inherit +} + +.rst-content tt.download span:first-child:before, +.rst-content code.download span:first-child:before { + margin-right: 4px +} + +@media screen and (max-width: 480px) { + .rst-content .sidebar { + width: 100% + } + +} + +span[id*='MathJax-Span'] { + color: #404040 +} + +.math { + text-align: center +} + +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 100; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_sans_pro/SourceSansPro-ExtraLight.ttf) format("truetype") +} + +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 100; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_sans_pro/SourceSansPro-ExtraLightItalic.ttf) format("truetype") +} + +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 300; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_sans_pro/SourceSansPro-Light.ttf) format("truetype") +} + +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 300; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_sans_pro/SourceSansPro-LightItalic.ttf) format("truetype") +} + +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 400; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_sans_pro/SourceSansPro-Regular.ttf) format("truetype") +} + +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 400; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_sans_pro/SourceSansPro-Italic.ttf) format("truetype") +} + +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 500; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_sans_pro/SourceSansPro-Semibold.ttf) format("truetype") +} + +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 500; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_sans_pro/SourceSansPro-SemiboldItalic.ttf) format("truetype") +} + +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 700; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_sans_pro/SourceSansPro-Bold.ttf) format("truetype") +} + +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 700; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_sans_pro/SourceSansPro-BoldItalic.ttf) format("truetype") +} + +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 300; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_code_pro/SourceCodePro-Light.ttf) format("truetype") +} + +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 400; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_code_pro/SourceCodePro-Regular.ttf) format("truetype") +} + +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 500; + src: url(https://cdn.crashlytics.io/static_assets/web/typefaces/source_code_pro/SourceCodePro-Medium.ttf) format("truetype") +} \ No newline at end of file diff --git a/docs/css/goreplay.css b/docs/css/goreplay.css new file mode 100644 index 0000000..a3d1d55 --- /dev/null +++ b/docs/css/goreplay.css @@ -0,0 +1,171 @@ +code { + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace !important; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + background-color: #f7f7f7 !important; + color: #333 !important; + font-size: 14px !important; + border: none !important; + padding: 5px !important; +} + +pre > code { + line-height: 20px; + padding: 16px !important; +} + +blockquote { + color: #777 !important; + border-left: 4px solid rgb(221, 221, 221); + padding-left: 16px; + padding-right: 16px; + margin-left: 0px !important; +} + +/* Visited links purple is not a good idea */ +a:visited { + color: #008BF3; +} + +a { + color: #008BF3; +} + +h3, h4, h5, h6 { + color: #333 !important; +} + +.rst-versions { + display: none !important; +} + +/* The next and previous button we don't need */ +.rst-footer-buttons { + display: none; +} + +/* Foldable parts */ +details > * { + margin-left: 20px; +} +details > summary { + margin-left: 0px; /* To overwrite the margin from above */ + + font-size: 120%; + cursor: pointer +} + +/* Fix the design not having the correct spacing */ +.wy-menu-vertical .subnav li.current > a { + padding: 0.4045em 2.427em; +} + +.toctree-l3 { + padding-left: 1.0em; +} + +footer .fastlane { + margin: 20px 0; +} + +footer .fastlane iframe { + vertical-align: middle; +} + +/* Custom Syntax highlighting to look more like GitHub.com */ +.hljs-symbol { + color: #0086b3; +} + +.hljs-keyword { + font-weight: normal; + color: #a71d5d; +} + +.hljs-string { + color: #183691; +} + +/* Header Anchors */ +/* From https://github.com/facelessuser/pymdown-extensions/blob/bf18d0635e9d91b0f98eacdcaa10f26e0ace0f20/doc_theme/css/theme_custom.css#L322-L395 */ +.rst-content .headeranchor-link { + position: absolute; + top: 0; + bottom: 0; + left: 0; + display: block; + padding-right: 6px; + padding-left: 30px; + margin-left: -30px; +} + +.rst-content .headeranchor-link:focus { + outline: none; +} + +.rst-content h1, +.rst-content h2, +.rst-content h3, +.rst-content h4, +.rst-content h5, +.rst-content h6 { + position: relative; +} + +.rst-content h1 .headeranchor, +.rst-content h2 .headeranchor, +.rst-content h3 .headeranchor, +.rst-content h4 .headeranchor, +.rst-content h5 .headeranchor, +.rst-content h6 .headeranchor { + display: none; + color: #000; + vertical-align: middle; +} + +.rst-content h1:hover .headeranchor-link, +.rst-content h2:hover .headeranchor-link, +.rst-content h3:hover .headeranchor-link, +.rst-content h4:hover .headeranchor-link, +.rst-content h5:hover .headeranchor-link, +.rst-content h6:hover .headeranchor-link { + height: 1em; + padding-left: 8px; + margin-left: -30px; + text-decoration: none; +} + +.rst-content h1:hover .headeranchor-link .headeranchor, +.rst-content h2:hover .headeranchor-link .headeranchor, +.rst-content h3:hover .headeranchor-link .headeranchor, +.rst-content h4:hover .headeranchor-link .headeranchor, +.rst-content h5:hover .headeranchor-link .headeranchor, +.rst-content h6:hover .headeranchor-link .headeranchor { + display: inline-block; +} + +.rst-content .headeranchor { + font: normal normal 16px FontAwesome; + line-height: 1; + display: inline-block; + text-decoration: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.rst-content .headeranchor:before { + content: '\f0c1'; +} + +/* index.md badges */ +@media screen and (max-width: 768px) { + .badge img { + width: auto; + } +} \ No newline at end of file diff --git a/docs/css/sidenav.css b/docs/css/sidenav.css new file mode 100644 index 0000000..5a1e37e --- /dev/null +++ b/docs/css/sidenav.css @@ -0,0 +1,435 @@ +.wy-affix { + position: fixed; + top: 1.618em; +} + +.wy-menu a:hover { + text-decoration: none; +} + +.wy-menu-vertical header, +.wy-menu-vertical p.caption { + height: 32px; + display: inline-block; + line-height: 32px; + padding: 0 1.618em; + margin-bottom: 0; + margin-top: 14px; + display: block; + font-weight: 700; + text-transform: uppercase; + font-size: 85%; + color: #ccc; + white-space: nowrap; +} + +.wy-menu-vertical span { + color: #666; +} + +.wy-menu-vertical ul { + margin-bottom: 0; +} + +.wy-menu-vertical li.divide-top { + border-top: solid 1px #404040; +} + +.wy-menu-vertical li.divide-bottom { + border-bottom: solid 1px #404040; +} + +.wy-menu-vertical li.current { + background-color: #e5e5e5; +} + +.wy-menu-vertical li.current a { + color: rgba(0, 93, 255, 0.7); + border-right: none; +} + +.wy-menu-vertical li.current a:hover { + color: rgba(0, 93, 255, 0.9); +} + +.wy-menu-vertical li code, +.wy-menu-vertical li .rst-content tt, +.rst-content .wy-menu-vertical li tt { + border: none; + background: inherit; + color: inherit; + padding-left: 0; + padding-right: 0 +} + +.wy-menu-vertical li span.toctree-expand { + display: block; + float: left; + margin-left: -1.2em; + font-size: .8em; + line-height: 1.6em; + color: #999; +} + +.wy-menu-vertical li.on a, +.wy-menu-vertical li.current>a { + color: rgba(0, 93, 255, 0.9); + font-weight: 700; + position: relative; + background: #fafafa; + border: none; +} + +/*.wy-menu-vertical li.on a:hover span.toctree-expand, +.wy-menu-vertical li.current>a:hover span.toctree-expand { + color: gray; +}*/ + +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand { + display: block; + font-size: .8em; + line-height: 1.6em; + color: #333; +} + +.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul, +.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul { + display: none; +} + +.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul, +.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul { + display: block; +} + +.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a { + display: block; + padding: .4045em 4.045em; +} + +.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand { + color: #999; +} + +.wy-menu-vertical li.toctree-l2 span.toctree-expand { + color: #999; +} + +.wy-menu-vertical li.toctree-l3 { + background-color: #eee; + font-size: .9em; +} + +.wy-menu-vertical li.toctree-l3.current>a { + padding: .4045em 4.045em; +} + +.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a { + display: block; + padding: .4045em 5.663em; + border-top: none; + border-bottom: none; +} + +.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand { + color: rgba(0, 93, 255, 0.9); +} + +.wy-menu-vertical li.toctree-l3 span.toctree-expand { + color: #999; +} + +.wy-menu-vertical li.toctree-l4 { + font-size: .9em; +} + +.wy-menu-vertical li.current ul { + display: block; +} + +.wy-menu-vertical .local-toc li ul { + display: block; +} + +.wy-menu-vertical li ul li a { + margin-bottom: 0; + color: rgba(0, 93, 255, 0.7); + font-weight: 400; +} + +.wy-menu-vertical a { + display: inline-block; + line-height: 18px; + padding: .4045em 1.618em; + display: block; + position: relative; + font-size: 90%; + color: rgba(0, 93, 255, 0.7); +} + +.wy-menu-vertical li.on a:hover, +.wy-menu-vertical li.current>a:hover { + background-color: #fafafa; +} + +.wy-menu-vertical a:hover { + color: rgba(0, 93, 255, 0.9); + cursor: pointer; + background-color: #fafafa; +} + +.wy-menu-vertical a:hover span.toctree-expand { + color: rgba(0, 93, 255, 0.5); +} + +.wy-menu-vertical a:active span.toctree-expand { + color: rgba(0, 93, 255, 0.7); +} + +/* Search */ + +.wy-side-nav-search { + z-index: 200; + background-color: #fafafa; + border-bottom: #333; + text-align: center; + padding: .809em; + display: block; + color: #333; + margin-bottom: .809em +} + +.wy-side-nav-search input[type=text] { + width: 100%; + color: #333; + border-radius: 3px; + outline: 0; + padding: 10px; + background-color: #fff; + border: solid 1px #6d6d6d; + box-shadow: none +} + +.wy-side-nav-search img { + display: block; + margin: auto auto .809em; + height: 45px; + width: 45px; + background-color: #2980B9; + padding: 5px; + border-radius: 100% +} + +.wy-side-nav-search>a, +.wy-side-nav-search .wy-dropdown>a { + color: #333; + font-size: 100%; + font-weight: 700; + display: inline-block; + padding: 4px 6px; + margin-bottom: .809em +} + +.wy-side-nav-search>a:hover, +.wy-side-nav-search .wy-dropdown>a:hover { + background: rgba(255,255,255,0.1) +} + +.wy-side-nav-search>a img.logo, +.wy-side-nav-search .wy-dropdown>a img.logo { + display: block; + margin: 0 auto; + height: auto; + width: auto; + border-radius: 0; + max-width: 100%; + background: transparent +} + +.wy-side-nav-search>a.icon img.logo, +.wy-side-nav-search .wy-dropdown>a.icon img.logo { + margin-top: .85em +} + +.wy-nav .wy-menu-vertical header { + color: #2980B9 +} + +.wy-nav .wy-menu-vertical a { + color: #b3b3b3 +} + +.wy-nav .wy-menu-vertical a:hover { + background-color: #2980B9; + color: #fff +} + +[data-menu-wrap] { + -webkit-transition: all .2s ease-in; + -moz-transition: all .2s ease-in; + transition: all .2s ease-in; + position: absolute; + opacity: 1; + width: 100%; + opacity: 0 +} + +[data-menu-wrap].move-center { + left: 0; + right: auto; + opacity: 1 +} + +[data-menu-wrap].move-left { + right: auto; + left: -100%; + opacity: 0 +} + +[data-menu-wrap].move-right { + right: -100%; + left: auto; + opacity: 0 +} + +.wy-body-for-nav { + background: left repeat-y #fcfcfc; + background-image: url(data:image/png; + base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC); + background-size: 300px 1px +} + +.wy-grid-for-nav { + position: absolute; + width: 100%; + height: 100% +} + +.wy-nav-side { + position: fixed; + top: 0; + bottom: 0; + left: 0; + padding-bottom: 2em; + width: 300px; + overflow-x: hidden; + overflow-y: scroll; + min-height: 100%; + background-color: #fafafa; + z-index: 200; + border-right: 2px solid #eee; +} + +.wy-nav-top { + display: none; + background-color: #333; + color: #fff; + padding: .4045em .809em; + position: relative; + line-height: 50px; + text-align: center; + font-size: 100%; + *zoom: 1 +} + +.wy-nav-top:before, +.wy-nav-top:after { + display: table; + content: "" +} + +.wy-nav-top:after { + clear: both +} + +.wy-nav-top a { + color: #fff; + font-weight: 700 +} + +.wy-nav-top img { + margin-right: 12px; + height: 45px; + width: 45px; + background-color: #2980B9; + padding: 5px; + border-radius: 100% +} + +.wy-nav-top i { + font-size: 30px; + line-height: 50px; + float: left; + cursor: pointer +} + +.wy-nav-content-wrap { + margin-left: 300px; + background: #fcfcfc; + min-height: 100% +} + +.wy-nav-content { + padding: 1.618em 3.236em; + height: 100%; + max-width: 1100px; + margin: auto +} + +.wy-body-mask { + position: fixed; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.2); + display: none; + z-index: 499 +} + +.wy-body-mask.on { + display: block +} + +@media screen and (max-width: 768px) { + .wy-body-for-nav { + background: #fcfcfc + } + + .wy-nav-top { + display: block + } + + .wy-nav-side { + left: -300px + } + + .wy-nav-side.shift { + width: 85%; + left: 0 + } + + .wy-nav-content-wrap { + margin-left: 0 + } + + .wy-nav-content-wrap .wy-nav-content { + padding: 1.618em + } + + .wy-nav-content-wrap.shift { + position: fixed; + min-width: 100%; + left: 85%; + top: 0; + height: 100%; + overflow: hidden + } + +} + +@media screen and (min-width: 1400px) { + .wy-nav-content { + margin: 0; + background: #fcfcfc + } +} \ No newline at end of file diff --git a/docs/getting-started/basics.md b/docs/getting-started/basics.md new file mode 100644 index 0000000..4add302 --- /dev/null +++ b/docs/getting-started/basics.md @@ -0,0 +1,17 @@ +### Overview +Gor architecture tries to follow UNIX philosophy: everything made of pipes, various inputs multiplexing data to outputs. + +You can [rate limit](/rate-limiting.md), [filter](request-filtering.md), [rewrite](request-rewriting.md) requests or even use your own [middleware](middleware.md) to implement custom logic. Also, it is possible to replay requests at the higher rate for [load testing](saving-and-replaying-from-file.md). + +### Available inputs + +* `--input-raw` - used to capture HTTP traffic, you should specify IP address or interface and application port. More about [[Capturing and replaying traffic]]. +* `--input-file` - accepts file which previously was recorded using ` --output-file`. More about [[Saving and Replaying from file]] +* `--input-tcp` - used by Gor aggregation instance if you decided forward traffic from multiple forwarder Gor instances to it. Read about using [[Aggregator-forwarder setup]]. + +### Available outputs + +* `--output-http` - replay HTTP traffic to given endpoint, accepts base url. Read [more about it](Replaying HTTP traffic) +* `--output-file` - records incoming traffic to the file. More about [[Saving and Replaying from file]] +* `--output-tcp` - forward incoming data to another Gor instance, used in conjunction with `--input-tcp`. Read more about [[Aggregator-forwarder setup]]. +* `--output-stdout` - used for debugging, outputs all data to stdout. \ No newline at end of file diff --git a/docs/Getting-Started.md b/docs/getting-started/tutorial.md similarity index 96% rename from docs/Getting-Started.md rename to docs/getting-started/tutorial.md index 6d16f63..1807c38 100644 --- a/docs/Getting-Started.md +++ b/docs/getting-started/tutorial.md @@ -41,4 +41,4 @@ Next: [[The Basics]] ### Watch an overview: -[![Foo](http://img.youtube.com/vi/CxuKZcMKaW4/0.jpg)](https://www.youtube.com/watch?v=CxuKZcMKaW4) \ No newline at end of file +![YOUTUBE](https://www.youtube.com/watch?v=CxuKZcMKaW4) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..cd7a7d9 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,33 @@ +--- +layout: home +--- + +Gor is an open-source tool for capturing and replaying live HTTP traffic into a test environment in order to continuously test your system with real data. It can be used to increase confidence in code deployments, configuration changes and infrastructure changes. + +Read for more info: + +* [Tutorial](getting-started/tutorial.md) +* [The Basics](getting-started/basics.md) +* [Capturing and replaying traffic](capturing-and-replaying-traffic.md) +* [Replaying HTTP traffic](replaying-http-traffic.md) +* [[PRO] Replaying Binary protocols](pro/replaying-binary-protocols.md) +* [[PRO] Recording and replaying keep alive TCP sessions](pro/recording-and-replaying-keep-alive-tcp-sessions.md) +* [Saving and Replaying from file](saving-and-replaying-from-file.md) +* [Performance testing](saving-and-replaying-from-file.md#performance-testing) +* [Rate limiting](rate-limiting.md) +* [Request filtering](request-filtering.md) +* [Request rewriting](request-rewriting.md) +* [Middleware](middleware.md) +* [Distributed configuration](distributed-configuration.md) +* [Exporting to ElasticSearch](exporting-to-elasticsearch.md) +* [FAQ](faq.md) +* [Troubleshooting](troubleshooting.md) + +## Commercial Aspects + +* [Commercial Support](commercial-support.md) +* [Commercial FAQ](commercial-faq.md) +* [Commercial collaboration](commercial-collaboration.md) + + +Next: [Getting Started](Getting-Started) diff --git a/docs/js/base.js b/docs/js/base.js new file mode 100644 index 0000000..dc4b343 --- /dev/null +++ b/docs/js/base.js @@ -0,0 +1,7 @@ +$(document).ready(function () { + $('img[alt="YOUTUBE"]').each(function () { + var id = $(this).attr('src').split('/')[$(this).attr('src').split('/').length - 1].split("=")[1]; + var video = ''; + $(this).replaceWith(video); + }); +}); \ No newline at end of file diff --git a/docs/js/turbolinks.js b/docs/js/turbolinks.js new file mode 100644 index 0000000..19f6b97 --- /dev/null +++ b/docs/js/turbolinks.js @@ -0,0 +1,6 @@ +/* +Turbolinks 5.0.0 +Copyright © 2016 Basecamp, LLC + */ +(function(){(function(){(function(){this.Turbolinks={supported:function(){return null!=window.history.pushState&&null!=window.requestAnimationFrame}(),visit:function(e,r){return t.controller.visit(e,r)},clearCache:function(){return t.controller.clearCache()}}}).call(this)}).call(this);var t=this.Turbolinks;(function(){(function(){var e,r;t.copyObject=function(t){var e,r,n;r={};for(e in t)n=t[e],r[e]=n;return r},t.closest=function(t,r){return e.call(t,r)},e=function(){var t,e;return t=document.documentElement,null!=(e=t.closest)?e:function(t){var e;for(e=this;e;){if(e.nodeType===Node.ELEMENT_NODE&&r.call(e,t))return e;e=e.parentNode}}}(),t.defer=function(t){return setTimeout(t,1)},t.dispatch=function(t,e){var r,n,o,i,s;return i=null!=e?e:{},s=i.target,r=i.cancelable,n=i.data,o=document.createEvent("Events"),o.initEvent(t,!0,r===!0),o.data=null!=n?n:{},(null!=s?s:document).dispatchEvent(o),o},t.match=function(t,e){return r.call(t,e)},r=function(){var t,e,r,n;return t=document.documentElement,null!=(e=null!=(r=null!=(n=t.matchesSelector)?n:t.webkitMatchesSelector)?r:t.msMatchesSelector)?e:t.mozMatchesSelector}(),t.uuid=function(){var t,e,r;for(r="",t=e=1;36>=e;t=++e)r+=9===t||14===t||19===t||24===t?"-":15===t?"4":20===t?(Math.floor(4*Math.random())+8).toString(16):Math.floor(15*Math.random()).toString(16);return r}}).call(this),function(){t.Location=function(){function t(t){var e,r;null==t&&(t=""),r=document.createElement("a"),r.href=t.toString(),this.absoluteURL=r.href,e=r.hash.length,2>e?this.requestURL=this.absoluteURL:(this.requestURL=this.absoluteURL.slice(0,-e),this.anchor=r.hash.slice(1))}var e,r,n,o;return t.wrap=function(t){return t instanceof this?t:new this(t)},t.prototype.getOrigin=function(){return this.absoluteURL.split("/",3).join("/")},t.prototype.getPath=function(){var t,e;return null!=(t=null!=(e=this.absoluteURL.match(/\/\/[^\/]*(\/[^?;]*)/))?e[1]:void 0)?t:"/"},t.prototype.getPathComponents=function(){return this.getPath().split("/").slice(1)},t.prototype.getLastPathComponent=function(){return this.getPathComponents().slice(-1)[0]},t.prototype.getExtension=function(){var t,e;return null!=(t=null!=(e=this.getLastPathComponent().match(/\.[^.]*$/))?e[0]:void 0)?t:""},t.prototype.isHTML=function(){return this.getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/)},t.prototype.isPrefixedBy=function(t){var e;return e=r(t),this.isEqualTo(t)||o(this.absoluteURL,e)},t.prototype.isEqualTo=function(t){return this.absoluteURL===(null!=t?t.absoluteURL:void 0)},t.prototype.toCacheKey=function(){return this.requestURL},t.prototype.toJSON=function(){return this.absoluteURL},t.prototype.toString=function(){return this.absoluteURL},t.prototype.valueOf=function(){return this.absoluteURL},r=function(t){return e(t.getOrigin()+t.getPath())},e=function(t){return n(t,"/")?t:t+"/"},o=function(t,e){return t.slice(0,e.length)===e},n=function(t,e){return t.slice(-e.length)===e},t}()}.call(this),function(){var e=function(t,e){return function(){return t.apply(e,arguments)}};t.HttpRequest=function(){function r(r,n,o){this.delegate=r,this.requestCanceled=e(this.requestCanceled,this),this.requestTimedOut=e(this.requestTimedOut,this),this.requestFailed=e(this.requestFailed,this),this.requestLoaded=e(this.requestLoaded,this),this.requestProgressed=e(this.requestProgressed,this),this.url=t.Location.wrap(n).requestURL,this.referrer=t.Location.wrap(o).absoluteURL,this.createXHR()}return r.NETWORK_FAILURE=0,r.TIMEOUT_FAILURE=-1,r.timeout=60,r.prototype.send=function(){var t;return this.xhr&&!this.sent?(this.notifyApplicationBeforeRequestStart(),this.setProgress(0),this.xhr.send(),this.sent=!0,"function"==typeof(t=this.delegate).requestStarted?t.requestStarted():void 0):void 0},r.prototype.cancel=function(){return this.xhr&&this.sent?this.xhr.abort():void 0},r.prototype.requestProgressed=function(t){return t.lengthComputable?this.setProgress(t.loaded/t.total):void 0},r.prototype.requestLoaded=function(){return this.endRequest(function(t){return function(){var e;return 200<=(e=t.xhr.status)&&300>e?t.delegate.requestCompletedWithResponse(t.xhr.responseText,t.xhr.getResponseHeader("Turbolinks-Location")):(t.failed=!0,t.delegate.requestFailedWithStatusCode(t.xhr.status,t.xhr.responseText))}}(this))},r.prototype.requestFailed=function(){return this.endRequest(function(t){return function(){return t.failed=!0,t.delegate.requestFailedWithStatusCode(t.constructor.NETWORK_FAILURE)}}(this))},r.prototype.requestTimedOut=function(){return this.endRequest(function(t){return function(){return t.failed=!0,t.delegate.requestFailedWithStatusCode(t.constructor.TIMEOUT_FAILURE)}}(this))},r.prototype.requestCanceled=function(){return this.endRequest()},r.prototype.notifyApplicationBeforeRequestStart=function(){return t.dispatch("turbolinks:request-start",{data:{url:this.url,xhr:this.xhr}})},r.prototype.notifyApplicationAfterRequestEnd=function(){return t.dispatch("turbolinks:request-end",{data:{url:this.url,xhr:this.xhr}})},r.prototype.createXHR=function(){return this.xhr=new XMLHttpRequest,this.xhr.open("GET",this.url,!0),this.xhr.timeout=1e3*this.constructor.timeout,this.xhr.setRequestHeader("Accept","text/html, application/xhtml+xml"),this.xhr.setRequestHeader("Turbolinks-Referrer",this.referrer),this.xhr.onprogress=this.requestProgressed,this.xhr.onload=this.requestLoaded,this.xhr.onerror=this.requestFailed,this.xhr.ontimeout=this.requestTimedOut,this.xhr.onabort=this.requestCanceled},r.prototype.endRequest=function(t){return this.xhr?(this.notifyApplicationAfterRequestEnd(),null!=t&&t.call(this),this.destroy()):void 0},r.prototype.setProgress=function(t){var e;return this.progress=t,"function"==typeof(e=this.delegate).requestProgressed?e.requestProgressed(this.progress):void 0},r.prototype.destroy=function(){var t;return this.setProgress(1),"function"==typeof(t=this.delegate).requestFinished&&t.requestFinished(),this.delegate=null,this.xhr=null},r}()}.call(this),function(){var e=function(t,e){return function(){return t.apply(e,arguments)}};t.ProgressBar=function(){function t(){this.trickle=e(this.trickle,this),this.stylesheetElement=this.createStylesheetElement(),this.progressElement=this.createProgressElement()}var r;return r=300,t.defaultCSS=".turbolinks-progress-bar {\n position: fixed;\n display: block;\n top: 0;\n left: 0;\n height: 3px;\n background: #0076ff;\n z-index: 9999;\n transition: width "+r+"ms ease-out, opacity "+r/2+"ms "+r/2+"ms ease-in;\n transform: translate3d(0, 0, 0);\n}",t.prototype.show=function(){return this.visible?void 0:(this.visible=!0,this.installStylesheetElement(),this.installProgressElement(),this.startTrickling())},t.prototype.hide=function(){return this.visible&&!this.hiding?(this.hiding=!0,this.fadeProgressElement(function(t){return function(){return t.uninstallProgressElement(),t.stopTrickling(),t.visible=!1,t.hiding=!1}}(this))):void 0},t.prototype.setValue=function(t){return this.value=t,this.refresh()},t.prototype.installStylesheetElement=function(){return document.head.insertBefore(this.stylesheetElement,document.head.firstChild)},t.prototype.installProgressElement=function(){return this.progressElement.style.width=0,this.progressElement.style.opacity=1,document.documentElement.insertBefore(this.progressElement,document.body),this.refresh()},t.prototype.fadeProgressElement=function(t){return this.progressElement.style.opacity=0,setTimeout(t,1.5*r)},t.prototype.uninstallProgressElement=function(){return this.progressElement.parentNode?document.documentElement.removeChild(this.progressElement):void 0},t.prototype.startTrickling=function(){return null!=this.trickleInterval?this.trickleInterval:this.trickleInterval=setInterval(this.trickle,r)},t.prototype.stopTrickling=function(){return clearInterval(this.trickleInterval),this.trickleInterval=null},t.prototype.trickle=function(){return this.setValue(this.value+Math.random()/100)},t.prototype.refresh=function(){return requestAnimationFrame(function(t){return function(){return t.progressElement.style.width=10+90*t.value+"%"}}(this))},t.prototype.createStylesheetElement=function(){var t;return t=document.createElement("style"),t.type="text/css",t.textContent=this.constructor.defaultCSS,t},t.prototype.createProgressElement=function(){var t;return t=document.createElement("div"),t.className="turbolinks-progress-bar",t},t}()}.call(this),function(){var e=function(t,e){return function(){return t.apply(e,arguments)}};t.BrowserAdapter=function(){function r(r){this.controller=r,this.showProgressBar=e(this.showProgressBar,this),this.progressBar=new t.ProgressBar}var n,o,i,s;return s=t.HttpRequest,n=s.NETWORK_FAILURE,i=s.TIMEOUT_FAILURE,o=500,r.prototype.visitProposedToLocationWithAction=function(t,e){return this.controller.startVisitToLocationWithAction(t,e)},r.prototype.visitStarted=function(t){return t.issueRequest(),t.changeHistory(),t.loadCachedSnapshot()},r.prototype.visitRequestStarted=function(t){return this.progressBar.setValue(0),t.hasCachedSnapshot()||"restore"!==t.action?this.showProgressBarAfterDelay():this.showProgressBar()},r.prototype.visitRequestProgressed=function(t){return this.progressBar.setValue(t.progress)},r.prototype.visitRequestCompleted=function(t){return t.loadResponse()},r.prototype.visitRequestFailedWithStatusCode=function(t,e){switch(e){case n:case i:return this.reload();default:return t.loadResponse()}},r.prototype.visitRequestFinished=function(t){return this.hideProgressBar()},r.prototype.visitCompleted=function(t){return t.followRedirect()},r.prototype.pageInvalidated=function(){return this.reload()},r.prototype.showProgressBarAfterDelay=function(){return this.progressBarTimeout=setTimeout(this.showProgressBar,o)},r.prototype.showProgressBar=function(){return this.progressBar.show()},r.prototype.hideProgressBar=function(){return this.progressBar.hide(),clearTimeout(this.progressBarTimeout)},r.prototype.reload=function(){return window.location.reload()},r}()}.call(this),function(){var e,r=function(t,e){return function(){return t.apply(e,arguments)}};e=!1,addEventListener("load",function(){return t.defer(function(){return e=!0})},!1),t.History=function(){function n(t){this.delegate=t,this.onPopState=r(this.onPopState,this)}return n.prototype.start=function(){return this.started?void 0:(addEventListener("popstate",this.onPopState,!1),this.started=!0)},n.prototype.stop=function(){return this.started?(removeEventListener("popstate",this.onPopState,!1),this.started=!1):void 0},n.prototype.push=function(e,r){return e=t.Location.wrap(e),this.update("push",e,r)},n.prototype.replace=function(e,r){return e=t.Location.wrap(e),this.update("replace",e,r)},n.prototype.onPopState=function(e){var r,n,o,i;return this.shouldHandlePopState()&&(i=null!=(n=e.state)?n.turbolinks:void 0)?(r=t.Location.wrap(window.location),o=i.restorationIdentifier,this.delegate.historyPoppedToLocationWithRestorationIdentifier(r,o)):void 0},n.prototype.shouldHandlePopState=function(){return e===!0},n.prototype.update=function(t,e,r){var n;return n={turbolinks:{restorationIdentifier:r}},history[t+"State"](n,null,e)},n}()}.call(this),function(){t.Snapshot=function(){function e(t){var e,r;r=t.head,e=t.body,this.head=null!=r?r:document.createElement("head"),this.body=null!=e?e:document.createElement("body")}return e.wrap=function(t){return t instanceof this?t:this.fromHTML(t)},e.fromHTML=function(t){var e;return e=document.createElement("html"),e.innerHTML=t,this.fromElement(e)},e.fromElement=function(t){return new this({head:t.querySelector("head"),body:t.querySelector("body")})},e.prototype.clone=function(){return new e({head:this.head.cloneNode(!0),body:this.body.cloneNode(!0)})},e.prototype.getRootLocation=function(){var e,r;return r=null!=(e=this.getSetting("root"))?e:"/",new t.Location(r)},e.prototype.getCacheControlValue=function(){return this.getSetting("cache-control")},e.prototype.hasAnchor=function(t){try{return null!=this.body.querySelector("[id='"+t+"']")}catch(e){}},e.prototype.isPreviewable=function(){return"no-preview"!==this.getCacheControlValue()},e.prototype.isCacheable=function(){return"no-cache"!==this.getCacheControlValue()},e.prototype.getSetting=function(t){var e,r;return r=this.head.querySelectorAll("meta[name='turbolinks-"+t+"']"),e=r[r.length-1],null!=e?e.getAttribute("content"):void 0},e}()}.call(this),function(){var e=[].slice;t.Renderer=function(){function t(){}var r;return t.render=function(){var t,r,n,o;return n=arguments[0],r=arguments[1],t=3<=arguments.length?e.call(arguments,2):[],o=function(t,e,r){r.prototype=t.prototype;var n=new r,o=t.apply(n,e);return Object(o)===o?o:n}(this,t,function(){}),o.delegate=n,o.render(r),o},t.prototype.renderView=function(t){return this.delegate.viewWillRender(this.newBody),t(),this.delegate.viewRendered(this.newBody)},t.prototype.invalidateView=function(){return this.delegate.viewInvalidated()},t.prototype.createScriptElement=function(t){var e;return"false"===t.getAttribute("data-turbolinks-eval")?t:(e=document.createElement("script"),e.textContent=t.textContent,r(e,t),e)},r=function(t,e){var r,n,o,i,s,a,u;for(i=e.attributes,a=[],r=0,n=i.length;n>r;r++)s=i[r],o=s.name,u=s.value,a.push(t.setAttribute(o,u));return a},t}()}.call(this),function(){t.HeadDetails=function(){function t(t){var e,r,i,s,a,u,c;for(this.element=t,this.elements={},c=this.element.childNodes,s=0,u=c.length;u>s;s++)i=c[s],i.nodeType===Node.ELEMENT_NODE&&(a=i.outerHTML,r=null!=(e=this.elements)[a]?e[a]:e[a]={type:o(i),tracked:n(i),elements:[]},r.elements.push(i))}var e,r,n,o;return t.prototype.hasElementWithKey=function(t){return t in this.elements},t.prototype.getTrackedElementSignature=function(){var t,e;return function(){var r,n;r=this.elements,n=[];for(t in r)e=r[t].tracked,e&&n.push(t);return n}.call(this).join("")},t.prototype.getScriptElementsNotInDetails=function(t){return this.getElementsMatchingTypeNotInDetails("script",t)},t.prototype.getStylesheetElementsNotInDetails=function(t){return this.getElementsMatchingTypeNotInDetails("stylesheet",t)},t.prototype.getElementsMatchingTypeNotInDetails=function(t,e){var r,n,o,i,s,a;o=this.elements,s=[];for(n in o)i=o[n],a=i.type,r=i.elements,a!==t||e.hasElementWithKey(n)||s.push(r[0]);return s},t.prototype.getProvisionalElements=function(){var t,e,r,n,o,i,s;r=[],n=this.elements;for(e in n)o=n[e],s=o.type,i=o.tracked,t=o.elements,null!=s||i?t.length>1&&r.push.apply(r,t.slice(1)):r.push.apply(r,t);return r},o=function(t){return e(t)?"script":r(t)?"stylesheet":void 0},n=function(t){return"reload"===t.getAttribute("data-turbolinks-track")},e=function(t){var e;return e=t.tagName.toLowerCase(),"script"===e},r=function(t){var e;return e=t.tagName.toLowerCase(),"style"===e||"link"===e&&"stylesheet"===t.getAttribute("rel")},t}()}.call(this),function(){var e=function(t,e){function n(){this.constructor=t}for(var o in e)r.call(e,o)&&(t[o]=e[o]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},r={}.hasOwnProperty;t.SnapshotRenderer=function(r){function n(e,r){this.currentSnapshot=e,this.newSnapshot=r,this.currentHeadDetails=new t.HeadDetails(this.currentSnapshot.head),this.newHeadDetails=new t.HeadDetails(this.newSnapshot.head),this.newBody=this.newSnapshot.body}return e(n,r),n.prototype.render=function(t){return this.trackedElementsAreIdentical()?(this.mergeHead(),this.renderView(function(e){return function(){return e.replaceBody(),e.focusFirstAutofocusableElement(),t()}}(this))):this.invalidateView()},n.prototype.mergeHead=function(){return this.copyNewHeadStylesheetElements(),this.copyNewHeadScriptElements(),this.removeCurrentHeadProvisionalElements(),this.copyNewHeadProvisionalElements()},n.prototype.replaceBody=function(){return this.activateBodyScriptElements(),this.importBodyPermanentElements(),this.assignNewBody()},n.prototype.trackedElementsAreIdentical=function(){return this.currentHeadDetails.getTrackedElementSignature()===this.newHeadDetails.getTrackedElementSignature()},n.prototype.copyNewHeadStylesheetElements=function(){var t,e,r,n,o;for(n=this.getNewHeadStylesheetElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(t));return o},n.prototype.copyNewHeadScriptElements=function(){var t,e,r,n,o;for(n=this.getNewHeadScriptElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(this.createScriptElement(t)));return o},n.prototype.removeCurrentHeadProvisionalElements=function(){var t,e,r,n,o;for(n=this.getCurrentHeadProvisionalElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.removeChild(t));return o},n.prototype.copyNewHeadProvisionalElements=function(){var t,e,r,n,o;for(n=this.getNewHeadProvisionalElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(t));return o},n.prototype.importBodyPermanentElements=function(){var t,e,r,n,o,i;for(n=this.getNewBodyPermanentElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],(t=this.findCurrentBodyPermanentElement(o))?i.push(o.parentNode.replaceChild(t,o)):i.push(void 0);return i},n.prototype.activateBodyScriptElements=function(){var t,e,r,n,o,i;for(n=this.getNewBodyScriptElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],t=this.createScriptElement(o),i.push(o.parentNode.replaceChild(t,o));return i},n.prototype.assignNewBody=function(){return document.body=this.newBody},n.prototype.focusFirstAutofocusableElement=function(){var t;return null!=(t=this.findFirstAutofocusableElement())?t.focus():void 0},n.prototype.getNewHeadStylesheetElements=function(){return this.newHeadDetails.getStylesheetElementsNotInDetails(this.currentHeadDetails)},n.prototype.getNewHeadScriptElements=function(){return this.newHeadDetails.getScriptElementsNotInDetails(this.currentHeadDetails)},n.prototype.getCurrentHeadProvisionalElements=function(){return this.currentHeadDetails.getProvisionalElements()},n.prototype.getNewHeadProvisionalElements=function(){return this.newHeadDetails.getProvisionalElements()},n.prototype.getNewBodyPermanentElements=function(){return this.newBody.querySelectorAll("[id][data-turbolinks-permanent]")},n.prototype.findCurrentBodyPermanentElement=function(t){return document.body.querySelector("#"+t.id+"[data-turbolinks-permanent]")},n.prototype.getNewBodyScriptElements=function(){return this.newBody.querySelectorAll("script")},n.prototype.findFirstAutofocusableElement=function(){return document.body.querySelector("[autofocus]")},n}(t.Renderer)}.call(this),function(){var e=function(t,e){function n(){this.constructor=t}for(var o in e)r.call(e,o)&&(t[o]=e[o]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},r={}.hasOwnProperty;t.ErrorRenderer=function(t){function r(t){this.html=t}return e(r,t),r.prototype.render=function(t){return this.renderView(function(e){return function(){return e.replaceDocumentHTML(),e.activateBodyScriptElements(),t()}}(this))},r.prototype.replaceDocumentHTML=function(){return document.documentElement.innerHTML=this.html},r.prototype.activateBodyScriptElements=function(){var t,e,r,n,o,i;for(n=this.getScriptElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],t=this.createScriptElement(o),i.push(o.parentNode.replaceChild(t,o));return i},r.prototype.getScriptElements=function(){return document.documentElement.querySelectorAll("script")},r}(t.Renderer)}.call(this),function(){t.View=function(){function e(t){this.delegate=t,this.element=document.documentElement}return e.prototype.getRootLocation=function(){return this.getSnapshot().getRootLocation()},e.prototype.getSnapshot=function(){return t.Snapshot.fromElement(this.element)},e.prototype.render=function(t,e){var r,n,o;return o=t.snapshot,r=t.error,n=t.isPreview,this.markAsPreview(n),null!=o?this.renderSnapshot(o,e):this.renderError(r,e)},e.prototype.markAsPreview=function(t){return t?this.element.setAttribute("data-turbolinks-preview",""):this.element.removeAttribute("data-turbolinks-preview")},e.prototype.renderSnapshot=function(e,r){return t.SnapshotRenderer.render(this.delegate,r,this.getSnapshot(),t.Snapshot.wrap(e))},e.prototype.renderError=function(e,r){return t.ErrorRenderer.render(this.delegate,r,e)},e}()}.call(this),function(){var e=function(t,e){return function(){return t.apply(e,arguments)}};t.ScrollManager=function(){function t(t){this.delegate=t,this.onScroll=e(this.onScroll,this)}return t.prototype.start=function(){return this.started?void 0:(addEventListener("scroll",this.onScroll,!1),this.onScroll(),this.started=!0)},t.prototype.stop=function(){return this.started?(removeEventListener("scroll",this.onScroll,!1),this.started=!1):void 0},t.prototype.scrollToElement=function(t){return t.scrollIntoView()},t.prototype.scrollToPosition=function(t){var e,r;return e=t.x,r=t.y,window.scrollTo(e,r)},t.prototype.onScroll=function(t){return this.updatePosition({x:window.pageXOffset,y:window.pageYOffset})},t.prototype.updatePosition=function(t){var e;return this.position=t,null!=(e=this.delegate)?e.scrollPositionChanged(this.position):void 0},t}()}.call(this),function(){t.SnapshotCache=function(){function e(t){this.size=t,this.keys=[],this.snapshots={}}var r;return e.prototype.has=function(t){var e;return e=r(t),e in this.snapshots},e.prototype.get=function(t){var e;if(this.has(t))return e=this.read(t),this.touch(t),e},e.prototype.put=function(t,e){return this.write(t,e),this.touch(t),e},e.prototype.read=function(t){var e;return e=r(t),this.snapshots[e]},e.prototype.write=function(t,e){var n;return n=r(t),this.snapshots[n]=e},e.prototype.touch=function(t){var e,n;return n=r(t),e=this.keys.indexOf(n),e>-1&&this.keys.splice(e,1),this.keys.unshift(n),this.trim()},e.prototype.trim=function(){var t,e,r,n,o;for(n=this.keys.splice(this.size),o=[],t=0,r=n.length;r>t;t++)e=n[t],o.push(delete this.snapshots[e]);return o},r=function(e){return t.Location.wrap(e).toCacheKey()},e}()}.call(this),function(){var e=function(t,e){return function(){return t.apply(e,arguments)}};t.Visit=function(){function r(r,n,o){this.controller=r,this.action=o,this.performScroll=e(this.performScroll,this),this.identifier=t.uuid(),this.location=t.Location.wrap(n),this.adapter=this.controller.adapter,this.state="initialized",this.timingMetrics={}}var n;return r.prototype.start=function(){return"initialized"===this.state?(this.recordTimingMetric("visitStart"),this.state="started",this.adapter.visitStarted(this)):void 0},r.prototype.cancel=function(){var t;return"started"===this.state?(null!=(t=this.request)&&t.cancel(),this.cancelRender(),this.state="canceled"):void 0},r.prototype.complete=function(){var t;return"started"===this.state?(this.recordTimingMetric("visitEnd"),this.state="completed","function"==typeof(t=this.adapter).visitCompleted&&t.visitCompleted(this),this.controller.visitCompleted(this)):void 0},r.prototype.fail=function(){var t;return"started"===this.state?(this.state="failed","function"==typeof(t=this.adapter).visitFailed?t.visitFailed(this):void 0):void 0},r.prototype.changeHistory=function(){var t,e;return this.historyChanged?void 0:(t=this.location.isEqualTo(this.referrer)?"replace":this.action,e=n(t),this.controller[e](this.location,this.restorationIdentifier),this.historyChanged=!0)},r.prototype.issueRequest=function(){return this.shouldIssueRequest()&&null==this.request?(this.progress=0,this.request=new t.HttpRequest(this,this.location,this.referrer),this.request.send()):void 0},r.prototype.getCachedSnapshot=function(){var t;return!(t=this.controller.getCachedSnapshotForLocation(this.location))||null!=this.location.anchor&&!t.hasAnchor(this.location.anchor)||"restore"!==this.action&&!t.isPreviewable()?void 0:t},r.prototype.hasCachedSnapshot=function(){return null!=this.getCachedSnapshot()},r.prototype.loadCachedSnapshot=function(){var t,e;return(e=this.getCachedSnapshot())?(t=this.shouldIssueRequest(),this.render(function(){var r;return this.cacheSnapshot(),this.controller.render({snapshot:e,isPreview:t},this.performScroll),"function"==typeof(r=this.adapter).visitRendered&&r.visitRendered(this),t?void 0:this.complete()})):void 0},r.prototype.loadResponse=function(){return null!=this.response?this.render(function(){var t,e;return this.cacheSnapshot(),this.request.failed?(this.controller.render({error:this.response},this.performScroll),"function"==typeof(t=this.adapter).visitRendered&&t.visitRendered(this),this.fail()):(this.controller.render({snapshot:this.response},this.performScroll),"function"==typeof(e=this.adapter).visitRendered&&e.visitRendered(this),this.complete())}):void 0},r.prototype.followRedirect=function(){return this.redirectedToLocation&&!this.followedRedirect?(this.location=this.redirectedToLocation,this.controller.replaceHistoryWithLocationAndRestorationIdentifier(this.redirectedToLocation,this.restorationIdentifier),this.followedRedirect=!0):void 0},r.prototype.requestStarted=function(){var t;return this.recordTimingMetric("requestStart"),"function"==typeof(t=this.adapter).visitRequestStarted?t.visitRequestStarted(this):void 0},r.prototype.requestProgressed=function(t){var e;return this.progress=t,"function"==typeof(e=this.adapter).visitRequestProgressed?e.visitRequestProgressed(this):void 0},r.prototype.requestCompletedWithResponse=function(e,r){return this.response=e,null!=r&&(this.redirectedToLocation=t.Location.wrap(r)),this.adapter.visitRequestCompleted(this)},r.prototype.requestFailedWithStatusCode=function(t,e){return this.response=e,this.adapter.visitRequestFailedWithStatusCode(this,t)},r.prototype.requestFinished=function(){var t;return this.recordTimingMetric("requestEnd"),"function"==typeof(t=this.adapter).visitRequestFinished?t.visitRequestFinished(this):void 0},r.prototype.performScroll=function(){return this.scrolled?void 0:("restore"===this.action?this.scrollToRestoredPosition()||this.scrollToTop():this.scrollToAnchor()||this.scrollToTop(),this.scrolled=!0)},r.prototype.scrollToRestoredPosition=function(){var t,e;return t=null!=(e=this.restorationData)?e.scrollPosition:void 0,null!=t?(this.controller.scrollToPosition(t),!0):void 0},r.prototype.scrollToAnchor=function(){return null!=this.location.anchor?(this.controller.scrollToAnchor(this.location.anchor),!0):void 0},r.prototype.scrollToTop=function(){return this.controller.scrollToPosition({x:0,y:0})},r.prototype.recordTimingMetric=function(t){var e;return null!=(e=this.timingMetrics)[t]?e[t]:e[t]=(new Date).getTime()},r.prototype.getTimingMetrics=function(){return t.copyObject(this.timingMetrics)},n=function(t){switch(t){case"replace":return"replaceHistoryWithLocationAndRestorationIdentifier";case"advance":case"restore":return"pushHistoryWithLocationAndRestorationIdentifier"}},r.prototype.shouldIssueRequest=function(){return"restore"===this.action?!this.hasCachedSnapshot():!0},r.prototype.cacheSnapshot=function(){return this.snapshotCached?void 0:(this.controller.cacheSnapshot(),this.snapshotCached=!0)},r.prototype.render=function(t){return this.cancelRender(),this.frame=requestAnimationFrame(function(e){return function(){return e.frame=null,t.call(e)}}(this))},r.prototype.cancelRender=function(){return this.frame?cancelAnimationFrame(this.frame):void 0},r}()}.call(this),function(){var e=function(t,e){return function(){return t.apply(e,arguments)}};t.Controller=function(){function r(){this.clickBubbled=e(this.clickBubbled,this),this.clickCaptured=e(this.clickCaptured,this),this.pageLoaded=e(this.pageLoaded,this),this.history=new t.History(this),this.view=new t.View(this),this.scrollManager=new t.ScrollManager(this),this.restorationData={},this.clearCache()}return r.prototype.start=function(){return t.supported&&!this.started?(addEventListener("click",this.clickCaptured,!0),addEventListener("DOMContentLoaded",this.pageLoaded,!1),this.scrollManager.start(),this.startHistory(),this.started=!0,this.enabled=!0):void 0},r.prototype.disable=function(){return this.enabled=!1},r.prototype.stop=function(){return this.started?(removeEventListener("click",this.clickCaptured,!0),removeEventListener("DOMContentLoaded",this.pageLoaded,!1),this.scrollManager.stop(),this.stopHistory(),this.started=!1):void 0},r.prototype.clearCache=function(){return this.cache=new t.SnapshotCache(10)},r.prototype.visit=function(e,r){var n,o;return null==r&&(r={}),e=t.Location.wrap(e),this.applicationAllowsVisitingLocation(e)?this.locationIsVisitable(e)?(n=null!=(o=r.action)?o:"advance",this.adapter.visitProposedToLocationWithAction(e,n)):window.location=e:void 0},r.prototype.startVisitToLocationWithAction=function(e,r,n){var o;return t.supported?(o=this.getRestorationDataForIdentifier(n),this.startVisit(e,r,{restorationData:o})):window.location=e},r.prototype.startHistory=function(){return this.location=t.Location.wrap(window.location),this.restorationIdentifier=t.uuid(),this.history.start(),this.history.replace(this.location,this.restorationIdentifier)},r.prototype.stopHistory=function(){return this.history.stop()},r.prototype.pushHistoryWithLocationAndRestorationIdentifier=function(e,r){return this.restorationIdentifier=r,this.location=t.Location.wrap(e),this.history.push(this.location,this.restorationIdentifier)},r.prototype.replaceHistoryWithLocationAndRestorationIdentifier=function(e,r){return this.restorationIdentifier=r,this.location=t.Location.wrap(e),this.history.replace(this.location,this.restorationIdentifier)},r.prototype.historyPoppedToLocationWithRestorationIdentifier=function(e,r){var n;return this.restorationIdentifier=r,this.enabled?(n=this.getRestorationDataForIdentifier(this.restorationIdentifier),this.startVisit(e,"restore",{restorationIdentifier:this.restorationIdentifier,restorationData:n,historyChanged:!0}),this.location=t.Location.wrap(e)):this.adapter.pageInvalidated()},r.prototype.getCachedSnapshotForLocation=function(t){var e;return e=this.cache.get(t),e?e.clone():void 0},r.prototype.shouldCacheSnapshot=function(){return this.view.getSnapshot().isCacheable()},r.prototype.cacheSnapshot=function(){var t;return this.shouldCacheSnapshot()?(this.notifyApplicationBeforeCachingSnapshot(),t=this.view.getSnapshot(),this.cache.put(this.lastRenderedLocation,t.clone())):void 0},r.prototype.scrollToAnchor=function(t){var e;return(e=document.getElementById(t))?this.scrollToElement(e):this.scrollToPosition({x:0,y:0})},r.prototype.scrollToElement=function(t){return this.scrollManager.scrollToElement(t)},r.prototype.scrollToPosition=function(t){return this.scrollManager.scrollToPosition(t)},r.prototype.scrollPositionChanged=function(t){var e;return e=this.getCurrentRestorationData(),e.scrollPosition=t},r.prototype.render=function(t,e){return this.view.render(t,e)},r.prototype.viewInvalidated=function(){return this.adapter.pageInvalidated()},r.prototype.viewWillRender=function(t){return this.notifyApplicationBeforeRender(t)},r.prototype.viewRendered=function(){return this.lastRenderedLocation=this.currentVisit.location,this.notifyApplicationAfterRender()},r.prototype.pageLoaded=function(){return this.lastRenderedLocation=this.location,this.notifyApplicationAfterPageLoad()},r.prototype.clickCaptured=function(){return removeEventListener("click",this.clickBubbled,!1),addEventListener("click",this.clickBubbled,!1)},r.prototype.clickBubbled=function(t){var e,r,n;return this.enabled&&this.clickEventIsSignificant(t)&&(r=this.getVisitableLinkForNode(t.target))&&(n=this.getVisitableLocationForLink(r))&&this.applicationAllowsFollowingLinkToLocation(r,n)?(t.preventDefault(),e=this.getActionForLink(r),this.visit(n,{action:e})):void 0},r.prototype.applicationAllowsFollowingLinkToLocation=function(t,e){var r;return r=this.notifyApplicationAfterClickingLinkToLocation(t,e),!r.defaultPrevented},r.prototype.applicationAllowsVisitingLocation=function(t){var e;return e=this.notifyApplicationBeforeVisitingLocation(t),!e.defaultPrevented},r.prototype.notifyApplicationAfterClickingLinkToLocation=function(e,r){return t.dispatch("turbolinks:click",{target:e,data:{url:r.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationBeforeVisitingLocation=function(e){return t.dispatch("turbolinks:before-visit",{data:{url:e.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationAfterVisitingLocation=function(e){return t.dispatch("turbolinks:visit",{data:{url:e.absoluteURL}})},r.prototype.notifyApplicationBeforeCachingSnapshot=function(){return t.dispatch("turbolinks:before-cache")},r.prototype.notifyApplicationBeforeRender=function(e){return t.dispatch("turbolinks:before-render",{data:{newBody:e}})},r.prototype.notifyApplicationAfterRender=function(){return t.dispatch("turbolinks:render")},r.prototype.notifyApplicationAfterPageLoad=function(e){return null==e&&(e={}),t.dispatch("turbolinks:load",{data:{url:this.location.absoluteURL,timing:e}})},r.prototype.startVisit=function(t,e,r){var n;return null!=(n=this.currentVisit)&&n.cancel(),this.currentVisit=this.createVisit(t,e,r),this.currentVisit.start(),this.notifyApplicationAfterVisitingLocation(t)},r.prototype.createVisit=function(e,r,n){ +var o,i,s,a,u;return i=null!=n?n:{},a=i.restorationIdentifier,s=i.restorationData,o=i.historyChanged,u=new t.Visit(this,e,r),u.restorationIdentifier=null!=a?a:t.uuid(),u.restorationData=t.copyObject(s),u.historyChanged=o,u.referrer=this.location,u},r.prototype.visitCompleted=function(t){return this.notifyApplicationAfterPageLoad(t.getTimingMetrics())},r.prototype.clickEventIsSignificant=function(t){return!(t.defaultPrevented||t.target.isContentEditable||t.which>1||t.altKey||t.ctrlKey||t.metaKey||t.shiftKey)},r.prototype.getVisitableLinkForNode=function(e){return this.nodeIsVisitable(e)?t.closest(e,"a[href]:not([target])"):void 0},r.prototype.getVisitableLocationForLink=function(e){var r;return r=new t.Location(e.getAttribute("href")),this.locationIsVisitable(r)?r:void 0},r.prototype.getActionForLink=function(t){var e;return null!=(e=t.getAttribute("data-turbolinks-action"))?e:"advance"},r.prototype.nodeIsVisitable=function(e){var r;return(r=t.closest(e,"[data-turbolinks]"))?"false"!==r.getAttribute("data-turbolinks"):!0},r.prototype.locationIsVisitable=function(t){return t.isPrefixedBy(this.view.getRootLocation())&&t.isHTML()},r.prototype.getCurrentRestorationData=function(){return this.getRestorationDataForIdentifier(this.restorationIdentifier)},r.prototype.getRestorationDataForIdentifier=function(t){var e;return null!=(e=this.restorationData)[t]?e[t]:e[t]={}},r}()}.call(this),function(){var e,r,n;t.start=function(){return r()?(null==t.controller&&(t.controller=e()),t.controller.start()):void 0},r=function(){return null==window.Turbolinks&&(window.Turbolinks=t),n()},e=function(){var e;return e=new t.Controller,e.adapter=new t.BrowserAdapter(e),e},n=function(){return window.Turbolinks===t},n()&&t.start()}.call(this)}).call(this),"object"==typeof module&&module.exports?module.exports=t:false}).call(this); \ No newline at end of file diff --git a/docs/[PRO]-Recording-and-replaying-keep-alive-TCP-sessions.md b/docs/pro/recording-and-replaying-keep-alive-tcp-sessions.md similarity index 100% rename from docs/[PRO]-Recording-and-replaying-keep-alive-TCP-sessions.md rename to docs/pro/recording-and-replaying-keep-alive-tcp-sessions.md diff --git a/docs/[PRO]-Replaying-Binary-protocols.md b/docs/pro/replaying-binary-protocols.md similarity index 100% rename from docs/[PRO]-Replaying-Binary-protocols.md rename to docs/pro/replaying-binary-protocols.md diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c68ea1d --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,2 @@ +site_name: My Docs +theme: readthedocs diff --git a/sidenav.css b/sidenav.css new file mode 100644 index 0000000..5a1e37e --- /dev/null +++ b/sidenav.css @@ -0,0 +1,435 @@ +.wy-affix { + position: fixed; + top: 1.618em; +} + +.wy-menu a:hover { + text-decoration: none; +} + +.wy-menu-vertical header, +.wy-menu-vertical p.caption { + height: 32px; + display: inline-block; + line-height: 32px; + padding: 0 1.618em; + margin-bottom: 0; + margin-top: 14px; + display: block; + font-weight: 700; + text-transform: uppercase; + font-size: 85%; + color: #ccc; + white-space: nowrap; +} + +.wy-menu-vertical span { + color: #666; +} + +.wy-menu-vertical ul { + margin-bottom: 0; +} + +.wy-menu-vertical li.divide-top { + border-top: solid 1px #404040; +} + +.wy-menu-vertical li.divide-bottom { + border-bottom: solid 1px #404040; +} + +.wy-menu-vertical li.current { + background-color: #e5e5e5; +} + +.wy-menu-vertical li.current a { + color: rgba(0, 93, 255, 0.7); + border-right: none; +} + +.wy-menu-vertical li.current a:hover { + color: rgba(0, 93, 255, 0.9); +} + +.wy-menu-vertical li code, +.wy-menu-vertical li .rst-content tt, +.rst-content .wy-menu-vertical li tt { + border: none; + background: inherit; + color: inherit; + padding-left: 0; + padding-right: 0 +} + +.wy-menu-vertical li span.toctree-expand { + display: block; + float: left; + margin-left: -1.2em; + font-size: .8em; + line-height: 1.6em; + color: #999; +} + +.wy-menu-vertical li.on a, +.wy-menu-vertical li.current>a { + color: rgba(0, 93, 255, 0.9); + font-weight: 700; + position: relative; + background: #fafafa; + border: none; +} + +/*.wy-menu-vertical li.on a:hover span.toctree-expand, +.wy-menu-vertical li.current>a:hover span.toctree-expand { + color: gray; +}*/ + +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand { + display: block; + font-size: .8em; + line-height: 1.6em; + color: #333; +} + +.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul, +.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul { + display: none; +} + +.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul, +.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul { + display: block; +} + +.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a { + display: block; + padding: .4045em 4.045em; +} + +.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand { + color: #999; +} + +.wy-menu-vertical li.toctree-l2 span.toctree-expand { + color: #999; +} + +.wy-menu-vertical li.toctree-l3 { + background-color: #eee; + font-size: .9em; +} + +.wy-menu-vertical li.toctree-l3.current>a { + padding: .4045em 4.045em; +} + +.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a { + display: block; + padding: .4045em 5.663em; + border-top: none; + border-bottom: none; +} + +.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand { + color: rgba(0, 93, 255, 0.9); +} + +.wy-menu-vertical li.toctree-l3 span.toctree-expand { + color: #999; +} + +.wy-menu-vertical li.toctree-l4 { + font-size: .9em; +} + +.wy-menu-vertical li.current ul { + display: block; +} + +.wy-menu-vertical .local-toc li ul { + display: block; +} + +.wy-menu-vertical li ul li a { + margin-bottom: 0; + color: rgba(0, 93, 255, 0.7); + font-weight: 400; +} + +.wy-menu-vertical a { + display: inline-block; + line-height: 18px; + padding: .4045em 1.618em; + display: block; + position: relative; + font-size: 90%; + color: rgba(0, 93, 255, 0.7); +} + +.wy-menu-vertical li.on a:hover, +.wy-menu-vertical li.current>a:hover { + background-color: #fafafa; +} + +.wy-menu-vertical a:hover { + color: rgba(0, 93, 255, 0.9); + cursor: pointer; + background-color: #fafafa; +} + +.wy-menu-vertical a:hover span.toctree-expand { + color: rgba(0, 93, 255, 0.5); +} + +.wy-menu-vertical a:active span.toctree-expand { + color: rgba(0, 93, 255, 0.7); +} + +/* Search */ + +.wy-side-nav-search { + z-index: 200; + background-color: #fafafa; + border-bottom: #333; + text-align: center; + padding: .809em; + display: block; + color: #333; + margin-bottom: .809em +} + +.wy-side-nav-search input[type=text] { + width: 100%; + color: #333; + border-radius: 3px; + outline: 0; + padding: 10px; + background-color: #fff; + border: solid 1px #6d6d6d; + box-shadow: none +} + +.wy-side-nav-search img { + display: block; + margin: auto auto .809em; + height: 45px; + width: 45px; + background-color: #2980B9; + padding: 5px; + border-radius: 100% +} + +.wy-side-nav-search>a, +.wy-side-nav-search .wy-dropdown>a { + color: #333; + font-size: 100%; + font-weight: 700; + display: inline-block; + padding: 4px 6px; + margin-bottom: .809em +} + +.wy-side-nav-search>a:hover, +.wy-side-nav-search .wy-dropdown>a:hover { + background: rgba(255,255,255,0.1) +} + +.wy-side-nav-search>a img.logo, +.wy-side-nav-search .wy-dropdown>a img.logo { + display: block; + margin: 0 auto; + height: auto; + width: auto; + border-radius: 0; + max-width: 100%; + background: transparent +} + +.wy-side-nav-search>a.icon img.logo, +.wy-side-nav-search .wy-dropdown>a.icon img.logo { + margin-top: .85em +} + +.wy-nav .wy-menu-vertical header { + color: #2980B9 +} + +.wy-nav .wy-menu-vertical a { + color: #b3b3b3 +} + +.wy-nav .wy-menu-vertical a:hover { + background-color: #2980B9; + color: #fff +} + +[data-menu-wrap] { + -webkit-transition: all .2s ease-in; + -moz-transition: all .2s ease-in; + transition: all .2s ease-in; + position: absolute; + opacity: 1; + width: 100%; + opacity: 0 +} + +[data-menu-wrap].move-center { + left: 0; + right: auto; + opacity: 1 +} + +[data-menu-wrap].move-left { + right: auto; + left: -100%; + opacity: 0 +} + +[data-menu-wrap].move-right { + right: -100%; + left: auto; + opacity: 0 +} + +.wy-body-for-nav { + background: left repeat-y #fcfcfc; + background-image: url(data:image/png; + base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC); + background-size: 300px 1px +} + +.wy-grid-for-nav { + position: absolute; + width: 100%; + height: 100% +} + +.wy-nav-side { + position: fixed; + top: 0; + bottom: 0; + left: 0; + padding-bottom: 2em; + width: 300px; + overflow-x: hidden; + overflow-y: scroll; + min-height: 100%; + background-color: #fafafa; + z-index: 200; + border-right: 2px solid #eee; +} + +.wy-nav-top { + display: none; + background-color: #333; + color: #fff; + padding: .4045em .809em; + position: relative; + line-height: 50px; + text-align: center; + font-size: 100%; + *zoom: 1 +} + +.wy-nav-top:before, +.wy-nav-top:after { + display: table; + content: "" +} + +.wy-nav-top:after { + clear: both +} + +.wy-nav-top a { + color: #fff; + font-weight: 700 +} + +.wy-nav-top img { + margin-right: 12px; + height: 45px; + width: 45px; + background-color: #2980B9; + padding: 5px; + border-radius: 100% +} + +.wy-nav-top i { + font-size: 30px; + line-height: 50px; + float: left; + cursor: pointer +} + +.wy-nav-content-wrap { + margin-left: 300px; + background: #fcfcfc; + min-height: 100% +} + +.wy-nav-content { + padding: 1.618em 3.236em; + height: 100%; + max-width: 1100px; + margin: auto +} + +.wy-body-mask { + position: fixed; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.2); + display: none; + z-index: 499 +} + +.wy-body-mask.on { + display: block +} + +@media screen and (max-width: 768px) { + .wy-body-for-nav { + background: #fcfcfc + } + + .wy-nav-top { + display: block + } + + .wy-nav-side { + left: -300px + } + + .wy-nav-side.shift { + width: 85%; + left: 0 + } + + .wy-nav-content-wrap { + margin-left: 0 + } + + .wy-nav-content-wrap .wy-nav-content { + padding: 1.618em + } + + .wy-nav-content-wrap.shift { + position: fixed; + min-width: 100%; + left: 85%; + top: 0; + height: 100%; + overflow: hidden + } + +} + +@media screen and (min-width: 1400px) { + .wy-nav-content { + margin: 0; + background: #fcfcfc + } +} \ No newline at end of file diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..45c1505 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,3 @@ +_site +.sass-cache +.jekyll-metadata diff --git a/site/Gemfile b/site/Gemfile new file mode 100644 index 0000000..5202882 --- /dev/null +++ b/site/Gemfile @@ -0,0 +1,24 @@ +source "https://rubygems.org" +ruby RUBY_VERSION + +# Hello! This is where you manage which Jekyll version is used to run. +# When you want to use a different version, change it below, save the +# file and run `bundle install`. Run Jekyll with `bundle exec`, like so: +# +# bundle exec jekyll serve +# +# This will help ensure the proper Jekyll version is running. +# Happy Jekylling! +gem "jekyll", "3.3.1" + +# This is the default theme for new Jekyll sites. You may change this to anything you like. +gem "minima", "~> 2.0" + +# If you want to use GitHub Pages, remove the "gem "jekyll"" above and +# uncomment the line below. To upgrade, run `bundle update github-pages`. +# gem "github-pages", group: :jekyll_plugins + +# If you have any plugins, put them here! +group :jekyll_plugins do + gem "jekyll-feed", "~> 0.6" +end diff --git a/site/Gemfile.lock b/site/Gemfile.lock new file mode 100644 index 0000000..946b69f --- /dev/null +++ b/site/Gemfile.lock @@ -0,0 +1,56 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.5.0) + public_suffix (~> 2.0, >= 2.0.2) + colorator (1.1.0) + ffi (1.9.14) + forwardable-extended (2.6.0) + jekyll (3.3.1) + addressable (~> 2.4) + colorator (~> 1.0) + jekyll-sass-converter (~> 1.0) + jekyll-watch (~> 1.1) + kramdown (~> 1.3) + liquid (~> 3.0) + mercenary (~> 0.3.3) + pathutil (~> 0.9) + rouge (~> 1.7) + safe_yaml (~> 1.0) + jekyll-feed (0.8.0) + jekyll (~> 3.3) + jekyll-sass-converter (1.5.0) + sass (~> 3.4) + jekyll-watch (1.5.0) + listen (~> 3.0, < 3.1) + kramdown (1.13.1) + liquid (3.0.6) + listen (3.0.8) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + mercenary (0.3.6) + minima (2.1.0) + jekyll (~> 3.3) + pathutil (0.14.0) + forwardable-extended (~> 2.6) + public_suffix (2.0.5) + rb-fsevent (0.9.8) + rb-inotify (0.9.7) + ffi (>= 0.5.0) + rouge (1.11.1) + safe_yaml (1.0.4) + sass (3.4.23) + +PLATFORMS + ruby + +DEPENDENCIES + jekyll (= 3.3.1) + jekyll-feed (~> 0.6) + minima (~> 2.0) + +RUBY VERSION + ruby 2.3.1p112 + +BUNDLED WITH + 1.13.7 diff --git a/site/_config.yml b/site/_config.yml new file mode 100644 index 0000000..f5df983 --- /dev/null +++ b/site/_config.yml @@ -0,0 +1,34 @@ +# Welcome to Jekyll! +# +# This config file is meant for settings that affect your whole blog, values +# which you are expected to set up once and rarely edit after that. If you find +# yourself editing this file very often, consider using Jekyll's data files +# feature for the data you need to update frequently. +# +# For technical reasons, this file is *NOT* reloaded automatically when you use +# 'bundle exec jekyll serve'. If you change this file, please restart the server process. + +# Site settings +# These are used to personalize your new site. If you look in the HTML files, +# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. +# You can create any custom variable you would like, and they will be accessible +# in the templates via {{ site.myvariable }}. +title: Your awesome title +email: your-email@domain.com +description: > # this means to ignore newlines until "baseurl:" + Write an awesome description for your new site here. You can edit this + line in _config.yml. It will appear in your document head meta (for + Google search results) and in your feed.xml site description. +baseurl: "" # the subpath of your site, e.g. /blog +url: "" # the base hostname & protocol for your site, e.g. http://example.com +twitter_username: jekyllrb +github_username: jekyll + +# Build settings +markdown: kramdown +theme: minima +gems: + - jekyll-feed +exclude: + - Gemfile + - Gemfile.lock diff --git a/site/_posts/2017-01-06-welcome-to-jekyll.markdown b/site/_posts/2017-01-06-welcome-to-jekyll.markdown new file mode 100644 index 0000000..d68a8ab --- /dev/null +++ b/site/_posts/2017-01-06-welcome-to-jekyll.markdown @@ -0,0 +1,25 @@ +--- +layout: post +title: "Welcome to Jekyll!" +date: 2017-01-06 11:19:34 +0300 +categories: jekyll update +--- +You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated. + +To add new posts, simply add a file in the `_posts` directory that follows the convention `YYYY-MM-DD-name-of-post.ext` and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works. + +Jekyll also offers powerful support for code snippets: + +{% highlight ruby %} +def print_hi(name) + puts "Hi, #{name}" +end +print_hi('Tom') +#=> prints 'Hi, Tom' to STDOUT. +{% endhighlight %} + +Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk]. + +[jekyll-docs]: http://jekyllrb.com/docs/home +[jekyll-gh]: https://github.com/jekyll/jekyll +[jekyll-talk]: https://talk.jekyllrb.com/ diff --git a/site/about.md b/site/about.md new file mode 100644 index 0000000..19d6b97 --- /dev/null +++ b/site/about.md @@ -0,0 +1,15 @@ +--- +layout: page +title: About +permalink: /about/ +--- + +This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](http://jekyllrb.com/) + +You can find the source code for the Jekyll new theme at: +{% include icon-github.html username="jekyll" %} / +[minima](https://github.com/jekyll/minima) + +You can find the source code for Jekyll at +{% include icon-github.html username="jekyll" %} / +[jekyll](https://github.com/jekyll/jekyll) diff --git a/site/index.md b/site/index.md new file mode 100644 index 0000000..1eb5d67 --- /dev/null +++ b/site/index.md @@ -0,0 +1,6 @@ +--- +# You don't need to edit this file, it's empty on purpose. +# Edit theme's home layout instead if you wanna make some changes +# See: https://jekyllrb.com/docs/themes/#overriding-theme-defaults +layout: home +--- From def3c91898f62e1feef07f687bf4b3a09864560b Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 20 Jan 2017 19:13:30 +0400 Subject: [PATCH 18/19] Add Kafka tests and raw format mode (#415) * Add tests and send messages in raw format by default --- output_kafka.go | 68 ++++++++++++++++++++++++++++++-------------- output_kafka_test.go | 53 ++++++++++++++++++++++++++++++++++ settings.go | 1 + 3 files changed, 100 insertions(+), 22 deletions(-) create mode 100644 output_kafka_test.go diff --git a/output_kafka.go b/output_kafka.go index 4ebfc4c..23ba31a 100644 --- a/output_kafka.go +++ b/output_kafka.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "github.com/Shopify/sarama" + "github.com/Shopify/sarama/mocks" "github.com/buger/gor/proto" "io" "log" @@ -13,8 +14,10 @@ import ( // KafkaConfig should contains required information to // build producers. type KafkaConfig struct { - host string - topic string + host string + topic string + producer sarama.AsyncProducer + useJSON bool } // KafkaOutput should make producer client. @@ -27,6 +30,9 @@ type KafkaOutput struct { // passed as Json to Apache Kafka. type KafkaMessage struct { ReqURL string `json:"Req_URL"` + ReqType string `json:"Req_Type"` + ReqID string `json:"Req_ID"` + ReqTs string `json:"Req_Ts"` ReqMethod string `json:"Req_Method"` ReqBody string `json:"Req_Body,omitempty"` ReqHeaders map[string]string `json:"Req_Headers,omitempty"` @@ -38,15 +44,23 @@ const KafkaOutputFrequency = 500 // NewKafkaOutput creates instance of kafka producer client. func NewKafkaOutput(address string, config *KafkaConfig) io.Writer { c := sarama.NewConfig() - c.Producer.RequiredAcks = sarama.WaitForLocal - c.Producer.Compression = sarama.CompressionSnappy - c.Producer.Flush.Frequency = KafkaOutputFrequency * time.Millisecond - brokerList := strings.Split(config.host, ",") + var producer sarama.AsyncProducer - producer, err := sarama.NewAsyncProducer(brokerList, c) - if err != nil { - log.Fatalln("Failed to start Sarama(Kafka) producer:", err) + if config.producer.(*mocks.AsyncProducer) != nil { + producer = config.producer + } else { + c.Producer.RequiredAcks = sarama.WaitForLocal + c.Producer.Compression = sarama.CompressionSnappy + c.Producer.Flush.Frequency = KafkaOutputFrequency * time.Millisecond + + brokerList := strings.Split(config.host, ",") + + var err error + producer, err = sarama.NewAsyncProducer(brokerList, c) + if err != nil { + log.Fatalln("Failed to start Sarama(Kafka) producer:", err) + } } o := &KafkaOutput{ @@ -70,22 +84,32 @@ func (o *KafkaOutput) ErrorHandler() { } func (o *KafkaOutput) Write(data []byte) (n int, err error) { - headers := make(map[string]string) - proto.ParseHeaders([][]byte{data}, func(header []byte, value []byte) bool { - headers[string(header)] = string(value) - return true - }) + var message sarama.StringEncoder - req := payloadBody(data) + if !o.config.useJSON { + message = sarama.StringEncoder(data) + } else { + headers := make(map[string]string) + proto.ParseHeaders([][]byte{data}, func(header []byte, value []byte) bool { + headers[string(header)] = string(value) + return true + }) - kafkaMessage := KafkaMessage{ - ReqURL: string(proto.Path(req)), - ReqMethod: string(proto.Method(req)), - ReqBody: string(proto.Body(req)), - ReqHeaders: headers, + meta := payloadMeta(data) + req := payloadBody(data) + + kafkaMessage := KafkaMessage{ + ReqURL: string(proto.Path(req)), + ReqType: string(meta[0]), + ReqID: string(meta[1]), + ReqTs: string(meta[2]), + ReqMethod: string(proto.Method(req)), + ReqBody: string(proto.Body(req)), + ReqHeaders: headers, + } + jsonMessage, _ := json.Marshal(&kafkaMessage) + message = sarama.StringEncoder(jsonMessage) } - jsonMessage, _ := json.Marshal(&kafkaMessage) - message := sarama.StringEncoder(jsonMessage) o.producer.Input() <- &sarama.ProducerMessage{ Topic: o.config.topic, diff --git a/output_kafka_test.go b/output_kafka_test.go new file mode 100644 index 0000000..cf9efe0 --- /dev/null +++ b/output_kafka_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "github.com/Shopify/sarama" + "github.com/Shopify/sarama/mocks" + "testing" +) + +func TestOutputKafkaRAW(t *testing.T) { + config := sarama.NewConfig() + config.Producer.Return.Successes = true + producer := mocks.NewAsyncProducer(t, config) + producer.ExpectInputAndSucceed() + + output := NewKafkaOutput("", &KafkaConfig{ + producer: producer, + topic: "test", + useJSON: false, + }) + + output.Write([]byte("1 2 3\nGET / HTTP1.1\r\nHeader: 1\r\n\r\n")) + + resp := <-producer.Successes() + + data, _ := resp.Value.Encode() + + if string(data) != "1 2 3\nGET / HTTP1.1\r\nHeader: 1\r\n\r\n" { + t.Error("Message not properly encoded: ", string(data)) + } +} + +func TestOutputKafkaJSON(t *testing.T) { + config := sarama.NewConfig() + config.Producer.Return.Successes = true + producer := mocks.NewAsyncProducer(t, config) + producer.ExpectInputAndSucceed() + + output := NewKafkaOutput("", &KafkaConfig{ + producer: producer, + topic: "test", + useJSON: true, + }) + + output.Write([]byte("1 2 3\nGET / HTTP1.1\r\nHeader: 1\r\n\r\n")) + + resp := <-producer.Successes() + + data, _ := resp.Value.Encode() + + if string(data) != `{"Req_URL":"/","Req_Type":"1","Req_ID":"2","Req_Ts":"3","Req_Method":"GET","Req_Headers":{"Header":"1"}}` { + t.Error("Message not properly encoded: ", string(data)) + } +} diff --git a/settings.go b/settings.go index f012867..d57deb6 100644 --- a/settings.go +++ b/settings.go @@ -130,6 +130,7 @@ func init() { flag.StringVar(&Settings.outputKafkaConfig.host, "output-kafka-host", "", "Send request and response stats to Kafka:\n\tgor --input-raw :8080 --output-kafka-host '192.168.0.1:9092,192.168.0.2:9092'") flag.StringVar(&Settings.outputKafkaConfig.topic, "output-kafka-topic", "", "Send request and response stats to Kafka:\n\tgor --input-raw :8080 --output-kafka-topic 'kafka-log'") + flag.BoolVar(&Settings.outputKafkaConfig.useJSON, "output-kafka-json-format", false, "If turned on, it will serialize messages from GoReplay text format to JSON.") 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") From 5e90377a7fbb378c1c430d6713e53548c854868b Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Fri, 20 Jan 2017 21:01:19 +0300 Subject: [PATCH 19/19] Fmt changes --- input_kafka.go | 2 +- input_kafka_test.go | 82 ++++++++++++++++++++++----------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/input_kafka.go b/input_kafka.go index 609a704..715d5ee 100644 --- a/input_kafka.go +++ b/input_kafka.go @@ -2,9 +2,9 @@ package main import ( "encoding/json" - "log" "github.com/Shopify/sarama" "github.com/Shopify/sarama/mocks" + "log" ) // KafkaInput is used for recieving Kafka messages and diff --git a/input_kafka_test.go b/input_kafka_test.go index 3a0803d..b41ec8f 100644 --- a/input_kafka_test.go +++ b/input_kafka_test.go @@ -1,61 +1,61 @@ package main import ( - "github.com/Shopify/sarama" - "github.com/Shopify/sarama/mocks" - "testing" + "github.com/Shopify/sarama" + "github.com/Shopify/sarama/mocks" + "testing" ) func TestInputKafkaRAW(t *testing.T) { - consumer := mocks.NewConsumer(t, nil) - defer consumer.Close() + consumer := mocks.NewConsumer(t, nil) + defer consumer.Close() - consumer.ExpectConsumePartition("test", 0, mocks.AnyOffset).YieldMessage(&sarama.ConsumerMessage{Value: []byte("1 2 3\nGET / HTTP1.1\r\nHeader: 1\r\n\r\n")}) - consumer.SetTopicMetadata( - map[string][]int32{"test": []int32{0}}, - ) + consumer.ExpectConsumePartition("test", 0, mocks.AnyOffset).YieldMessage(&sarama.ConsumerMessage{Value: []byte("1 2 3\nGET / HTTP1.1\r\nHeader: 1\r\n\r\n")}) + consumer.SetTopicMetadata( + map[string][]int32{"test": []int32{0}}, + ) - input := NewKafkaInput("", &KafkaConfig{ - consumer: consumer, - topic: "test", - useJSON: false, - }) + input := NewKafkaInput("", &KafkaConfig{ + consumer: consumer, + topic: "test", + useJSON: false, + }) - buf := make([]byte, 1024) - n, err := input.Read(buf) + buf := make([]byte, 1024) + n, err := input.Read(buf) - if err != nil { - t.Fatal(err) - } + if err != nil { + t.Fatal(err) + } - if string(buf[:n]) != "1 2 3\nGET / HTTP1.1\r\nHeader: 1\r\n\r\n" { - t.Error("Message not properly decoded: ", string(buf[:n]), n) - } + if string(buf[:n]) != "1 2 3\nGET / HTTP1.1\r\nHeader: 1\r\n\r\n" { + t.Error("Message not properly decoded: ", string(buf[:n]), n) + } } func TestInputKafkaJSON(t *testing.T) { - consumer := mocks.NewConsumer(t, nil) - defer consumer.Close() + consumer := mocks.NewConsumer(t, nil) + defer consumer.Close() - consumer.ExpectConsumePartition("test", 0, mocks.AnyOffset).YieldMessage(&sarama.ConsumerMessage{Value: []byte(`{"Req_URL":"/","Req_Type":"1","Req_ID":"2","Req_Ts":"3","Req_Method":"GET","Req_Headers":{"Header":"1"}}`)}) - consumer.SetTopicMetadata( - map[string][]int32{"test": []int32{0}}, - ) + consumer.ExpectConsumePartition("test", 0, mocks.AnyOffset).YieldMessage(&sarama.ConsumerMessage{Value: []byte(`{"Req_URL":"/","Req_Type":"1","Req_ID":"2","Req_Ts":"3","Req_Method":"GET","Req_Headers":{"Header":"1"}}`)}) + consumer.SetTopicMetadata( + map[string][]int32{"test": []int32{0}}, + ) - input := NewKafkaInput("", &KafkaConfig{ - consumer: consumer, - topic: "test", - useJSON: true, - }) + input := NewKafkaInput("", &KafkaConfig{ + consumer: consumer, + topic: "test", + useJSON: true, + }) - buf := make([]byte, 1024) - n, err := input.Read(buf) + buf := make([]byte, 1024) + n, err := input.Read(buf) - if err != nil { - t.Fatal(err) - } + if err != nil { + t.Fatal(err) + } - if string(buf[:n]) != "1 2 3\nGET / HTTP/1.1\r\nHeader: 1\r\n\r\n" { - t.Error("Message not properly decoded: ", string(buf[:n]), n) - } + if string(buf[:n]) != "1 2 3\nGET / HTTP/1.1\r\nHeader: 1\r\n\r\n" { + t.Error("Message not properly decoded: ", string(buf[:n]), n) + } }