diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9081e51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +*~ +*.log + +artifacts +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f8f0b4a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,82 @@ +# GoLang Module Release Notes + +## Unreleased + + +## 1.6.1 2019-06-13 + +* Cleaned up internal code + +## 1.6.0 2019-05-30 + +* Updated list of inspectable XML content types +* Added `http.Flusher` interface when the underlying handler supports this interface +* Updated timeout to include time to connect to the agent +* Cleaned up docs/code/examples + +## 1.5.0 2019-01-31 + +* Switched Update / Post RPC call to async +* Internal release for agent reverse proxy + +## 1.4.3 2018-08-07 + +* Improved error and debug messages +* Exposed more functionality to allow easier extending + + +## 1.4.2 2018-06-15 +* Improved handling of the `Host` request header +* Improved debugging output + +## 1.4.1 2018-06-04 +* Improved error and debug messages + +## 1.4.0 2018-05-24 + +* Standardized release notes +* Added support for multipart/form-data post +* Extended architecture to allow more flexibility +* Updated response writer interface to allow for WebSocket use +* Removed default filters on CONNECT/OPTIONS methods - now inspected by default +* Standardized error page +* Updated to contact agent on init for faster module registration + +## 1.3.1 2017-09-25 + +* Removed unused dependency +* Removed internal testing example + +## 1.3.0 2017-09-19 + +* Improved internal testing +* Updated msgpack serialization + +## 1.2.3 2017-09-11 + +* Standardized defaults across modules and document +* Bad release + +## 1.2.2 2017-07-02 + +* Updated to use [signalsciences/tlstext](https://github.com/signalsciences/tlstext) + +## 1.2.1 2017-03-21 + +* Added ability to send XML post bodies to agent +* Improved content-type processing + +## 1.2.0 2017-03-06 + +* Improved performance +* Exposed internal datastructures and methods + to allow alternative module implementations and + performance tests + +## 1.1.0 2017-02-28 + +* Fixed TCP vs. UDS configuration + +## 0.1.0 2016-09-02 + +* Initial release diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7dfed40 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:1.10.6-alpine3.8 + +COPY goroot/ /go/ +# this is used to lint and build tarball +RUN gometalinter --install --debug + +# we will mount the current directory here +VOLUME [ "/go/src/github.com/signalsciences/sigsci-module-golang" ] +WORKDIR /go/src/github.com/signalsciences/sigsci-module-golang diff --git a/Dockerfile.git b/Dockerfile.git new file mode 100644 index 0000000..89cdcee --- /dev/null +++ b/Dockerfile.git @@ -0,0 +1,2 @@ +FROM golang:1.10.6-alpine3.8 +RUN apk --update add git diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d0cd441 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,24 @@ +# sigsci-module-golang + +The MIT License (MIT) + +Copyright (c) 2019 Signal Sciences Corp. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9142d9d --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ + + +build: ## build and lint locally + ./scripts/build.sh + +# clean up each time to make sure nothing is cached between runs +# +test: ## build and run integration test + ./scripts/test.sh + +init: ## install gometalinter and msgp locally + go get -u github.com/alecthomas/gometalinter + gometalinter --install --debug + go get -u github.com/tinylib/msgp/msgp + go get . + + +clean: ## cleanup + find . -name 'goroot' -type d | xargs rm -rf + rm -rf artifacts + find . -name '*.log' | xargs rm -f + go clean ./... + git gc + +# https://www.client9.com/self-documenting-makefiles/ +help: + @awk -F ':|##' '/^[^\t].+?:.*?##/ {\ + printf "\033[36m%-30s\033[0m %s\n", $$1, $$NF \ + }' $(MAKEFILE_LIST) +.DEFAULT_GOAL=help +.PHONY=help diff --git a/README.md b/README.md new file mode 100644 index 0000000..478e056 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# sigsci-module-golang + +The Signal Sciences module in golang allows for integrating your golang +application directly with the Signal Sciences agent at the source code +level. This golang module is written as a `http.Handler` wrapper. To +integrate your application with the module, you will need to wrap your +existing handler with the module handler. + +Example Code Snippet: +```go +// Existing http.Handler +mux := http.NewServeMux() +mux.HandleFunc("/", helloworld) + +// Wrap the existing http.Handler with the SigSci module handler +wrapped, err := sigsci.NewModule( + // Existing handler to wrap + mux, + + // Any additional module options: + //sigsci.Socket("unix", "/var/run/sigsci.sock"), + //sigsci.Timeout(100 * time.Millisecond), + //sigsci.AnomalySize(512 * 1024), + //sigsci.AnomalyDuration(1 * time.Second), + //sigsci.MaxContentLength(100000), +) +if err != nil { + log.Fatal(err) +} + +// Listen and Serve as usual using the wrapped sigsci handler +s := &http.Server{ + Handler: wrapped, + Addr: "localhost:8000", +} +log.Fatal(s.ListenAndServe()) +``` + +## Dependencies + +The golang module requires two prerequisite packages to be installed: +[MessagePack Code Generator](https://github.com/tinylib/msgp/) and the +Signal Sciences custom [tlstext](https://github.com/signalsciences/tlstext) +package. + +The easiest way to install these packages is by using the `go get` +command to download and install these packages directly from their +public GitHub repositories: + +```bash +go get -u -t github.com/tinylib/msgp/msgp +go get -u -t github.com/signalsciences/tlstext +``` + +## Examples + +The [examples](examples/) directory contains complete example code. + +To run the simple [helloworld](examples/helloworld/main.go) example: + +```bash +go run examples/helloworld/main.go +``` + +Or, if your agent is running with a non-default `rpc-address`, you can +pass the sigsci-agent address as an argument such as one of the following: + +```bash +# Another UNIX Domain socket +go run examples/helloworld/main.go /tmp/sigsci.sock +# A TCP address:port +go run examples/helloworld/main.go localhost:9999 +``` + +This will run a HTTP listener on `localhost:8000`, which will send any +traffic to this listener to a running sigsci-agent for inspection. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..fdd3be6 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.6.2 diff --git a/clientcodec.go b/clientcodec.go new file mode 100644 index 0000000..d8dd5ef --- /dev/null +++ b/clientcodec.go @@ -0,0 +1,170 @@ +package sigsci + +import ( + "fmt" + "io" + "net" + "net/rpc" + + "github.com/tinylib/msgp/msgp" +) + +// Adaptors for golang's RPC mechanism using MSGPACK +// +// * http://msgpack.org +// * https://golang.org/pkg/net/rpc/ +// + +// defines the MSGPACK RPC format +type msgpClientCodec struct { + dec *msgp.Reader + enc *msgp.Writer + c io.Closer +} + +// NewMsgpClientCodec creates a new rpc.ClientCodec from an existing connection +func NewMsgpClientCodec(conn io.ReadWriteCloser) rpc.ClientCodec { + return msgpClientCodec{ + dec: msgp.NewReader(conn), + enc: msgp.NewWriter(conn), + c: conn, + } +} + +func (c msgpClientCodec) Close() error { + return c.c.Close() +} + +func (c msgpClientCodec) WriteRequest(r *rpc.Request, x interface{}) error { + if err := c.enc.WriteArrayHeader(4); err != nil { + return fmt.Errorf("WriteRequest failed in writing array header: %s", err) + } + + if err := c.enc.WriteInt(0); err != nil { + return fmt.Errorf("WriteRequest failed in requiting rpc msg type 0: %s", err) + } + + if err := c.enc.WriteUint32(uint32(r.Seq)); err != nil { + return fmt.Errorf("WriteRequest failed in writing sequence id: %s", err) + } + + if err := c.enc.WriteString(r.ServiceMethod); err != nil { + return fmt.Errorf("WriteRequest failed in writing service method: %s", err) + } + + if err := c.enc.WriteArrayHeader(1); err != nil { + return fmt.Errorf("WriteRequest failed in writing arg array header: %s", err) + } + + if err := c.enc.WriteIntf(x); err != nil { + return fmt.Errorf("WriteRequest failed in writing %T: %s", x, err) + } + + if err := c.enc.Flush(); err != nil { + return fmt.Errorf("WriteRequest failed in flushing: %s", err) + } + + return nil +} + +func (c msgpClientCodec) ReadResponseHeader(r *rpc.Response) error { + sz, err := c.dec.ReadArrayHeader() + if err != nil || sz != 4 { + if cerr := knownError(err); cerr != nil { + return cerr + } + if err == nil && sz != 4 { + err = fmt.Errorf("invalid array size %d", sz) + } + return fmt.Errorf("ReadResponseHeader failed in initial array: %s", err) + } + + msgtype, err := c.dec.ReadUint() + if err != nil || msgtype != 1 { + if cerr := knownError(err); cerr != nil { + return cerr + } + if err == nil && msgtype != 1 { + err = fmt.Errorf("invalid message type %d", msgtype) + } + return fmt.Errorf("ReadResponseHeader failed in message type: %s", err) + } + + seqID, err := c.dec.ReadUint() + if err != nil { + if cerr := knownError(err); cerr != nil { + return cerr + } + return fmt.Errorf("ReadResponseHeader failed in error type: %s", err) + } + r.Seq = uint64(seqID) + + err = c.dec.ReadNil() + if err != nil { + if cerr := knownError(err); cerr != nil { + return cerr + } + // if there is an error maybe its not nil + // try to read string. if still an error + // then assume its bad response + rawerr, err := c.dec.ReadString() + if err != nil { + if cerr := knownError(err); cerr != nil { + return cerr + } + return fmt.Errorf("ReadResponseHeader failed in error message: %s", err) + } + return fmt.Errorf("remote error: %s", rawerr) + } + return nil +} + +func (c msgpClientCodec) ReadResponseBody(x interface{}) error { + if x == nil { + return nil + } + + // if its a decode-able object, then sort it out. + if obj, ok := x.(msgp.Decodable); ok { + if err := obj.DecodeMsg(c.dec); err != nil { + if cerr := knownError(err); cerr != nil { + return cerr + } + return fmt.Errorf("ReadResponseBody failed in obj decode: %s", err) + } + return nil + } + + // we use a plain "int" for response codes just hardwired this + // case. in future use an object to simplify this. + // + if xint, ok := x.(*int); ok { + val, err := c.dec.ReadInt() + if err != nil { + if cerr := knownError(err); cerr != nil { + return cerr + } + return fmt.Errorf("ReadResponseBody failed in int decode: %s", err) + } + *xint = val + return nil + } + + return fmt.Errorf("ReadResponseBody failed: unable to decode") +} + +// knownError checks the error against known errors, does any fixups and returns an error to indicate it is a known error that should not be handled further as well as the replaced error. A nil error is returned if the original error should be handled further. +func knownError(err error) error { + if err == nil { + return nil + } + if err == io.EOF { + return err + } + if nerr, ok := err.(net.Error); ok { + if nerr.Timeout() { + return nerr + } + } + return nil +} diff --git a/examples/helloworld/main.go b/examples/helloworld/main.go new file mode 100644 index 0000000..e947bb2 --- /dev/null +++ b/examples/helloworld/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "log" + "net/http" + "os" + "strings" + + // Import the module + sigsci "github.com/signalsciences/sigsci-module-golang" +) + +func main() { + // Process sigsci-agent rpc-address if passed + sigsciAgentNetwork := "unix" + sigsciAgentAddress := "/var/run/sigsci.sock" + if len(os.Args) > 1 { + sigsciAgentAddress = os.Args[1] + } + if !strings.Contains(sigsciAgentAddress, "/") { + sigsciAgentNetwork = "tcp" + } + log.Printf("Using sigsci-agent address (pass address as program argument to change): %s:%s", sigsciAgentNetwork, sigsciAgentAddress) + + // Existing handler, in this case a simple http.ServeMux, + // but could be any http.Handler in the application + mux := http.NewServeMux() + mux.HandleFunc("/", helloworld) + + // Wrap the existing http.Handler with the SigSci module handler + wrapped, err := sigsci.NewModule( + // Existing handler to wrap + mux, + + // Any additional module options: + sigsci.Socket(sigsciAgentNetwork, sigsciAgentAddress), + //sigsci.Timeout(100 * time.Millisecond), + //sigsci.AnomalySize(512 * 1024), + //sigsci.AnomalyDuration(1 * time.Second), + //sigsci.MaxContentLength(100000), + + // Turn on debug logging for this example (do not use in production) + sigsci.Debug(true), + ) + if err != nil { + log.Fatal(err) + } + + // Listen and Serve as usual using the wrapped sigsci handler + s := &http.Server{ + Handler: wrapped, + Addr: "localhost:8000", + } + log.Printf("Server URL: http://%s/", s.Addr) + log.Fatal(s.ListenAndServe()) +} + +// helloworld just displays a banner message for testing +func helloworld(w http.ResponseWriter, r *http.Request) { + status := http.StatusOK + w.WriteHeader(status) + w.Write([]byte(` + +Hello World +

Hello World!

+ +`)) +} diff --git a/examples/mtest/main.go b/examples/mtest/main.go new file mode 100644 index 0000000..a99429f --- /dev/null +++ b/examples/mtest/main.go @@ -0,0 +1,128 @@ +package main + +import ( + "bytes" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "net/http/httputil" + "net/url" + "os" + "strconv" + "strings" + "time" + + sigsci "github.com/signalsciences/sigsci-module-golang" +) + +var ( + debug = false +) + +func helloworld(w http.ResponseWriter, r *http.Request) { + if debug { + reqbytes, _ := httputil.DumpRequest(r, true) + reqstr := string(reqbytes) + if !strings.HasSuffix(reqstr, "\n") { + reqstr += "\n" + } + log.Printf("REQUEST %s:\n%s", r.Header.Get("Content-Type"), reqstr) + } + delay := 0 + body := []byte("OK") + code := 200 + + var err error + var qs url.Values + if r.URL != nil { + qs, err = url.ParseQuery(r.URL.RawQuery) + } + if qs == nil { + qs = make(url.Values) + } + + if num, err := strconv.Atoi(qs.Get("response_time")); err == nil { + delay = num + } + if num, err := strconv.Atoi(qs.Get("response_code")); err == nil { + code = num + } + if num, err := strconv.Atoi(qs.Get("size")); err == nil { + body = bytes.Repeat([]byte{'a'}, num) + } + if len(qs.Get("echo")) > 0 { + body, err = ioutil.ReadAll(r.Body) + if err != nil { + log.Printf("ioutil.ReadAll erred: %s", err) + } + r.Body = ioutil.NopCloser(bytes.NewReader(body)) + } + + if delay > 0 { + time.Sleep(time.Millisecond * time.Duration(delay)) + } + if code >= 300 && code < 400 { + w.Header().Set("Location", "/foo") + } + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("Content-Length", strconv.Itoa(len(body))) + + // Populate varX response headers from form values for mtest form processing + if varq := r.FormValue("varq"); len(varq) > 0 { + w.Header().Add("varq", varq) + } + if varb := r.PostFormValue("varb"); len(varb) > 0 { + w.Header().Add("varb", varb) + } + if place := r.PostFormValue("place"); len(place) > 0 { + w.Header().Add("place", place) + } + + if debug { + // Record the response so it can be logged + wrec := httptest.NewRecorder() + for k := range w.Header() { + v := w.Header().Get(k) + wrec.Header().Set(k, v) + } + wrec.WriteHeader(code) + wrec.Write(body) + resp := wrec.Result() + respbytes, _ := httputil.DumpResponse(resp, true) + respstr := string(respbytes) + if !strings.HasSuffix(respstr, "\n") { + respstr += "\n" + } + log.Printf("RESPONSE:\n%s", respstr) + } + w.WriteHeader(code) + w.Write(body) +} + +func main() { + if dbg := os.Getenv("DEBUG"); len(dbg) > 0 && dbg != "0" { + debug = true + } + + mux := http.NewServeMux() + + // "/" is handle everything + mux.HandleFunc("/response", helloworld) + + h, err := sigsci.NewModule(mux, + sigsci.Socket("tcp", "agent:9090"), + sigsci.Timeout(1500*time.Millisecond), + // Match agent defaults and better deal with mtest behavior test defaults + sigsci.MaxContentLength(300*1024), + sigsci.AnomalySize(512*1024), + ) + if err != nil { + log.Fatal(err) + } + s := &http.Server{ + Handler: h, + Addr: "0.0.0.0:8085", + } + log.Fatal(s.ListenAndServe()) +} diff --git a/inspector.go b/inspector.go new file mode 100644 index 0000000..bdcfa75 --- /dev/null +++ b/inspector.go @@ -0,0 +1,39 @@ +package sigsci + +import "net/http" + +// InspectorInitFunc is called to decide if the request should be inspected +// Return true if inspection should occur for the request or false if +// inspection should be bypassed. +type InspectorInitFunc func(*http.Request) bool + +// InspectorFiniFunc is called after any inspection on the request is completed +type InspectorFiniFunc func(*http.Request) + +// Inspector is an interface to implement how the +// module communicates with the inspection engine. +type Inspector interface { + // ModuleInit can be called when the module starts up. This allows the module + // data (e.g., `ModuleVersion`, `ServerVersion`, `ServerFlavor`, etc.) to be + // sent to the collector so that the agent shows up initialized without having + // to wait for data to be sent through the inspector. This should only be called + // once when the app/module starts. + ModuleInit(*RPCMsgIn, *RPCMsgOut) error + // PreRequest is called before the request is processed by the app. The results + // should be analyzed for any anomalies or blocking conditions. In addition, any + // `RequestID` returned in the response should be recorded for future use. + PreRequest(*RPCMsgIn, *RPCMsgOut) error + // PostRequest is called after the request has been processed by the app and the + // response data (e.g., status code, headers, etc.) is available. This should be + // called if there was NOT a `RequestID` in the response to a previous `PreRequest` + // call for the same transaction (if a `RequestID` was in the response, then it + // should be used in an `UpdateRequest` call instead). + PostRequest(*RPCMsgIn, *RPCMsgOut) error + // UpdateRequest is called after the request has been processed by the app and the + // response data (e.g., status code, headers, etc.) is available. This should be used + // instead of a `PostRequest` call when a prior `PreRequest` call for the same + // transaction included a `RequestID`. In this case, this call is updating the data + // collected in the `PreRequest` with the given response data (e.g., status code, + // headers, etc.). + UpdateRequest(*RPCMsgIn2, *RPCMsgOut) error +} diff --git a/make-jenkins.sh b/make-jenkins.sh new file mode 100755 index 0000000..7a970a9 --- /dev/null +++ b/make-jenkins.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +if [ -z "${BUILD_NUMBER}" ]; then + echo "Must be run in Jenkins with BUILD_NUMBER set" + exit 2 +fi + +set -ex + +# build / lint agent in a container +find . -name "goroot" -type d | xargs rm -rf +mkdir goroot +docker build -f Dockerfile.git -t golang-git:1.10.6-alpine3.8 . +docker run --user 1015:1015 -v ${PWD}/goroot:/go/ --rm golang-git:1.10.6-alpine3.8 /bin/sh -c 'go get github.com/signalsciences/tlstext && go get github.com/tinylib/msgp && go get github.com/alecthomas/gometalinter' +./scripts/build-docker.sh + +# run module tests +./scripts/test.sh + +BASE=$PWD +## setup our package properties by distro +PKG_NAME="sigsci-module-golang" +DST_BUCKET="s3://package-build-artifacts/${PKG_NAME}/${BUILD_NUMBER}" +VERSION=$(cat ./VERSION) + + +cd ${BASE} +aws s3 cp \ + --no-follow-symlinks \ + --cache-control="max-age=300" \ + ./artifacts/${PKG_NAME}.tar.gz ${DST_BUCKET}/${PKG_NAME}_${VERSION}.tar.gz + +aws s3 cp \ + --no-follow-symlinks \ + --cache-control="max-age=300" \ + --content-type="text/plain; charset=UTF-8" \ + VERSION ${DST_BUCKET}/VERSION + +aws s3 cp \ + --no-follow-symlinks \ + --cache-control="max-age=300" \ + --content-language="en-US" \ + --content-type="text/markdown; charset=UTF-8" \ + CHANGELOG.md ${DST_BUCKET}/CHANGELOG.md + diff --git a/module.go b/module.go new file mode 100644 index 0000000..201a965 --- /dev/null +++ b/module.go @@ -0,0 +1,518 @@ +package sigsci + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" + "time" + + "github.com/signalsciences/tlstext" +) + +const moduleVersion = "sigsci-module-golang " + version + +// Module is an http.Handler that wraps an existing handler with +// data collection and sends it to the Signal Sciences Agent for +// inspection. +type Module struct { + handler http.Handler + rpcNetwork string + rpcAddress string + debug bool + timeout time.Duration + anomalySize int64 + anomalyDuration time.Duration + maxContentLength int64 + moduleVersion string + serverVersion string + inspector Inspector + inspInit InspectorInitFunc + inspFini InspectorFiniFunc +} + +// ModuleConfigOption is a functional config option for configuring the module +// See: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis +type ModuleConfigOption func(*Module) error + +// NewModule wraps an existing http.Handler with one that extracts data and +// sends it to the Signal Sciences Agent for inspection. The module is configured +// via functional options. +func NewModule(h http.Handler, options ...ModuleConfigOption) (*Module, error) { + // The following are the defaults, overridden by passing in functional options + m := Module{ + handler: h, + rpcNetwork: "unix", + rpcAddress: "/var/run/sigsci.sock", + debug: false, + timeout: 100 * time.Millisecond, + anomalySize: 512 * 1024, + anomalyDuration: 1 * time.Second, + maxContentLength: 100000, + moduleVersion: moduleVersion, + serverVersion: runtime.Version(), + } + + // Override defaults from functional options + for _, opt := range options { + err := opt(&m) + if err != nil { + return nil, err + } + } + + // By default, use an RPC based inspector + if m.inspector == nil { + m.inspector = &RPCInspector{ + Network: m.rpcNetwork, + Address: m.rpcAddress, + Timeout: m.timeout, + Debug: m.debug, + } + } + + // Call ModuleInit to initialize the module data, so that the agent is + // registered on module creation + now := time.Now().UTC() + in := RPCMsgIn{ + ModuleVersion: m.moduleVersion, + ServerVersion: m.serverVersion, + ServerFlavor: "", + Timestamp: now.Unix(), + NowMillis: now.UnixNano() / 1e6, + } + out := RPCMsgOut{} + if err := m.inspector.ModuleInit(&in, &out); err != nil { + if m.debug { + log.Println("Error in moduleinit to inspector: ", err.Error()) + } + } + + return &m, nil +} + +// Version returns a SemVer version string +func Version() string { + return version +} + +// Debug turns on debug logging +func Debug(enable bool) ModuleConfigOption { + return func(m *Module) error { + m.debug = enable + return nil + } +} + +// Socket is a function argument to set where to send data to the +// Signal Sciences Agent. The network argument should be `unix` +// or `tcp` and the corresponding address should be either an absolute +// path or an `address:port`, respectively. +func Socket(network, address string) ModuleConfigOption { + return func(m *Module) error { + switch network { + case "unix": + if !filepath.IsAbs(address) { + return errors.New(`address must be an absolute path for network="unix"`) + } + case "tcp": + if _, _, err := net.SplitHostPort(address); err != nil { + return fmt.Errorf(`address must be in "address:port" form for network="tcp": %s`, err) + } + default: + return errors.New(`network must be "tcp" or "unix"`) + } + + m.rpcNetwork = network + m.rpcAddress = address + + return nil + } +} + +// AnomalySize is a function argument to indicate when to send data to +// the inspector if the response was abnormally large +func AnomalySize(size int64) ModuleConfigOption { + return func(m *Module) error { + m.anomalySize = size + return nil + } +} + +// AnomalyDuration is a function argument to indicate when to send data +// to the inspector if the response was abnormally slow +func AnomalyDuration(dur time.Duration) ModuleConfigOption { + return func(m *Module) error { + m.anomalyDuration = dur + return nil + } +} + +// MaxContentLength is a function argument to set the maximum post +// body length that will be processed +func MaxContentLength(size int64) ModuleConfigOption { + return func(m *Module) error { + m.maxContentLength = size + return nil + } +} + +// Timeout is a function argument that sets the maximum time to wait until +// receiving a reply from the inspector. Once this timeout is reached, the +// module will fail open. +func Timeout(t time.Duration) ModuleConfigOption { + return func(m *Module) error { + m.timeout = t + return nil + } +} + +// ModuleIdentifier is a function argument that sets the module name +// and version for custom setups. +// The version should be a sem-version (e.g., "1.2.3") +func ModuleIdentifier(name, version string) ModuleConfigOption { + return func(m *Module) error { + m.moduleVersion = name + " " + version + return nil + } +} + +// ServerIdentifier is a function argument that sets the server +// identifier for custom setups +func ServerIdentifier(id string) ModuleConfigOption { + return func(m *Module) error { + m.serverVersion = id + return nil + } +} + +// CustomInspector is a function argument that sets a custom inspector, +// an optional inspector initializer to decide if inspection should occur, and +// an optional inspector finalizer that can perform any post-inspection steps +func CustomInspector(insp Inspector, init InspectorInitFunc, fini InspectorFiniFunc) ModuleConfigOption { + return func(m *Module) error { + m.inspector = insp + m.inspInit = init + m.inspFini = fini + return nil + } +} + +// ServeHTTP satisfies the http.Handler interface +func (m *Module) ServeHTTP(w http.ResponseWriter, req *http.Request) { + start := time.Now().UTC() + finiwg := sync.WaitGroup{} + + // Use the inspector init/fini functions if available + if m.inspInit != nil && !m.inspInit(req) { + // No inspection is desired, so just defer to the underlying handler + m.handler.ServeHTTP(w, req) + return + } + if m.inspFini != nil { + defer func() { + // Delay the finalizer call until inspection (any pending Post + // or Update call) is complete + go func() { + finiwg.Wait() + m.inspFini(req) + }() + }() + } + + if m.debug { + log.Printf("DEBUG: calling 'RPC.PreRequest' for inspection: method=%s host=%s url=%s", req.Method, req.Host, req.URL) + } + inspin2, out, err := m.inspectorPreRequest(req) + if err != nil { + // Fail open + if m.debug { + log.Printf("ERROR: 'RPC.PreRequest' call failed (failing open): %s", err.Error()) + } + m.handler.ServeHTTP(w, req) + return + } + + rw := NewResponseWriter(w) + + wafresponse := out.WAFResponse + switch wafresponse { + case 406: + status := int(wafresponse) + http.Error(rw, fmt.Sprintf("%d %s\n", status, http.StatusText(status)), status) + case 200: + // continue with normal request + m.handler.ServeHTTP(rw, req) + default: + log.Printf("ERROR: Received invalid response code from inspector (failing open): %d", wafresponse) + // continue with normal request + m.handler.ServeHTTP(rw, req) + } + + duration := time.Since(start) + code := rw.StatusCode() + size := rw.BytesWritten() + + if len(inspin2.RequestID) > 0 { + // Do the UpdateRequest inspection in the background while the foreground hurries the response back to the end-user. + inspin2.ResponseCode = int32(code) + inspin2.ResponseSize = size + inspin2.ResponseMillis = int64(duration / time.Millisecond) + inspin2.HeadersOut = convertHeaders(rw.Header()) + if m.debug { + log.Printf("DEBUG: calling 'RPC.UpdateRequest' due to returned requestid=%s: method=%s host=%s url=%s code=%d size=%d duration=%s", inspin2.RequestID, req.Method, req.Host, req.URL, code, size, duration) + } + finiwg.Add(1) // Inspection finializer will wait for this goroutine + go func() { + defer finiwg.Done() + if err := m.inspectorUpdateRequest(inspin2); err != nil && m.debug { + log.Printf("ERROR: 'RPC.UpdateRequest' call failed: %s", err.Error()) + } + }() + } else if code >= 300 || size >= m.anomalySize || duration >= m.anomalyDuration { + // Do the PostRequest inspection in the background while the foreground hurries the response back to the end-user. + if m.debug { + log.Printf("DEBUG: calling 'RPC.PostRequest' due to anomaly: method=%s host=%s url=%s code=%d size=%d duration=%s", req.Method, req.Host, req.URL, code, size, duration) + } + inspin := NewRPCMsgIn(req, nil, code, size, duration, m.moduleVersion, m.serverVersion) + inspin.WAFResponse = wafresponse + inspin.HeadersOut = convertHeaders(rw.Header()) + + finiwg.Add(1) // Inspection finializer will wait for this goroutine + go func() { + defer finiwg.Done() + if err := m.inspectorPostRequest(inspin); err != nil && m.debug { + log.Printf("ERROR: 'RPC.PostRequest' call failed: %s", err.Error()) + } + }() + } +} + +// Inspector returns the configured inspector +func (m *Module) Inspector() Inspector { + return m.inspector +} + +// Version returns the module version string +func (m *Module) Version() string { + return m.moduleVersion +} + +// ServerVersion returns the server version string +func (m *Module) ServerVersion() string { + return m.serverVersion +} + +// inspectorPreRequest reads the body if required and makes a prerequest call to the inspector +func (m *Module) inspectorPreRequest(req *http.Request) (inspin2 RPCMsgIn2, out RPCMsgOut, err error) { + // Create message to the inspector from the input request + // see if we can read-in the post body + + var reqbody []byte + if shouldReadBody(req, m) { + // Read all of it and close + // if error, just keep going + // It's possible that it is an error event + // but not sure what it is. Likely + // the client disconnected. + reqbody, _ = ioutil.ReadAll(req.Body) + req.Body.Close() + + // make a new reader so the next handler + // can still read the post normally as if + // nothing happened + req.Body = ioutil.NopCloser(bytes.NewBuffer(reqbody)) + } + + inspin := NewRPCMsgIn(req, reqbody, -1, -1, -1, m.moduleVersion, m.serverVersion) + + if m.debug { + log.Printf("DEBUG: Making PreRequest call to inspector: %s %s", inspin.Method, inspin.URI) + } + + err = m.inspector.PreRequest(inspin, &out) + if err != nil { + if m.debug { + log.Printf("DEBUG: PreRequest call error (%s %s): %s", inspin.Method, inspin.URI, err) + } + return + } + + // set any request headers + if out.RequestID != "" { + req.Header.Add("X-Sigsci-Requestid", out.RequestID) + } + + wafresponse := out.WAFResponse + req.Header.Add("X-Sigsci-Agentresponse", strconv.Itoa(int(wafresponse))) + for _, kv := range out.RequestHeaders { + req.Header.Add(kv[0], kv[1]) + } + + inspin2 = RPCMsgIn2{ + RequestID: out.RequestID, + ResponseCode: -1, + ResponseMillis: -1, + ResponseSize: -1, + } + + if m.debug { + tags := req.Header.Get("X-Sigsci-Tags") + log.Printf("DEBUG: PreRequest call (%s %s): %d RequestID=%s Tags=%v", inspin.Method, inspin.URI, wafresponse, out.RequestID, tags) + } + + return +} + +// inspectorPostRequest makes a postrequest call to the inspector +func (m *Module) inspectorPostRequest(inspin *RPCMsgIn) error { + // Create message to agent from the input request + + if m.debug { + log.Printf("DEBUG: Making PostRequest call to inspector: %s %s", inspin.Method, inspin.URI) + } + + // NOTE: Currently the output argument is not used + err := m.inspector.PostRequest(inspin, &RPCMsgOut{}) + if err != nil { + if m.debug { + log.Printf("DEBUG: PostRequest call error (%s %s): %s", inspin.Method, inspin.URI, err) + } + } + + return err +} + +// inspectorUpdateRequest makes an updaterequest call to the inspector +func (m *Module) inspectorUpdateRequest(inspin RPCMsgIn2) error { + if m.debug { + log.Printf("DEBUG: Making UpdateRequest call to inspector: RequestID=%s", inspin.RequestID) + } + + // NOTE: Currently the output argument is not used + err := m.inspector.UpdateRequest(&inspin, &RPCMsgOut{}) + if err != nil { + if m.debug { + log.Printf("DEBUG: UpdateRequest call error (RequestID=%s): %s", inspin.RequestID, err) + } + } + + return err +} + +// NewRPCMsgIn creates a message from a go http.Request object +// End-users of the golang module never need to use this +// directly and it is only exposed for performance testing +func NewRPCMsgIn(r *http.Request, postbody []byte, code int, size int64, dur time.Duration, module, server string) *RPCMsgIn { + now := time.Now().UTC() + + // assemble a message to send to inspector + tlsProtocol := "" + tlsCipher := "" + scheme := "http" + if r.TLS != nil { + // convert golang/spec integers into something human readable + scheme = "https" + tlsProtocol = tlstext.Version(r.TLS.Version) + tlsCipher = tlstext.CipherSuite(r.TLS.CipherSuite) + } + + // golang removes Host header from req.Header map and + // promotes it to r.Host field. Add it back as the first header. + hin := convertHeaders(r.Header) + if len(r.Host) > 0 { + hin = append([][2]string{{"Host", r.Host}}, hin...) + } + + return &RPCMsgIn{ + ModuleVersion: module, + ServerVersion: server, + ServerName: r.Host, + Timestamp: now.Unix(), + NowMillis: now.UnixNano() / 1e6, + RemoteAddr: stripPort(r.RemoteAddr), + Method: r.Method, + Scheme: scheme, + URI: r.RequestURI, + Protocol: r.Proto, + TLSProtocol: tlsProtocol, + TLSCipher: tlsCipher, + ResponseCode: int32(code), + ResponseMillis: int64(dur / time.Millisecond), + ResponseSize: size, + PostBody: string(postbody), + HeadersIn: hin, + } +} + +// stripPort removes any port from an address (e.g., the client port from the RemoteAddr) +func stripPort(ipdots string) string { + host, _, err := net.SplitHostPort(ipdots) + if err != nil { + return ipdots + } + return host +} + +// shouldReadBody returns true if the body should be read +func shouldReadBody(req *http.Request, m *Module) bool { + // nothing to do + if req.Body == nil { + return false + } + + // skip reading if post is invalid or too long + if req.ContentLength <= 0 || req.ContentLength > m.maxContentLength { + return false + } + + // only read certain types of content + return inspectableContentType(req.Header.Get("Content-Type")) +} + +// inspectableContentType returns true for an inspectable content type +func inspectableContentType(s string) bool { + s = strings.ToLower(s) + switch { + + // Form + case strings.HasPrefix(s, "application/x-www-form-urlencoded"): + return true + case strings.HasPrefix(s, "multipart/form-data"): + return true + + // JSON + case strings.Contains(s, "json") || + strings.Contains(s, "javascript"): + return true + + // XML + case strings.HasPrefix(s, "text/xml") || + strings.HasPrefix(s, "application/xml") || + strings.Contains(s, "+xml"): + return true + } + + return false +} + +// converts a http.Header map to a [][2]string +func convertHeaders(h http.Header) [][2]string { + // get headers + out := make([][2]string, 0, len(h)+1) + + for key, values := range h { + for _, value := range values { + out = append(out, [2]string{key, value}) + } + } + return out +} diff --git a/module_test.go b/module_test.go new file mode 100644 index 0000000..99632ad --- /dev/null +++ b/module_test.go @@ -0,0 +1,395 @@ +package sigsci + +import ( + "bufio" + "bytes" + "crypto/tls" + "fmt" + "net/http" + "net/http/httptest" + "net/http/httputil" + "reflect" + "strconv" + "strings" + "testing" + "time" +) + +func TestNewRPCMsgFromRequest(t *testing.T) { + b := bytes.Buffer{} + b.WriteString("test") + r, err := http.NewRequest("GET", "http://localhost/", &b) + if err != nil { + t.Fatal(err) + } + r.RemoteAddr = "127.0.0.1" + r.Header.Add("If-None-Match", `W/"wyzzy"`) + r.RequestURI = "http://localhost/" + r.TLS = &tls.ConnectionState{} + + want := RPCMsgIn{ + ServerName: "localhost", + Method: "GET", + Scheme: "https", + URI: "http://localhost/", + Protocol: "HTTP/1.1", + RemoteAddr: "127.0.0.1", + HeadersIn: [][2]string{{"Host", "localhost"}, {"If-None-Match", `W/"wyzzy"`}}, + } + eq := func(got, want RPCMsgIn) (ne string, equal bool) { + switch { + case got.ServerName != want.ServerName: + return "ServerHostname", false + case got.Method != want.Method: + return "Method", false + case got.Scheme != want.Scheme: + return "Scheme", false + case got.URI != want.URI: + return "URI", false + case got.Protocol != want.Protocol: + return "Protocol", false + case got.RemoteAddr != want.RemoteAddr: + return "RemoteAddr", false + case !reflect.DeepEqual(got.HeadersIn, want.HeadersIn): + return "HeadersIn", false + default: + return "", true + } + } + + got := NewRPCMsgIn(r, nil, -1, -1, -1, "", "") + if ne, equal := eq(*got, want); !equal { + t.Errorf("NewRPCMsgIn: incorrect %q", ne) + } +} + +// helper functions + +func TestStripPort(t *testing.T) { + cases := []struct { + want string + content string + }{ + // Invalid, should not change + {"", ""}, + {"foo:bar:baz", "foo:bar:baz"}, + // Valid, should have port removed if exists + {"127.0.0.1", "127.0.0.1"}, + {"127.0.0.1", "127.0.0.1:8000"}, + } + for pos, tt := range cases { + got := stripPort(tt.content) + if got != tt.want { + t.Errorf("test %d: StripPort(%q) = %q, want %q", pos, tt.content, got, tt.want) + } + } +} + +func TestShouldReadBody(t *testing.T) { + m, err := NewModule( + http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + status := http.StatusOK + http.Error(w, fmt.Sprintf("%d %s\n", status, http.StatusText(status)), status) + }), + MaxContentLength(20), + ) + if err != nil { + t.Fatalf("Failed to create module: %s", err) + } + + cases := []struct { + want bool + genreq func() []byte + }{ + // No C-T + {false, func() []byte { + return genTestRequest("GET", "http://example.com/", "", "") + }}, + // Invalid C-T + {false, func() []byte { + return genTestRequest("GET", "http://example.com/", "bad/type", `{}`) + }}, + // Zero length + {false, func() []byte { + return genTestRequest("GET", "http://example.com/", "application/json", ``) + }}, + // Too long + {false, func() []byte { + return genTestRequest("GET", "http://example.com/", "application/json", `{"foo":"12345678901234567890"}`) + }}, + // Good to read + {true, func() []byte { + return genTestRequest("GET", "http://example.com/", "application/json", `{}`) + }}, + } + + for pos, tt := range cases { + req, err := requestParseRaw("127.0.0.1:59000", tt.genreq()) + if err != nil { + t.Fatalf("Failed to generate request: %s", err) + } + got := shouldReadBody(req, m) + if got != tt.want { + t.Errorf("test %d: expected %v got %v", pos, tt.want, got) + } + } +} + +func TestConvertHeaders(t *testing.T) { + cases := []struct { + want [][2]string // Only the order of like keys matters + content http.Header // Order of values matter + }{ + // Empty + { + [][2]string{}, + http.Header{}, + }, + // Single values + { + [][2]string{ + {http.CanonicalHeaderKey("a"), "val a"}, + {http.CanonicalHeaderKey("b"), "val b"}, + }, http.Header{ + http.CanonicalHeaderKey("a"): {"val a"}, + http.CanonicalHeaderKey("b"): {"val b"}, + }, + }, + // Multiple values + { + [][2]string{ + {http.CanonicalHeaderKey("a"), "val a"}, + {http.CanonicalHeaderKey("b"), "val b1"}, + {http.CanonicalHeaderKey("b"), "val b2"}, + }, http.Header{ + http.CanonicalHeaderKey("a"): {"val a"}, + http.CanonicalHeaderKey("b"): {"val b1", "val b2"}, + }, + }, + } + + for pos, tt := range cases { + got := convertHeaders(tt.content) + + // Convert result back to a http.Header for comparison + hmap := http.Header{} + for _, v := range got { + hmap.Add(v[0], v[1]) + } + if !reflect.DeepEqual(tt.content, hmap) { + t.Errorf("test %d: expected %#v, got %#v", pos, tt.content, hmap) + } + } +} + +func TestInspectableContentType(t *testing.T) { + cases := []struct { + want bool + content string + }{ + {true, "application/x-www-form-urlencoded"}, + {true, "application/x-www-form-urlencoded; charset=UTF-8"}, + {true, "multipart/form-data"}, + {true, "text/xml"}, + {true, "application/xml"}, + {true, "text/xml;charset=UTF-8"}, + {true, "application/xml; charset=iso-2022-kr"}, + {true, "application/rss+xml"}, + {true, "application/json"}, + {true, "application/x-javascript"}, + {true, "text/javascript"}, + {true, "text/x-javascript"}, + {true, "text/x-json"}, + {true, "application/javascript"}, + {false, "octet/stream"}, + {false, "junk/yard"}, + } + + for pos, tt := range cases { + got := inspectableContentType(tt.content) + if got != tt.want { + t.Errorf("test %d: expected %v got %v", pos, tt.want, got) + } + } +} + +func TestModule(t *testing.T) { + cases := []struct { + req []byte // Raw HTTP request + resp int32 // Inspection response (200 or 406) + tags string // Any tags in the PreRequest call + }{ + {genTestRequest("GET", "http://example.com/", "", ""), 200, ""}, + {genTestRequest("GET", "http://example.com/", "", ""), 406, "XSS"}, + {genTestRequest("GET", "http://example.com/", "", ""), 200, ""}, + {genTestRequest("OPTIONS", "http://example.com/", "", ""), 200, ""}, + {genTestRequest("OPTIONS", "http://example.com/", "", ""), 406, "XSS"}, + {genTestRequest("OPTIONS", "http://example.com/", "", ""), 200, ""}, + {genTestRequest("CONNECT", "http://example.com/", "", ""), 200, ""}, + {genTestRequest("CONNECT", "http://example.com/", "", ""), 406, "XSS"}, + {genTestRequest("CONNECT", "http://example.com/", "", ""), 200, ""}, + {genTestRequest("POST", "http://example.com/", "application/x-www-form-urlencoded", "a=1"), 200, ""}, + {genTestRequest("POST", "http://example.com/", "application/x-www-form-urlencoded", "a=1"), 406, "XSS"}, + {genTestRequest("POST", "http://example.com/", "application/x-www-form-urlencoded", "a=1"), 200, ""}, + {genTestRequest("PUT", "http://example.com/", "application/x-www-form-urlencoded", "a=1"), 200, ""}, + {genTestRequest("PUT", "http://example.com/", "application/x-www-form-urlencoded", "a=1"), 406, "XSS"}, + {genTestRequest("PUT", "http://example.com/", "application/x-www-form-urlencoded", "a=1"), 200, ""}, + {genTestRequest("POST", "http://example.com/", "text/xml;charset=UTF-8", `1`), 200, ""}, + {genTestRequest("POST", "http://example.com/", "text/xml;charset=UTF-8", `1`), 406, "XSS"}, + {genTestRequest("POST", "http://example.com/", "text/xml;charset=UTF-8", `1`), 200, ""}, + {genTestRequest("POST", "http://example.com/", "application/xml; charset=iso-2022-kr", `1`), 200, ""}, + {genTestRequest("POST", "http://example.com/", "application/xml; charset=iso-2022-kr", `1`), 406, "XSS"}, + {genTestRequest("POST", "http://example.com/", "application/xml; charset=iso-2022-kr", `1`), 200, ""}, + {genTestRequest("POST", "http://example.com/", "application/rss+xml", `1`), 200, ""}, + {genTestRequest("POST", "http://example.com/", "application/rss+xml", `1`), 406, "XSS"}, + {genTestRequest("POST", "http://example.com/", "application/rss+xml", `1`), 200, ""}, + } + + for pos, tt := range cases { + respstr := strconv.Itoa(int(tt.resp)) + m, err := NewModule( + http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + status := http.StatusOK + http.Error(w, fmt.Sprintf("%d %s\n", status, http.StatusText(status)), status) + }), + Timeout(500*time.Millisecond), + Debug(true), + CustomInspector(newTestInspector(tt.resp, tt.tags), nil, nil), + ) + if err != nil { + t.Fatalf("test %d: Failed to create module: %s", pos, err) + } + + req, err := requestParseRaw("127.0.0.1:12345", tt.req) + if err != nil { + t.Fatalf("test %d: Failed to parse request: %s\n%s", pos, err, tt.req) + } + + if dump, err := httputil.DumpRequest(req, true); err == nil { + t.Log("CLIENT REQUEST:\n" + string(dump)) + } + + if hv := req.Header.Get(`X-Sigsci-Agentresponse`); hv != "" { + t.Errorf("test %d: unexpected request header %s=%s", pos, `X-Sigsci-Agentresponse`, hv) + } + + w := httptest.NewRecorder() + m.ServeHTTP(w, req) + resp := w.Result() + + if dump, err := httputil.DumpRequest(req, true); err == nil { + t.Log("SERVER REQUEST:\n" + string(dump)) + } + if hv := req.Header.Get(`X-Sigsci-Agentresponse`); hv == "" || hv != respstr { + t.Errorf("test %d: unexpected request header %s=%s, expected %q", pos, `X-Sigsci-Agentresponse`, hv, respstr) + } + if len(tt.tags) > 0 { + if hv := req.Header.Get(`X-Sigsci-Requestid`); hv == "" { + t.Errorf("test %d: expected request header %s=%s", pos, `X-Sigsci-Requestid`, hv) + } + } + + if dump, err := httputil.DumpResponse(resp, true); err == nil { + t.Log("SERVER RESPONSE:\n" + string(dump)) + } + if resp.StatusCode != int(tt.resp) { + t.Errorf("test %d: unexpected status code=%d, expected=%d", pos, resp.StatusCode, tt.resp) + } + + } +} + +func genTestRequest(meth, uri, ctype, payload string) []byte { + var err error + var req *http.Request + + if len(payload) > 0 { + req, err = http.NewRequest(meth, uri, strings.NewReader(payload)) + if err != nil { + panic(err) + } + } else { + req, err = http.NewRequest(meth, uri, nil) + if err != nil { + panic(err) + } + } + + req.Header.Set(`User-Agent`, `SigSci Module Tester/0.1`) + if len(ctype) > 0 { + req.Header.Set(`Content-Type`, ctype) + } + + // This will add some extra headers typically added by the client + dump, err := httputil.DumpRequestOut(req, true) + if err != nil { + panic(err) + } + + return dump +} + +// requestParseRaw creates a request from the given raw HTTP data +func requestParseRaw(raddr string, raw []byte) (*http.Request, error) { + req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(raw))) + if err != nil { + return nil, err + } + + // Set fields typically set by the server + req.RemoteAddr = raddr + + return req, nil +} + +// testInspector is a custom inspector that calls the simulator +// harness within the golang module +type testInspector struct { + resp int32 // Either 200 or 406 + tags string // EX: "XSS" (csv) +} + +func newTestInspector(resp int32, tags string) *testInspector { + return &testInspector{ + resp: resp, + tags: tags, + } +} + +func (insp *testInspector) ModuleInit(in *RPCMsgIn, out *RPCMsgOut) error { + out.WAFResponse = 200 + out.RequestID = "" + out.RequestHeaders = nil + return nil +} + +func (insp *testInspector) PreRequest(in *RPCMsgIn, out *RPCMsgOut) error { + out.WAFResponse = insp.resp + if len(insp.tags) > 0 { + out.RequestID = "0123456789abcdef01234567" + out.RequestHeaders = [][2]string{ + {"X-SigSci-Tags", insp.tags}, + } + } else { + out.RequestID = "" + out.RequestHeaders = nil + } + + return nil +} + +func (insp *testInspector) PostRequest(in *RPCMsgIn, out *RPCMsgOut) error { + out.WAFResponse = insp.resp + out.RequestID = "" + out.RequestHeaders = nil + + return nil +} + +func (insp *testInspector) UpdateRequest(in *RPCMsgIn2, out *RPCMsgOut) error { + out.WAFResponse = insp.resp + out.RequestID = "" + out.RequestHeaders = nil + + return nil +} diff --git a/responsewriter.go b/responsewriter.go new file mode 100644 index 0000000..dd84a8c --- /dev/null +++ b/responsewriter.go @@ -0,0 +1,98 @@ +package sigsci + +import ( + "bufio" + "fmt" + "net" + "net/http" +) + +// ResponseWriter is a http.ResponseWriter allowing extraction of data needed for inspection +type ResponseWriter interface { + http.ResponseWriter + BaseResponseWriter() http.ResponseWriter + StatusCode() int + BytesWritten() int64 +} + +// ResponseWriterFlusher is a ResponseWriter with a http.Flusher interface +type ResponseWriterFlusher interface { + ResponseWriter + http.Flusher +} + +// NewResponseWriter returns a ResponseWriter or ResponseWriterFlusher depending on the base http.ResponseWriter. +func NewResponseWriter(base http.ResponseWriter) ResponseWriter { + // NOTE: according to net/http docs, if WriteHeader is not called explicitly, + // the first call to Write will trigger an implicit WriteHeader(http.StatusOK). + // this is why the default code is 200 and it only changes if WriteHeader is called. + w := &responseRecorder{ + base: base, + code: 200, + } + if _, ok := w.base.(http.Flusher); ok { + return &responseRecorderFlusher{w} + } + return w +} + +// responseRecorder wraps a base http.ResponseWriter allowing extraction of additional inspection data +type responseRecorder struct { + base http.ResponseWriter + code int + size int64 +} + +// BaseResponseWriter returns the base http.ResponseWriter allowing access if needed +func (w *responseRecorder) BaseResponseWriter() http.ResponseWriter { + return w.base +} + +// StatusCode returns the status code that was used +func (w *responseRecorder) StatusCode() int { + return w.code +} + +// BytesWritten returns the number of bytes written +func (w *responseRecorder) BytesWritten() int64 { + return w.size +} + +// Header returns the header object +func (w *responseRecorder) Header() http.Header { + return w.base.Header() +} + +// WriteHeader writes the header, recording the status code for inspection +func (w *responseRecorder) WriteHeader(status int) { + w.code = status + w.base.WriteHeader(status) +} + +// Write writes data, tracking the length written for inspection +func (w *responseRecorder) Write(b []byte) (int, error) { + w.size += int64(len(b)) + return w.base.Write(b) +} + +// Hijack hijacks the connection from the HTTP handler so that it can be used directly (websockets, etc.) +// NOTE: This will fail if the wrapped http.responseRecorder is not a http.Hijacker. +func (w *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if h, ok := w.base.(http.Hijacker); ok { + return h.Hijack() + } + // Required for WebSockets to work + return nil, nil, fmt.Errorf("response writer (%T) does not implement http.Hijacker", w.base) +} + +// responseRecorderFlusher wraps a base http.ResponseWriter/http.Flusher allowing extraction of additional inspection data +type responseRecorderFlusher struct { + *responseRecorder +} + +// Flush flushes data if the underlying http.ResponseWriter is capable of flushing +func (w *responseRecorderFlusher) Flush() { + if f, ok := w.responseRecorder.base.(http.Flusher); ok { + f.Flush() + } +} diff --git a/responsewriter_test.go b/responsewriter_test.go new file mode 100644 index 0000000..97ba218 --- /dev/null +++ b/responsewriter_test.go @@ -0,0 +1,114 @@ +package sigsci + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +// testResponseRecorder is a httptest.ResponseRecorder without the Flusher interface +type testResponseRecorder struct { + Recorder *httptest.ResponseRecorder +} + +func (w *testResponseRecorder) Header() http.Header { + return w.Recorder.Header() +} + +func (w *testResponseRecorder) WriteHeader(status int) { + w.Recorder.WriteHeader(status) +} + +func (w *testResponseRecorder) Write(b []byte) (int, error) { + return w.Recorder.Write(b) +} + +// testResponseRecorderFlusher is a httptest.ResponseRecorder with the Flusher interface +type testResponseRecorderFlusher struct { + Recorder *httptest.ResponseRecorder +} + +func (w *testResponseRecorderFlusher) Header() http.Header { + return w.Recorder.Header() +} + +func (w *testResponseRecorderFlusher) WriteHeader(status int) { + w.Recorder.WriteHeader(status) +} + +func (w *testResponseRecorderFlusher) Write(b []byte) (int, error) { + return w.Recorder.Write(b) +} + +func (w *testResponseRecorderFlusher) Flush() { + w.Recorder.Flush() +} + +func testResponseWriter(t *testing.T, w ResponseWriter, flusher bool) { + status := 200 + respbody := []byte("123456") + + req, err := http.NewRequest(http.MethodGet, "http://example.com/", nil) + if err != nil { + t.Fatalf("Failed to generate request: %s", err) + } + + // Grab the recorder from the base response writer + var recorder *httptest.ResponseRecorder + switch rec := w.BaseResponseWriter().(type) { + case *testResponseRecorder: + recorder = rec.Recorder + case *testResponseRecorderFlusher: + recorder = rec.Recorder + default: + panic(fmt.Sprintf("unhandled recorder type: %T", w)) + } + + // This handler writes header/body and then flushes if the writer implements a http.Flusher + handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(status) + w.Write(respbody) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + }) + handler.ServeHTTP(w, req) + + // Verify the response + resp := recorder.Result() + if resp.StatusCode != status { + t.Errorf("Unexpected status code=%d, expected=%d", resp.StatusCode, status) + } + if w.StatusCode() != status { + t.Errorf("Unexpected recorder status code=%d, expected=%d", w.StatusCode(), status) + } + + // Verify body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Failed to generate request: %s", err) + } + if string(body) != string(respbody) { + t.Errorf("Unexpected response body=%q, expected=%q", body, respbody) + } + if w.BytesWritten() != int64(len(respbody)) { + t.Errorf("Unexpected response size=%d, expected=%d", w.BytesWritten(), len(respbody)) + } + + // Verify expected flushed value + if recorder.Flushed != flusher { + t.Errorf("Unexpected flush=%v, expected %v w=%T recorder=%T", recorder.Flushed, flusher, w, recorder) + } +} + +// TestResponseWriter tests a non-flusher ResponseWriter +func TestResponseWriter(t *testing.T) { + testResponseWriter(t, NewResponseWriter(&testResponseRecorder{httptest.NewRecorder()}), false) +} + +// TestResponseWriterFlusher tests a flusher ResponseWriter +func TestResponseWriterFlusher(t *testing.T) { + testResponseWriter(t, NewResponseWriter(&testResponseRecorderFlusher{httptest.NewRecorder()}), true) +} diff --git a/rpc.go b/rpc.go new file mode 100644 index 0000000..0fd74db --- /dev/null +++ b/rpc.go @@ -0,0 +1,49 @@ +package sigsci + +//go:generate msgp -unexported -tests=false + +// +// This is for messages to and from the agent +// + +// RPCMsgIn is the primary message from the webserver module to the agent +type RPCMsgIn struct { + AccessKeyID string // AccessKeyID optional, what Site does this belong too (deprecated) + ModuleVersion string // The module build version + ServerVersion string // Main server identifier "apache 2.0.46..." + ServerFlavor string // Any other webserver configuration info (optional) + ServerName string // As in request website URL + Timestamp int64 // Start of request in the number of seconds elapsed since January 1, 1970 UTC. + NowMillis int64 // Current time, the number of milliseconds elapsed since January 1, 1970 UTC. + RemoteAddr string // Remote IP Address, from request socket + Method string // GET/POST, etc... + Scheme string // http/https + URI string // /path?query + Protocol string // HTTP protocol + TLSProtocol string // e.g. TLSv1.2 + TLSCipher string // e.g. ECDHE-RSA-AES128-GCM-SHA256 + WAFResponse int32 // Optional + ResponseCode int32 // HTTP Response Status Code, -1 if unknown + ResponseMillis int64 // HTTP Milliseconds - How many milliseconds did the full request take, -1 if unknown + ResponseSize int64 // HTTP Response size, -1 if unknown + HeadersIn [][2]string // HTTP Request headers (slice of name/value pairs); nil ok + HeadersOut [][2]string // HTTP Response headers (slice of name/value pairs); nil ok + PostBody string // HTTP Request body; empty string if none +} + +// RPCMsgOut is sent back to the webserver +type RPCMsgOut struct { + WAFResponse int32 + RequestID string `json:",omitempty"` // Set if the server expects an UpdateRequest with this ID (UUID) + RequestHeaders [][2]string `json:",omitempty"` // Any additional information in the form of additional request headers +} + +// RPCMsgIn2 is a follow-up message from the webserver to the Agent +// Note there is no formal response to this message +type RPCMsgIn2 struct { + RequestID string // The request id (UUID) + ResponseCode int32 // HTTP status code did the webserver send back + ResponseMillis int64 // How many milliseconds did the full request take + ResponseSize int64 // how many bytes did the webserver send back + HeadersOut [][2]string +} diff --git a/rpc_gen.go b/rpc_gen.go new file mode 100644 index 0000000..d2e95dd --- /dev/null +++ b/rpc_gen.go @@ -0,0 +1,1272 @@ +package sigsci + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "github.com/tinylib/msgp/msgp" +) + +// DecodeMsg implements msgp.Decodable +func (z *RPCMsgIn) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "AccessKeyID": + z.AccessKeyID, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "AccessKeyID") + return + } + case "ModuleVersion": + z.ModuleVersion, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "ModuleVersion") + return + } + case "ServerVersion": + z.ServerVersion, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "ServerVersion") + return + } + case "ServerFlavor": + z.ServerFlavor, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "ServerFlavor") + return + } + case "ServerName": + z.ServerName, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "ServerName") + return + } + case "Timestamp": + z.Timestamp, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "Timestamp") + return + } + case "NowMillis": + z.NowMillis, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "NowMillis") + return + } + case "RemoteAddr": + z.RemoteAddr, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "RemoteAddr") + return + } + case "Method": + z.Method, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Method") + return + } + case "Scheme": + z.Scheme, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Scheme") + return + } + case "URI": + z.URI, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "URI") + return + } + case "Protocol": + z.Protocol, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Protocol") + return + } + case "TLSProtocol": + z.TLSProtocol, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "TLSProtocol") + return + } + case "TLSCipher": + z.TLSCipher, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "TLSCipher") + return + } + case "WAFResponse": + z.WAFResponse, err = dc.ReadInt32() + if err != nil { + err = msgp.WrapError(err, "WAFResponse") + return + } + case "ResponseCode": + z.ResponseCode, err = dc.ReadInt32() + if err != nil { + err = msgp.WrapError(err, "ResponseCode") + return + } + case "ResponseMillis": + z.ResponseMillis, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "ResponseMillis") + return + } + case "ResponseSize": + z.ResponseSize, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "ResponseSize") + return + } + case "HeadersIn": + var zb0002 uint32 + zb0002, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err, "HeadersIn") + return + } + if cap(z.HeadersIn) >= int(zb0002) { + z.HeadersIn = (z.HeadersIn)[:zb0002] + } else { + z.HeadersIn = make([][2]string, zb0002) + } + for za0001 := range z.HeadersIn { + var zb0003 uint32 + zb0003, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err, "HeadersIn", za0001) + return + } + if zb0003 != uint32(2) { + err = msgp.ArrayError{Wanted: uint32(2), Got: zb0003} + return + } + for za0002 := range z.HeadersIn[za0001] { + z.HeadersIn[za0001][za0002], err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "HeadersIn", za0001, za0002) + return + } + } + } + case "HeadersOut": + var zb0004 uint32 + zb0004, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err, "HeadersOut") + return + } + if cap(z.HeadersOut) >= int(zb0004) { + z.HeadersOut = (z.HeadersOut)[:zb0004] + } else { + z.HeadersOut = make([][2]string, zb0004) + } + for za0003 := range z.HeadersOut { + var zb0005 uint32 + zb0005, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err, "HeadersOut", za0003) + return + } + if zb0005 != uint32(2) { + err = msgp.ArrayError{Wanted: uint32(2), Got: zb0005} + return + } + for za0004 := range z.HeadersOut[za0003] { + z.HeadersOut[za0003][za0004], err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "HeadersOut", za0003, za0004) + return + } + } + } + case "PostBody": + z.PostBody, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "PostBody") + return + } + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z *RPCMsgIn) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 21 + // write "AccessKeyID" + err = en.Append(0xde, 0x0, 0x15, 0xab, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x44) + if err != nil { + return + } + err = en.WriteString(z.AccessKeyID) + if err != nil { + err = msgp.WrapError(err, "AccessKeyID") + return + } + // write "ModuleVersion" + err = en.Append(0xad, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e) + if err != nil { + return + } + err = en.WriteString(z.ModuleVersion) + if err != nil { + err = msgp.WrapError(err, "ModuleVersion") + return + } + // write "ServerVersion" + err = en.Append(0xad, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e) + if err != nil { + return + } + err = en.WriteString(z.ServerVersion) + if err != nil { + err = msgp.WrapError(err, "ServerVersion") + return + } + // write "ServerFlavor" + err = en.Append(0xac, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x6c, 0x61, 0x76, 0x6f, 0x72) + if err != nil { + return + } + err = en.WriteString(z.ServerFlavor) + if err != nil { + err = msgp.WrapError(err, "ServerFlavor") + return + } + // write "ServerName" + err = en.Append(0xaa, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65) + if err != nil { + return + } + err = en.WriteString(z.ServerName) + if err != nil { + err = msgp.WrapError(err, "ServerName") + return + } + // write "Timestamp" + err = en.Append(0xa9, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70) + if err != nil { + return + } + err = en.WriteInt64(z.Timestamp) + if err != nil { + err = msgp.WrapError(err, "Timestamp") + return + } + // write "NowMillis" + err = en.Append(0xa9, 0x4e, 0x6f, 0x77, 0x4d, 0x69, 0x6c, 0x6c, 0x69, 0x73) + if err != nil { + return + } + err = en.WriteInt64(z.NowMillis) + if err != nil { + err = msgp.WrapError(err, "NowMillis") + return + } + // write "RemoteAddr" + err = en.Append(0xaa, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72) + if err != nil { + return + } + err = en.WriteString(z.RemoteAddr) + if err != nil { + err = msgp.WrapError(err, "RemoteAddr") + return + } + // write "Method" + err = en.Append(0xa6, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64) + if err != nil { + return + } + err = en.WriteString(z.Method) + if err != nil { + err = msgp.WrapError(err, "Method") + return + } + // write "Scheme" + err = en.Append(0xa6, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65) + if err != nil { + return + } + err = en.WriteString(z.Scheme) + if err != nil { + err = msgp.WrapError(err, "Scheme") + return + } + // write "URI" + err = en.Append(0xa3, 0x55, 0x52, 0x49) + if err != nil { + return + } + err = en.WriteString(z.URI) + if err != nil { + err = msgp.WrapError(err, "URI") + return + } + // write "Protocol" + err = en.Append(0xa8, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c) + if err != nil { + return + } + err = en.WriteString(z.Protocol) + if err != nil { + err = msgp.WrapError(err, "Protocol") + return + } + // write "TLSProtocol" + err = en.Append(0xab, 0x54, 0x4c, 0x53, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c) + if err != nil { + return + } + err = en.WriteString(z.TLSProtocol) + if err != nil { + err = msgp.WrapError(err, "TLSProtocol") + return + } + // write "TLSCipher" + err = en.Append(0xa9, 0x54, 0x4c, 0x53, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72) + if err != nil { + return + } + err = en.WriteString(z.TLSCipher) + if err != nil { + err = msgp.WrapError(err, "TLSCipher") + return + } + // write "WAFResponse" + err = en.Append(0xab, 0x57, 0x41, 0x46, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65) + if err != nil { + return + } + err = en.WriteInt32(z.WAFResponse) + if err != nil { + err = msgp.WrapError(err, "WAFResponse") + return + } + // write "ResponseCode" + err = en.Append(0xac, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x64, 0x65) + if err != nil { + return + } + err = en.WriteInt32(z.ResponseCode) + if err != nil { + err = msgp.WrapError(err, "ResponseCode") + return + } + // write "ResponseMillis" + err = en.Append(0xae, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x69, 0x6c, 0x6c, 0x69, 0x73) + if err != nil { + return + } + err = en.WriteInt64(z.ResponseMillis) + if err != nil { + err = msgp.WrapError(err, "ResponseMillis") + return + } + // write "ResponseSize" + err = en.Append(0xac, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x69, 0x7a, 0x65) + if err != nil { + return + } + err = en.WriteInt64(z.ResponseSize) + if err != nil { + err = msgp.WrapError(err, "ResponseSize") + return + } + // write "HeadersIn" + err = en.Append(0xa9, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x49, 0x6e) + if err != nil { + return + } + err = en.WriteArrayHeader(uint32(len(z.HeadersIn))) + if err != nil { + err = msgp.WrapError(err, "HeadersIn") + return + } + for za0001 := range z.HeadersIn { + err = en.WriteArrayHeader(uint32(2)) + if err != nil { + err = msgp.WrapError(err, "HeadersIn", za0001) + return + } + for za0002 := range z.HeadersIn[za0001] { + err = en.WriteString(z.HeadersIn[za0001][za0002]) + if err != nil { + err = msgp.WrapError(err, "HeadersIn", za0001, za0002) + return + } + } + } + // write "HeadersOut" + err = en.Append(0xaa, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x4f, 0x75, 0x74) + if err != nil { + return + } + err = en.WriteArrayHeader(uint32(len(z.HeadersOut))) + if err != nil { + err = msgp.WrapError(err, "HeadersOut") + return + } + for za0003 := range z.HeadersOut { + err = en.WriteArrayHeader(uint32(2)) + if err != nil { + err = msgp.WrapError(err, "HeadersOut", za0003) + return + } + for za0004 := range z.HeadersOut[za0003] { + err = en.WriteString(z.HeadersOut[za0003][za0004]) + if err != nil { + err = msgp.WrapError(err, "HeadersOut", za0003, za0004) + return + } + } + } + // write "PostBody" + err = en.Append(0xa8, 0x50, 0x6f, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79) + if err != nil { + return + } + err = en.WriteString(z.PostBody) + if err != nil { + err = msgp.WrapError(err, "PostBody") + return + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z *RPCMsgIn) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // map header, size 21 + // string "AccessKeyID" + o = append(o, 0xde, 0x0, 0x15, 0xab, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x44) + o = msgp.AppendString(o, z.AccessKeyID) + // string "ModuleVersion" + o = append(o, 0xad, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e) + o = msgp.AppendString(o, z.ModuleVersion) + // string "ServerVersion" + o = append(o, 0xad, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e) + o = msgp.AppendString(o, z.ServerVersion) + // string "ServerFlavor" + o = append(o, 0xac, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x46, 0x6c, 0x61, 0x76, 0x6f, 0x72) + o = msgp.AppendString(o, z.ServerFlavor) + // string "ServerName" + o = append(o, 0xaa, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65) + o = msgp.AppendString(o, z.ServerName) + // string "Timestamp" + o = append(o, 0xa9, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70) + o = msgp.AppendInt64(o, z.Timestamp) + // string "NowMillis" + o = append(o, 0xa9, 0x4e, 0x6f, 0x77, 0x4d, 0x69, 0x6c, 0x6c, 0x69, 0x73) + o = msgp.AppendInt64(o, z.NowMillis) + // string "RemoteAddr" + o = append(o, 0xaa, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72) + o = msgp.AppendString(o, z.RemoteAddr) + // string "Method" + o = append(o, 0xa6, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64) + o = msgp.AppendString(o, z.Method) + // string "Scheme" + o = append(o, 0xa6, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65) + o = msgp.AppendString(o, z.Scheme) + // string "URI" + o = append(o, 0xa3, 0x55, 0x52, 0x49) + o = msgp.AppendString(o, z.URI) + // string "Protocol" + o = append(o, 0xa8, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c) + o = msgp.AppendString(o, z.Protocol) + // string "TLSProtocol" + o = append(o, 0xab, 0x54, 0x4c, 0x53, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c) + o = msgp.AppendString(o, z.TLSProtocol) + // string "TLSCipher" + o = append(o, 0xa9, 0x54, 0x4c, 0x53, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72) + o = msgp.AppendString(o, z.TLSCipher) + // string "WAFResponse" + o = append(o, 0xab, 0x57, 0x41, 0x46, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65) + o = msgp.AppendInt32(o, z.WAFResponse) + // string "ResponseCode" + o = append(o, 0xac, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x64, 0x65) + o = msgp.AppendInt32(o, z.ResponseCode) + // string "ResponseMillis" + o = append(o, 0xae, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x69, 0x6c, 0x6c, 0x69, 0x73) + o = msgp.AppendInt64(o, z.ResponseMillis) + // string "ResponseSize" + o = append(o, 0xac, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x69, 0x7a, 0x65) + o = msgp.AppendInt64(o, z.ResponseSize) + // string "HeadersIn" + o = append(o, 0xa9, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x49, 0x6e) + o = msgp.AppendArrayHeader(o, uint32(len(z.HeadersIn))) + for za0001 := range z.HeadersIn { + o = msgp.AppendArrayHeader(o, uint32(2)) + for za0002 := range z.HeadersIn[za0001] { + o = msgp.AppendString(o, z.HeadersIn[za0001][za0002]) + } + } + // string "HeadersOut" + o = append(o, 0xaa, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x4f, 0x75, 0x74) + o = msgp.AppendArrayHeader(o, uint32(len(z.HeadersOut))) + for za0003 := range z.HeadersOut { + o = msgp.AppendArrayHeader(o, uint32(2)) + for za0004 := range z.HeadersOut[za0003] { + o = msgp.AppendString(o, z.HeadersOut[za0003][za0004]) + } + } + // string "PostBody" + o = append(o, 0xa8, 0x50, 0x6f, 0x73, 0x74, 0x42, 0x6f, 0x64, 0x79) + o = msgp.AppendString(o, z.PostBody) + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *RPCMsgIn) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "AccessKeyID": + z.AccessKeyID, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "AccessKeyID") + return + } + case "ModuleVersion": + z.ModuleVersion, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ModuleVersion") + return + } + case "ServerVersion": + z.ServerVersion, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ServerVersion") + return + } + case "ServerFlavor": + z.ServerFlavor, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ServerFlavor") + return + } + case "ServerName": + z.ServerName, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ServerName") + return + } + case "Timestamp": + z.Timestamp, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Timestamp") + return + } + case "NowMillis": + z.NowMillis, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "NowMillis") + return + } + case "RemoteAddr": + z.RemoteAddr, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "RemoteAddr") + return + } + case "Method": + z.Method, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Method") + return + } + case "Scheme": + z.Scheme, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Scheme") + return + } + case "URI": + z.URI, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "URI") + return + } + case "Protocol": + z.Protocol, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Protocol") + return + } + case "TLSProtocol": + z.TLSProtocol, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "TLSProtocol") + return + } + case "TLSCipher": + z.TLSCipher, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "TLSCipher") + return + } + case "WAFResponse": + z.WAFResponse, bts, err = msgp.ReadInt32Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "WAFResponse") + return + } + case "ResponseCode": + z.ResponseCode, bts, err = msgp.ReadInt32Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "ResponseCode") + return + } + case "ResponseMillis": + z.ResponseMillis, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "ResponseMillis") + return + } + case "ResponseSize": + z.ResponseSize, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "ResponseSize") + return + } + case "HeadersIn": + var zb0002 uint32 + zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "HeadersIn") + return + } + if cap(z.HeadersIn) >= int(zb0002) { + z.HeadersIn = (z.HeadersIn)[:zb0002] + } else { + z.HeadersIn = make([][2]string, zb0002) + } + for za0001 := range z.HeadersIn { + var zb0003 uint32 + zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "HeadersIn", za0001) + return + } + if zb0003 != uint32(2) { + err = msgp.ArrayError{Wanted: uint32(2), Got: zb0003} + return + } + for za0002 := range z.HeadersIn[za0001] { + z.HeadersIn[za0001][za0002], bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "HeadersIn", za0001, za0002) + return + } + } + } + case "HeadersOut": + var zb0004 uint32 + zb0004, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "HeadersOut") + return + } + if cap(z.HeadersOut) >= int(zb0004) { + z.HeadersOut = (z.HeadersOut)[:zb0004] + } else { + z.HeadersOut = make([][2]string, zb0004) + } + for za0003 := range z.HeadersOut { + var zb0005 uint32 + zb0005, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "HeadersOut", za0003) + return + } + if zb0005 != uint32(2) { + err = msgp.ArrayError{Wanted: uint32(2), Got: zb0005} + return + } + for za0004 := range z.HeadersOut[za0003] { + z.HeadersOut[za0003][za0004], bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "HeadersOut", za0003, za0004) + return + } + } + } + case "PostBody": + z.PostBody, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "PostBody") + return + } + default: + bts, err = msgp.Skip(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *RPCMsgIn) Msgsize() (s int) { + s = 3 + 12 + msgp.StringPrefixSize + len(z.AccessKeyID) + 14 + msgp.StringPrefixSize + len(z.ModuleVersion) + 14 + msgp.StringPrefixSize + len(z.ServerVersion) + 13 + msgp.StringPrefixSize + len(z.ServerFlavor) + 11 + msgp.StringPrefixSize + len(z.ServerName) + 10 + msgp.Int64Size + 10 + msgp.Int64Size + 11 + msgp.StringPrefixSize + len(z.RemoteAddr) + 7 + msgp.StringPrefixSize + len(z.Method) + 7 + msgp.StringPrefixSize + len(z.Scheme) + 4 + msgp.StringPrefixSize + len(z.URI) + 9 + msgp.StringPrefixSize + len(z.Protocol) + 12 + msgp.StringPrefixSize + len(z.TLSProtocol) + 10 + msgp.StringPrefixSize + len(z.TLSCipher) + 12 + msgp.Int32Size + 13 + msgp.Int32Size + 15 + msgp.Int64Size + 13 + msgp.Int64Size + 10 + msgp.ArrayHeaderSize + for za0001 := range z.HeadersIn { + s += msgp.ArrayHeaderSize + for za0002 := range z.HeadersIn[za0001] { + s += msgp.StringPrefixSize + len(z.HeadersIn[za0001][za0002]) + } + } + s += 11 + msgp.ArrayHeaderSize + for za0003 := range z.HeadersOut { + s += msgp.ArrayHeaderSize + for za0004 := range z.HeadersOut[za0003] { + s += msgp.StringPrefixSize + len(z.HeadersOut[za0003][za0004]) + } + } + s += 9 + msgp.StringPrefixSize + len(z.PostBody) + return +} + +// DecodeMsg implements msgp.Decodable +func (z *RPCMsgIn2) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "RequestID": + z.RequestID, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "RequestID") + return + } + case "ResponseCode": + z.ResponseCode, err = dc.ReadInt32() + if err != nil { + err = msgp.WrapError(err, "ResponseCode") + return + } + case "ResponseMillis": + z.ResponseMillis, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "ResponseMillis") + return + } + case "ResponseSize": + z.ResponseSize, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "ResponseSize") + return + } + case "HeadersOut": + var zb0002 uint32 + zb0002, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err, "HeadersOut") + return + } + if cap(z.HeadersOut) >= int(zb0002) { + z.HeadersOut = (z.HeadersOut)[:zb0002] + } else { + z.HeadersOut = make([][2]string, zb0002) + } + for za0001 := range z.HeadersOut { + var zb0003 uint32 + zb0003, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err, "HeadersOut", za0001) + return + } + if zb0003 != uint32(2) { + err = msgp.ArrayError{Wanted: uint32(2), Got: zb0003} + return + } + for za0002 := range z.HeadersOut[za0001] { + z.HeadersOut[za0001][za0002], err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "HeadersOut", za0001, za0002) + return + } + } + } + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z *RPCMsgIn2) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 5 + // write "RequestID" + err = en.Append(0x85, 0xa9, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x44) + if err != nil { + return + } + err = en.WriteString(z.RequestID) + if err != nil { + err = msgp.WrapError(err, "RequestID") + return + } + // write "ResponseCode" + err = en.Append(0xac, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x64, 0x65) + if err != nil { + return + } + err = en.WriteInt32(z.ResponseCode) + if err != nil { + err = msgp.WrapError(err, "ResponseCode") + return + } + // write "ResponseMillis" + err = en.Append(0xae, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x69, 0x6c, 0x6c, 0x69, 0x73) + if err != nil { + return + } + err = en.WriteInt64(z.ResponseMillis) + if err != nil { + err = msgp.WrapError(err, "ResponseMillis") + return + } + // write "ResponseSize" + err = en.Append(0xac, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x69, 0x7a, 0x65) + if err != nil { + return + } + err = en.WriteInt64(z.ResponseSize) + if err != nil { + err = msgp.WrapError(err, "ResponseSize") + return + } + // write "HeadersOut" + err = en.Append(0xaa, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x4f, 0x75, 0x74) + if err != nil { + return + } + err = en.WriteArrayHeader(uint32(len(z.HeadersOut))) + if err != nil { + err = msgp.WrapError(err, "HeadersOut") + return + } + for za0001 := range z.HeadersOut { + err = en.WriteArrayHeader(uint32(2)) + if err != nil { + err = msgp.WrapError(err, "HeadersOut", za0001) + return + } + for za0002 := range z.HeadersOut[za0001] { + err = en.WriteString(z.HeadersOut[za0001][za0002]) + if err != nil { + err = msgp.WrapError(err, "HeadersOut", za0001, za0002) + return + } + } + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z *RPCMsgIn2) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // map header, size 5 + // string "RequestID" + o = append(o, 0x85, 0xa9, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x44) + o = msgp.AppendString(o, z.RequestID) + // string "ResponseCode" + o = append(o, 0xac, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x64, 0x65) + o = msgp.AppendInt32(o, z.ResponseCode) + // string "ResponseMillis" + o = append(o, 0xae, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x69, 0x6c, 0x6c, 0x69, 0x73) + o = msgp.AppendInt64(o, z.ResponseMillis) + // string "ResponseSize" + o = append(o, 0xac, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x69, 0x7a, 0x65) + o = msgp.AppendInt64(o, z.ResponseSize) + // string "HeadersOut" + o = append(o, 0xaa, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x4f, 0x75, 0x74) + o = msgp.AppendArrayHeader(o, uint32(len(z.HeadersOut))) + for za0001 := range z.HeadersOut { + o = msgp.AppendArrayHeader(o, uint32(2)) + for za0002 := range z.HeadersOut[za0001] { + o = msgp.AppendString(o, z.HeadersOut[za0001][za0002]) + } + } + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *RPCMsgIn2) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "RequestID": + z.RequestID, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "RequestID") + return + } + case "ResponseCode": + z.ResponseCode, bts, err = msgp.ReadInt32Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "ResponseCode") + return + } + case "ResponseMillis": + z.ResponseMillis, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "ResponseMillis") + return + } + case "ResponseSize": + z.ResponseSize, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "ResponseSize") + return + } + case "HeadersOut": + var zb0002 uint32 + zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "HeadersOut") + return + } + if cap(z.HeadersOut) >= int(zb0002) { + z.HeadersOut = (z.HeadersOut)[:zb0002] + } else { + z.HeadersOut = make([][2]string, zb0002) + } + for za0001 := range z.HeadersOut { + var zb0003 uint32 + zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "HeadersOut", za0001) + return + } + if zb0003 != uint32(2) { + err = msgp.ArrayError{Wanted: uint32(2), Got: zb0003} + return + } + for za0002 := range z.HeadersOut[za0001] { + z.HeadersOut[za0001][za0002], bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "HeadersOut", za0001, za0002) + return + } + } + } + default: + bts, err = msgp.Skip(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *RPCMsgIn2) Msgsize() (s int) { + s = 1 + 10 + msgp.StringPrefixSize + len(z.RequestID) + 13 + msgp.Int32Size + 15 + msgp.Int64Size + 13 + msgp.Int64Size + 11 + msgp.ArrayHeaderSize + for za0001 := range z.HeadersOut { + s += msgp.ArrayHeaderSize + for za0002 := range z.HeadersOut[za0001] { + s += msgp.StringPrefixSize + len(z.HeadersOut[za0001][za0002]) + } + } + return +} + +// DecodeMsg implements msgp.Decodable +func (z *RPCMsgOut) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "WAFResponse": + z.WAFResponse, err = dc.ReadInt32() + if err != nil { + err = msgp.WrapError(err, "WAFResponse") + return + } + case "RequestID": + z.RequestID, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "RequestID") + return + } + case "RequestHeaders": + var zb0002 uint32 + zb0002, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err, "RequestHeaders") + return + } + if cap(z.RequestHeaders) >= int(zb0002) { + z.RequestHeaders = (z.RequestHeaders)[:zb0002] + } else { + z.RequestHeaders = make([][2]string, zb0002) + } + for za0001 := range z.RequestHeaders { + var zb0003 uint32 + zb0003, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err, "RequestHeaders", za0001) + return + } + if zb0003 != uint32(2) { + err = msgp.ArrayError{Wanted: uint32(2), Got: zb0003} + return + } + for za0002 := range z.RequestHeaders[za0001] { + z.RequestHeaders[za0001][za0002], err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "RequestHeaders", za0001, za0002) + return + } + } + } + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z *RPCMsgOut) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 3 + // write "WAFResponse" + err = en.Append(0x83, 0xab, 0x57, 0x41, 0x46, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65) + if err != nil { + return + } + err = en.WriteInt32(z.WAFResponse) + if err != nil { + err = msgp.WrapError(err, "WAFResponse") + return + } + // write "RequestID" + err = en.Append(0xa9, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x44) + if err != nil { + return + } + err = en.WriteString(z.RequestID) + if err != nil { + err = msgp.WrapError(err, "RequestID") + return + } + // write "RequestHeaders" + err = en.Append(0xae, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73) + if err != nil { + return + } + err = en.WriteArrayHeader(uint32(len(z.RequestHeaders))) + if err != nil { + err = msgp.WrapError(err, "RequestHeaders") + return + } + for za0001 := range z.RequestHeaders { + err = en.WriteArrayHeader(uint32(2)) + if err != nil { + err = msgp.WrapError(err, "RequestHeaders", za0001) + return + } + for za0002 := range z.RequestHeaders[za0001] { + err = en.WriteString(z.RequestHeaders[za0001][za0002]) + if err != nil { + err = msgp.WrapError(err, "RequestHeaders", za0001, za0002) + return + } + } + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z *RPCMsgOut) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // map header, size 3 + // string "WAFResponse" + o = append(o, 0x83, 0xab, 0x57, 0x41, 0x46, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65) + o = msgp.AppendInt32(o, z.WAFResponse) + // string "RequestID" + o = append(o, 0xa9, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x44) + o = msgp.AppendString(o, z.RequestID) + // string "RequestHeaders" + o = append(o, 0xae, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73) + o = msgp.AppendArrayHeader(o, uint32(len(z.RequestHeaders))) + for za0001 := range z.RequestHeaders { + o = msgp.AppendArrayHeader(o, uint32(2)) + for za0002 := range z.RequestHeaders[za0001] { + o = msgp.AppendString(o, z.RequestHeaders[za0001][za0002]) + } + } + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *RPCMsgOut) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "WAFResponse": + z.WAFResponse, bts, err = msgp.ReadInt32Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "WAFResponse") + return + } + case "RequestID": + z.RequestID, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "RequestID") + return + } + case "RequestHeaders": + var zb0002 uint32 + zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "RequestHeaders") + return + } + if cap(z.RequestHeaders) >= int(zb0002) { + z.RequestHeaders = (z.RequestHeaders)[:zb0002] + } else { + z.RequestHeaders = make([][2]string, zb0002) + } + for za0001 := range z.RequestHeaders { + var zb0003 uint32 + zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "RequestHeaders", za0001) + return + } + if zb0003 != uint32(2) { + err = msgp.ArrayError{Wanted: uint32(2), Got: zb0003} + return + } + for za0002 := range z.RequestHeaders[za0001] { + z.RequestHeaders[za0001][za0002], bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "RequestHeaders", za0001, za0002) + return + } + } + } + default: + bts, err = msgp.Skip(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *RPCMsgOut) Msgsize() (s int) { + s = 1 + 12 + msgp.Int32Size + 10 + msgp.StringPrefixSize + len(z.RequestID) + 15 + msgp.ArrayHeaderSize + for za0001 := range z.RequestHeaders { + s += msgp.ArrayHeaderSize + for za0002 := range z.RequestHeaders[za0001] { + s += msgp.StringPrefixSize + len(z.RequestHeaders[za0001][za0002]) + } + } + return +} diff --git a/rpcinspector.go b/rpcinspector.go new file mode 100644 index 0000000..d21b7e1 --- /dev/null +++ b/rpcinspector.go @@ -0,0 +1,125 @@ +package sigsci + +import ( + "fmt" + "net" + "net/rpc" + "time" +) + +// RPCInspector is an Inspector implemented as RPC calls to the agent +type RPCInspector struct { + Network string + Address string + Timeout time.Duration + Debug bool + InitRPCClientFunc func() (*rpc.Client, error) + FiniRPCClientFunc func(*rpc.Client, error) +} + +// ModuleInit sends a RPC.ModuleInit message to the agent +func (ri *RPCInspector) ModuleInit(in *RPCMsgIn, out *RPCMsgOut) error { + client, err := ri.GetRPCClient() + if err == nil { + err = client.Call("RPC.ModuleInit", in, out) + ri.CloseRPCClient(client, err) + } + + if err != nil { + return fmt.Errorf("RPC.ModuleInit call failed: %s", err) + } + + return nil +} + +// PreRequest sends a RPC.PreRequest message to the agent +func (ri *RPCInspector) PreRequest(in *RPCMsgIn, out *RPCMsgOut) error { + client, err := ri.GetRPCClient() + if err == nil { + err = client.Call("RPC.PreRequest", in, out) + ri.CloseRPCClient(client, err) + } + + if err != nil { + return fmt.Errorf("RPC.PreRequest call failed: %s", err) + } + + return nil +} + +// PostRequest sends a RPC.PostRequest message to the agent +func (ri *RPCInspector) PostRequest(in *RPCMsgIn, out *RPCMsgOut) error { + client, err := ri.GetRPCClient() + if err == nil { + var rpcout int + err = client.Call("RPC.PostRequest", in, &rpcout) + ri.CloseRPCClient(client, err) + + // Always success as the rpcout is not currently used + out.WAFResponse = 200 + out.RequestID = "" + out.RequestHeaders = nil + } + + if err != nil { + return fmt.Errorf("RPC.PostRequest call failed: %s", err) + } + + return nil +} + +// UpdateRequest sends a RPC.UpdateRequest message to the agent +func (ri *RPCInspector) UpdateRequest(in *RPCMsgIn2, out *RPCMsgOut) error { + client, err := ri.GetRPCClient() + if err == nil { + + var rpcout int + err = client.Call("RPC.UpdateRequest", in, &rpcout) + ri.CloseRPCClient(client, err) + + // Always success as the rpcout is not currently used + out.WAFResponse = 200 + out.RequestID = "" + out.RequestHeaders = nil + } + + return err +} + +// GetRPCClient gets a RPC client +func (ri *RPCInspector) GetRPCClient() (*rpc.Client, error) { + if ri.InitRPCClientFunc != nil { + return ri.InitRPCClientFunc() + } + + conn, err := ri.getConnection() + if err != nil { + return nil, err + } + rpcCodec := NewMsgpClientCodec(conn) + return rpc.NewClientWithCodec(rpcCodec), nil +} + +// CloseRPCClient closes a RPC client +func (ri *RPCInspector) CloseRPCClient(client *rpc.Client, err error) { + if ri.FiniRPCClientFunc != nil { + ri.FiniRPCClientFunc(client, err) + return + } + client.Close() +} + +func (ri *RPCInspector) makeConnection() (net.Conn, error) { + deadline := time.Now().Add(ri.Timeout) + conn, err := net.DialTimeout(ri.Network, ri.Address, ri.Timeout) + if err != nil { + return nil, err + } + conn.SetDeadline(deadline) + return conn, nil +} + +func (ri *RPCInspector) getConnection() (net.Conn, error) { + // here for future expansion to use pools, etc. + return ri.makeConnection() +} diff --git a/scripts/build-docker.sh b/scripts/build-docker.sh new file mode 100755 index 0000000..5fed5a3 --- /dev/null +++ b/scripts/build-docker.sh @@ -0,0 +1,6 @@ +#!/bin/sh -ex + +docker build -t foo . + +rm -rf goroot +docker run -v ${PWD}:/go/src/github.com/signalsciences/sigsci-module-golang --rm foo ./scripts/build.sh diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..d915cdc --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,46 @@ +#!/bin/sh +set -ex + +echo "package sigsci" > version.go +echo "" >> version.go +echo "const version = \"$(cat VERSION)\"" >> version.go +find . -name "goroot" -type d | xargs rm -rf +go generate ./... + +# make sure files made in docker are readable by all +chmod a+r *.go + +go build . +go test . + +# --enable=gosimple \ +# --enable=unused \ + +gometalinter \ + --vendor \ + --deadline=60s \ + --disable-all \ + --enable=vetshadow \ + --enable=ineffassign \ + --enable=deadcode \ + --enable=golint \ + --enable=gofmt \ + --enable=vet \ + --exclude=_gen.go \ + --exclude=/usr/local/go/src/net/lookup_unix.go \ + . + +rm -rf artifacts/ +mkdir -p artifacts/sigsci-module-golang +cp -rf \ + VERSION CHANGELOG.md LICENSE.md README.md \ + clientcodec.go rpc.go rpc_gen.go rpcinspector.go inspector.go responsewriter.go module.go version.go \ + responsewriter_test.go module_test.go \ + examples \ + artifacts/sigsci-module-golang/ + +# mtest is internal only +rm -fr artifacts/sigsci-module-golang/examples/mtest + +(cd artifacts; tar -czvf sigsci-module-golang.tar.gz sigsci-module-golang) +chmod a+rw artifacts diff --git a/scripts/test-golang110/Dockerfile b/scripts/test-golang110/Dockerfile new file mode 100644 index 0000000..b43d134 --- /dev/null +++ b/scripts/test-golang110/Dockerfile @@ -0,0 +1,7 @@ +FROM golang:1.10.6-alpine3.8 + +COPY goroot/ /go/ + +# we will mount the current directory here +VOLUME [ "/go/src/github.com/signalsciences/sigsci-module-golang" ] +WORKDIR /go/src/github.com/signalsciences/sigsci-module-golang diff --git a/scripts/test-golang110/docker-compose.override.yml b/scripts/test-golang110/docker-compose.override.yml new file mode 100644 index 0000000..7af37fd --- /dev/null +++ b/scripts/test-golang110/docker-compose.override.yml @@ -0,0 +1,15 @@ +version: "3" + +services: + # this defines our webserver uses our sigsci-module + # we only define it so it is attached to our fake network + # it will be run a few times with different options manually + # + # The volumes spec is a bit weird.. this script is run in scripts/test but + # needs stuff in ../../examples. Consider moving. + web: + volumes: + - ../..:/go/src/github.com/signalsciences/sigsci-module-golang + command: [ "go", "run", "/go/src/github.com/signalsciences/sigsci-module-golang/examples/mtest/main.go" ] + environment: + - DEBUG=0 diff --git a/scripts/test-golang110/docker-compose.yml b/scripts/test-golang110/docker-compose.yml new file mode 100644 index 0000000..ab36aef --- /dev/null +++ b/scripts/test-golang110/docker-compose.yml @@ -0,0 +1,57 @@ +version: "3" +networks: + mtest: + +services: + # this defines our webserver uses our sigsci-module + # we only define it so it is attached to our fake network + # it will be run a few times with different options manually + # + # + web: + build: + context: . + dockerfile: Dockerfile + expose: + - "8085" + networks: + - mtest + depends_on: + - agent + + # agent + agent: + image: local-dev/sigsci-agent:latest + command: [ "-debug-log-web-inputs", "2", "-rpc-address", "9090", "-debug-rpc-test-harness", "-debug-standalone", "3" ] + expose: + - "9090" + - "12345" + networks: + - mtest + + # punching bag + punchingbag: + image: local-dev/module-testing:latest + networks: + - mtest + expose: + - "8086" + command: [ "/bin/punchingbag", "-addr", ":8086" ] + + # mtest + # + mtest: + image: local-dev/module-testing:latest + networks: + - mtest + depends_on: + - web + - agent + - punchingbag + environment: + - DISABLE_HTTP_OPTIONS=1 + - DISABLE_NOCOOKIE=1 + - MTEST_BASEURL=web:8085 + - MTEST_AGENT=agent:12345 + command: [ "/bin/wait-for", "web:8085", "--", "/bin/mtest", "-test.v" ] + diff --git a/scripts/test-golang110/test.sh b/scripts/test-golang110/test.sh new file mode 100755 index 0000000..2fc3926 --- /dev/null +++ b/scripts/test-golang110/test.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e + +DOCKERCOMPOSE="docker-compose" + +# run at end no matter what +cleanup() { + echo "shutting down" + # capture log output + $DOCKERCOMPOSE logs --no-color agent >& agent.log + $DOCKERCOMPOSE logs --no-color web >& web.log + $DOCKERCOMPOSE logs --no-color mtest >& mtest.log + $DOCKERCOMPOSE logs --no-color punchingbag >& punchingbag.log + + # delete everything + $DOCKERCOMPOSE down + + # show output of module testing + cat mtest.log +} +trap cleanup 0 1 2 3 6 + +set -x + +# attempt to clean up any leftover junk +$DOCKERCOMPOSE down + +# always get latest agent +$DOCKERCOMPOSE pull + +# start everything, run tests +# +# --no-color --> safe for jenkins +# --build --> alway build test server/module container +# --abort-on-container-exit --> without this, the other servers keep the process running +# --exit-code-from mtest --> make exit code be the result of module test +# +# > /dev/null --> output of all servers is mixed together and ugly +# we get the individual logs at end +# +if [ -d "goroot" ]; then + rm -rf goroot +fi +docker run -v ${PWD}/goroot:/go/ --rm golang:1.10.6-alpine3.8 /bin/sh -c 'apk --update add git && go get github.com/signalsciences/tlstext && go get github.com/tinylib/msgp && go get github.com/alecthomas/gometalinter' +$DOCKERCOMPOSE up --no-color --build --abort-on-container-exit --exit-code-from mtest > /dev/null + diff --git a/scripts/test-golang111/Dockerfile b/scripts/test-golang111/Dockerfile new file mode 100644 index 0000000..cbd58ce --- /dev/null +++ b/scripts/test-golang111/Dockerfile @@ -0,0 +1,7 @@ +FROM golang:1.11.3-alpine3.8 + +COPY goroot/ /go/ + +# we will mount the current directory here +VOLUME [ "/go/src/github.com/signalsciences/sigsci-module-golang" ] +WORKDIR /go/src/github.com/signalsciences/sigsci-module-golang diff --git a/scripts/test-golang111/docker-compose.override.yml b/scripts/test-golang111/docker-compose.override.yml new file mode 100644 index 0000000..7af37fd --- /dev/null +++ b/scripts/test-golang111/docker-compose.override.yml @@ -0,0 +1,15 @@ +version: "3" + +services: + # this defines our webserver uses our sigsci-module + # we only define it so it is attached to our fake network + # it will be run a few times with different options manually + # + # The volumes spec is a bit weird.. this script is run in scripts/test but + # needs stuff in ../../examples. Consider moving. + web: + volumes: + - ../..:/go/src/github.com/signalsciences/sigsci-module-golang + command: [ "go", "run", "/go/src/github.com/signalsciences/sigsci-module-golang/examples/mtest/main.go" ] + environment: + - DEBUG=0 diff --git a/scripts/test-golang111/docker-compose.yml b/scripts/test-golang111/docker-compose.yml new file mode 100644 index 0000000..ab36aef --- /dev/null +++ b/scripts/test-golang111/docker-compose.yml @@ -0,0 +1,57 @@ +version: "3" +networks: + mtest: + +services: + # this defines our webserver uses our sigsci-module + # we only define it so it is attached to our fake network + # it will be run a few times with different options manually + # + # + web: + build: + context: . + dockerfile: Dockerfile + expose: + - "8085" + networks: + - mtest + depends_on: + - agent + + # agent + agent: + image: local-dev/sigsci-agent:latest + command: [ "-debug-log-web-inputs", "2", "-rpc-address", "9090", "-debug-rpc-test-harness", "-debug-standalone", "3" ] + expose: + - "9090" + - "12345" + networks: + - mtest + + # punching bag + punchingbag: + image: local-dev/module-testing:latest + networks: + - mtest + expose: + - "8086" + command: [ "/bin/punchingbag", "-addr", ":8086" ] + + # mtest + # + mtest: + image: local-dev/module-testing:latest + networks: + - mtest + depends_on: + - web + - agent + - punchingbag + environment: + - DISABLE_HTTP_OPTIONS=1 + - DISABLE_NOCOOKIE=1 + - MTEST_BASEURL=web:8085 + - MTEST_AGENT=agent:12345 + command: [ "/bin/wait-for", "web:8085", "--", "/bin/mtest", "-test.v" ] + diff --git a/scripts/test-golang111/test.sh b/scripts/test-golang111/test.sh new file mode 100755 index 0000000..8e8e969 --- /dev/null +++ b/scripts/test-golang111/test.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e + +DOCKERCOMPOSE="docker-compose" + +# run at end no matter what +cleanup() { + echo "shutting down" + # capture log output + $DOCKERCOMPOSE logs --no-color agent >& agent.log + $DOCKERCOMPOSE logs --no-color web >& web.log + $DOCKERCOMPOSE logs --no-color mtest >& mtest.log + $DOCKERCOMPOSE logs --no-color punchingbag >& punchingbag.log + + # delete everything + $DOCKERCOMPOSE down + + # show output of module testing + cat mtest.log +} +trap cleanup 0 1 2 3 6 + +set -x + +# attempt to clean up any leftover junk +$DOCKERCOMPOSE down + +# always get latest agent +$DOCKERCOMPOSE pull + +# start everything, run tests +# +# --no-color --> safe for jenkins +# --build --> alway build test server/module container +# --abort-on-container-exit --> without this, the other servers keep the process running +# --exit-code-from mtest --> make exit code be the result of module test +# +# > /dev/null --> output of all servers is mixed together and ugly +# we get the individual logs at end +# +if [ -d "goroot" ]; then + rm -rf goroot +fi +docker run -v ${PWD}/goroot:/go/ --rm golang:1.11.3-alpine3.8 /bin/sh -c 'apk --update add git && go get github.com/signalsciences/tlstext && go get github.com/tinylib/msgp && go get github.com/alecthomas/gometalinter' +$DOCKERCOMPOSE up --no-color --build --abort-on-container-exit --exit-code-from mtest > /dev/null + diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..aae4501 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +set -ex + +(cd ./scripts/test-golang110 && ./test.sh) +(cd ./scripts/test-golang111 && ./test.sh) diff --git a/version.go b/version.go new file mode 100644 index 0000000..5f3734a --- /dev/null +++ b/version.go @@ -0,0 +1,3 @@ +package sigsci + +const version = "1.6.2"