diff --git a/pkg/apigw/filter/postfilter.go b/pkg/apigw/filter/postfilter.go index fabd0958d..6993c23e7 100644 --- a/pkg/apigw/filter/postfilter.go +++ b/pkg/apigw/filter/postfilter.go @@ -7,11 +7,18 @@ import ( "net/http" "net/url" + "github.com/cortezaproject/corteza-server/automation/service" + atypes "github.com/cortezaproject/corteza-server/automation/types" + agctx "github.com/cortezaproject/corteza-server/pkg/apigw/ctx" "github.com/cortezaproject/corteza-server/pkg/apigw/types" - pe "github.com/cortezaproject/corteza-server/pkg/errors" + errors "github.com/cortezaproject/corteza-server/pkg/errors" + "github.com/cortezaproject/corteza-server/pkg/expr" ) type ( + typesRegistry interface { + Type(ref string) expr.Type + } redirection struct { types.FilterMeta @@ -33,6 +40,17 @@ type ( } } + jsonResponse struct { + types.FilterMeta + + reg typesRegistry + + params struct { + Exp *atypes.Expr + Evaluable expr.Evaluable + } + } + defaultJsonResponse struct { types.FilterMeta } @@ -80,6 +98,10 @@ func (h redirection) Weight() int { func (h *redirection) Merge(params []byte) (types.Handler, error) { err := json.NewDecoder(bytes.NewBuffer(params)).Decode(&h.params) + if err != nil { + return h, err + } + loc, err := url.ParseRequestURI(h.params.Location) if err != nil { @@ -135,7 +157,7 @@ func (j defaultJsonResponse) Handler() types.HandlerFunc { rw.WriteHeader(http.StatusAccepted) if _, err := rw.Write([]byte(`{}`)); err != nil { - return pe.Internal("could not write to body: %v", err) + return errors.Internal("could not write to body: %v", err) } return nil @@ -150,3 +172,106 @@ func checkStatus(typ string, status int) bool { return true } } + +func NewJsonResponse(reg typesRegistry) (e *jsonResponse) { + e = &jsonResponse{} + + e.Name = "jsonResponse" + e.Label = "JSON response" + e.Kind = types.PostFilter + + e.Args = []*types.FilterMetaArg{ + { + Type: "input", + Label: "input", + Options: map[string]interface{}{}, + }, + } + + e.reg = reg + + return +} + +func (j jsonResponse) New() types.Handler { + return NewJsonResponse(service.Registry()) +} + +func (j jsonResponse) String() string { + return fmt.Sprintf("apigw filter %s (%s)", j.Name, j.Label) +} + +func (j jsonResponse) Meta() types.FilterMeta { + return j.FilterMeta +} + +func (j *jsonResponse) Merge(params []byte) (h types.Handler, err error) { + var ( + parser = expr.NewParser() + ) + + err = json.NewDecoder(bytes.NewBuffer(params)).Decode(&j.params.Exp) + + if err != nil { + return j, err + } + + j.params.Evaluable, err = parser.Parse(j.params.Exp.Expr) + + if err != nil { + return j, fmt.Errorf("could not evaluate expression: %s", err) + } + + j.params.Exp.SetEval(j.params.Evaluable) + + return j, err +} + +func (j jsonResponse) Handler() types.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) (err error) { + var ( + ctx = r.Context() + scope = agctx.ScopeFromContext(ctx) + + evald interface{} + ) + + in, err := expr.NewVars(scope.Dict()) + + if err != nil { + return errors.Internal("could not validate request data: %v", err) + } + + // set type to the registered expression from + // any of the already registered types + j.params.Exp.SetType(func(name string) (e expr.Type, err error) { + if name == "" { + name = "Any" + } + + if typ := j.reg.Type(name); typ != nil { + return typ, nil + } else { + return nil, errors.Internal("unknown or unregistered type %s", name) + } + }) + + evald, err = j.params.Exp.Eval(ctx, in) + + if err != nil { + return + } + + rw.Header().Add("Content-Type", "application/json") + + switch v := evald.(type) { + case string: + rw.Write([]byte(v)) + default: + e := json.NewEncoder(rw) + err = e.Encode(v) + } + + return + } +} diff --git a/pkg/apigw/filter/processer.go b/pkg/apigw/filter/processer.go index 8cf8bc6de..64e98c338 100644 --- a/pkg/apigw/filter/processer.go +++ b/pkg/apigw/filter/processer.go @@ -91,16 +91,17 @@ func (h workflow) Handler() types.HandlerFunc { scope = agctx.ScopeFromContext(ctx) ) - payload, err := scope.Get("payload") + // original request with body as io.Reader + // read-only + ar, err := scope.Get("request") if err != nil { - return pe.Internal("could not get payload: %v", err) + return err } // setup scope for workflow vv := map[string]interface{}{ - "payload": payload, - "request": r, + "request": ar, } // get the request data and put it into vars @@ -152,8 +153,7 @@ func (h workflow) Handler() types.HandlerFunc { scope = ss - scope.Set("request", r) - scope.Set("payload", payload) + scope.Set("request", ar) return nil } diff --git a/pkg/apigw/registry/registry.go b/pkg/apigw/registry/registry.go index ada6634ef..9cdf1ac45 100644 --- a/pkg/apigw/registry/registry.go +++ b/pkg/apigw/registry/registry.go @@ -69,6 +69,7 @@ func (r *Registry) Preload() { // postfilters r.Add("redirection", filter.NewRedirection()) + r.Add("jsonResponse", filter.NewJsonResponse(service.Registry())) r.Add("defaultJsonResponse", filter.NewDefaultJsonResponse()) } diff --git a/pkg/apigw/route.go b/pkg/apigw/route.go index 76d82f4e5..7fcb50300 100644 --- a/pkg/apigw/route.go +++ b/pkg/apigw/route.go @@ -1,18 +1,15 @@ package apigw import ( - "bytes" "fmt" - "io" - "io/ioutil" "net/http" - "net/http/httputil" "time" actx "github.com/cortezaproject/corteza-server/pkg/apigw/ctx" "github.com/cortezaproject/corteza-server/pkg/apigw/types" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/options" + "github.com/cortezaproject/corteza-server/system/automation" "go.uber.org/zap" ) @@ -45,19 +42,16 @@ func (r route) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.log.Debug("started serving route") - b, _ := io.ReadAll(req.Body) - body := string(b) + // create a new automation HttpRequest + ar, err := automation.NewHttpRequest(req) - // write again - req.Body = ioutil.NopCloser(bytes.NewBuffer(b)) + if err != nil { + r.log.Error("could not prepare a request holder", zap.Error(err)) + return + } scope.Set("opts", r.opts) - scope.Set("payload", body) - - if r.opts.LogEnabled { - o, _ := httputil.DumpRequest(req, r.opts.LogRequestBody) - r.log.Debug("incoming request", zap.Any("request", string(o))) - } + scope.Set("request", ar) req = req.WithContext(actx.ScopeToContext(ctx, &scope)) diff --git a/pkg/apigw/types/scope.go b/pkg/apigw/types/scope.go index e89c4f852..0b8395532 100644 --- a/pkg/apigw/types/scope.go +++ b/pkg/apigw/types/scope.go @@ -65,6 +65,10 @@ func (s Scp) Get(k string) (v interface{}, err error) { return } +func (s *Scp) Dict() map[string]interface{} { + return *s +} + func (s *Scp) Filter(fn func(k string, v interface{}) bool) *Scp { ss := Scp{} @@ -78,3 +82,8 @@ func (s *Scp) Filter(fn func(k string, v interface{}) bool) *Scp { return &ss } + +func (s Scp) Has(k string) (has bool) { + _, has = s[k] + return +} diff --git a/system/automation/expr_types.gen.go b/system/automation/expr_types.gen.go index 59bf496fe..b914cf0dd 100644 --- a/system/automation/expr_types.gen.go +++ b/system/automation/expr_types.gen.go @@ -119,6 +119,203 @@ func (t *DocumentType) Assign(val interface{}) error { } } +// HttpRequest is an expression type, wrapper for *types.HttpRequest type +type HttpRequest struct { + value *types.HttpRequest + mux sync.RWMutex +} + +// NewHttpRequest creates new instance of HttpRequest expression type +func NewHttpRequest(val interface{}) (*HttpRequest, error) { + if c, err := CastToHttpRequest(val); err != nil { + return nil, fmt.Errorf("unable to create HttpRequest: %w", err) + } else { + return &HttpRequest{value: c}, nil + } +} + +// Get return underlying value on HttpRequest +func (t *HttpRequest) Get() interface{} { + t.mux.RLock() + defer t.mux.RUnlock() + return t.value +} + +// GetValue returns underlying value on HttpRequest +func (t *HttpRequest) GetValue() *types.HttpRequest { + t.mux.RLock() + defer t.mux.RUnlock() + return t.value +} + +// Type return type name +func (HttpRequest) Type() string { return "HttpRequest" } + +// Cast converts value to *types.HttpRequest +func (HttpRequest) Cast(val interface{}) (TypedValue, error) { + return NewHttpRequest(val) +} + +// Assign new value to HttpRequest +// +// value is first passed through CastToHttpRequest +func (t *HttpRequest) Assign(val interface{}) error { + if c, err := CastToHttpRequest(val); err != nil { + return err + } else { + t.value = c + return nil + } +} + +func (t *HttpRequest) AssignFieldValue(key string, val TypedValue) error { + t.mux.Lock() + defer t.mux.Unlock() + return assignToHttpRequest(t.value, key, val) +} + +// SelectGVal implements gval.Selector requirements +// +// It allows gval lib to access HttpRequest's underlying value (*types.HttpRequest) +// and it's fields +// +func (t *HttpRequest) SelectGVal(ctx context.Context, k string) (interface{}, error) { + t.mux.RLock() + defer t.mux.RUnlock() + return httpRequestGValSelector(t.value, k) +} + +// Select is field accessor for *types.HttpRequest +// +// Similar to SelectGVal but returns typed values +func (t *HttpRequest) Select(k string) (TypedValue, error) { + t.mux.RLock() + defer t.mux.RUnlock() + return httpRequestTypedValueSelector(t.value, k) +} + +func (t *HttpRequest) Has(k string) bool { + t.mux.RLock() + defer t.mux.RUnlock() + switch k { + case "Method": + return true + case "URL": + return true + case "Header": + return true + case "Body": + return true + case "Form": + return true + case "PostForm": + return true + } + return false +} + +// httpRequestGValSelector is field accessor for *types.HttpRequest +func httpRequestGValSelector(res *types.HttpRequest, k string) (interface{}, error) { + if res == nil { + return nil, nil + } + switch k { + case "Method": + return res.Method, nil + case "URL": + return res.URL, nil + case "Header": + return res.Header, nil + case "Body": + return res.Body, nil + case "Form": + return res.Form, nil + case "PostForm": + return res.PostForm, nil + } + + return nil, fmt.Errorf("unknown field '%s'", k) +} + +// httpRequestTypedValueSelector is field accessor for *types.HttpRequest +func httpRequestTypedValueSelector(res *types.HttpRequest, k string) (TypedValue, error) { + if res == nil { + return nil, nil + } + switch k { + case "Method": + return NewString(res.Method) + case "URL": + return NewUrl(res.URL) + case "Header": + return NewKVV(res.Header) + case "Body": + return NewReader(res.Body) + case "Form": + return NewKVV(res.Form) + case "PostForm": + return NewKVV(res.PostForm) + } + + return nil, fmt.Errorf("unknown field '%s'", k) +} + +// assignToHttpRequest is field value setter for *types.HttpRequest +func assignToHttpRequest(res *types.HttpRequest, k string, val interface{}) error { + switch k { + case "Method": + aux, err := CastToString(val) + if err != nil { + return err + } + + res.Method = aux + return nil + case "URL": + aux, err := CastToUrl(val) + if err != nil { + return err + } + + res.URL = aux + return nil + case "Header": + aux, err := CastToKVV(val) + if err != nil { + return err + } + + res.Header = aux + return nil + case "Body": + aux, err := CastToReader(val) + if err != nil { + return err + } + + res.Body = aux + return nil + case "Form": + aux, err := CastToKVV(val) + if err != nil { + return err + } + + res.Form = aux + return nil + case "PostForm": + aux, err := CastToKVV(val) + if err != nil { + return err + } + + res.PostForm = aux + return nil + } + + return fmt.Errorf("unknown field '%s'", k) +} + // QueueMessage is an expression type, wrapper for *types.QueueMessage type type QueueMessage struct { value *types.QueueMessage @@ -1102,6 +1299,245 @@ func assignToTemplateMeta(res types.TemplateMeta, k string, val interface{}) err return fmt.Errorf("unknown field '%s'", k) } +// Url is an expression type, wrapper for *types.Url type +type Url struct { + value *types.Url + mux sync.RWMutex +} + +// NewUrl creates new instance of Url expression type +func NewUrl(val interface{}) (*Url, error) { + if c, err := CastToUrl(val); err != nil { + return nil, fmt.Errorf("unable to create Url: %w", err) + } else { + return &Url{value: c}, nil + } +} + +// Get return underlying value on Url +func (t *Url) Get() interface{} { + t.mux.RLock() + defer t.mux.RUnlock() + return t.value +} + +// GetValue returns underlying value on Url +func (t *Url) GetValue() *types.Url { + t.mux.RLock() + defer t.mux.RUnlock() + return t.value +} + +// Type return type name +func (Url) Type() string { return "Url" } + +// Cast converts value to *types.Url +func (Url) Cast(val interface{}) (TypedValue, error) { + return NewUrl(val) +} + +// Assign new value to Url +// +// value is first passed through CastToUrl +func (t *Url) Assign(val interface{}) error { + if c, err := CastToUrl(val); err != nil { + return err + } else { + t.value = c + return nil + } +} + +func (t *Url) AssignFieldValue(key string, val TypedValue) error { + t.mux.Lock() + defer t.mux.Unlock() + return assignToUrl(t.value, key, val) +} + +// SelectGVal implements gval.Selector requirements +// +// It allows gval lib to access Url's underlying value (*types.Url) +// and it's fields +// +func (t *Url) SelectGVal(ctx context.Context, k string) (interface{}, error) { + t.mux.RLock() + defer t.mux.RUnlock() + return urlGValSelector(t.value, k) +} + +// Select is field accessor for *types.Url +// +// Similar to SelectGVal but returns typed values +func (t *Url) Select(k string) (TypedValue, error) { + t.mux.RLock() + defer t.mux.RUnlock() + return urlTypedValueSelector(t.value, k) +} + +func (t *Url) Has(k string) bool { + t.mux.RLock() + defer t.mux.RUnlock() + switch k { + case "Scheme": + return true + case "Opaque": + return true + case "Host": + return true + case "Path": + return true + case "RawPath": + return true + case "ForceQuery": + return true + case "RawQuery": + return true + case "Fragment": + return true + case "RawFragment": + return true + } + return false +} + +// urlGValSelector is field accessor for *types.Url +func urlGValSelector(res *types.Url, k string) (interface{}, error) { + if res == nil { + return nil, nil + } + switch k { + case "Scheme": + return res.Scheme, nil + case "Opaque": + return res.Opaque, nil + case "Host": + return res.Host, nil + case "Path": + return res.Path, nil + case "RawPath": + return res.RawPath, nil + case "ForceQuery": + return res.ForceQuery, nil + case "RawQuery": + return res.RawQuery, nil + case "Fragment": + return res.Fragment, nil + case "RawFragment": + return res.RawFragment, nil + } + + return nil, fmt.Errorf("unknown field '%s'", k) +} + +// urlTypedValueSelector is field accessor for *types.Url +func urlTypedValueSelector(res *types.Url, k string) (TypedValue, error) { + if res == nil { + return nil, nil + } + switch k { + case "Scheme": + return NewString(res.Scheme) + case "Opaque": + return NewString(res.Opaque) + case "Host": + return NewString(res.Host) + case "Path": + return NewString(res.Path) + case "RawPath": + return NewString(res.RawPath) + case "ForceQuery": + return NewBoolean(res.ForceQuery) + case "RawQuery": + return NewString(res.RawQuery) + case "Fragment": + return NewString(res.Fragment) + case "RawFragment": + return NewString(res.RawFragment) + } + + return nil, fmt.Errorf("unknown field '%s'", k) +} + +// assignToUrl is field value setter for *types.Url +func assignToUrl(res *types.Url, k string, val interface{}) error { + switch k { + case "Scheme": + aux, err := CastToString(val) + if err != nil { + return err + } + + res.Scheme = aux + return nil + case "Opaque": + aux, err := CastToString(val) + if err != nil { + return err + } + + res.Opaque = aux + return nil + case "Host": + aux, err := CastToString(val) + if err != nil { + return err + } + + res.Host = aux + return nil + case "Path": + aux, err := CastToString(val) + if err != nil { + return err + } + + res.Path = aux + return nil + case "RawPath": + aux, err := CastToString(val) + if err != nil { + return err + } + + res.RawPath = aux + return nil + case "ForceQuery": + aux, err := CastToBoolean(val) + if err != nil { + return err + } + + res.ForceQuery = aux + return nil + case "RawQuery": + aux, err := CastToString(val) + if err != nil { + return err + } + + res.RawQuery = aux + return nil + case "Fragment": + aux, err := CastToString(val) + if err != nil { + return err + } + + res.Fragment = aux + return nil + case "RawFragment": + aux, err := CastToString(val) + if err != nil { + return err + } + + res.RawFragment = aux + return nil + } + + return fmt.Errorf("unknown field '%s'", k) +} + // User is an expression type, wrapper for *types.User type type User struct { value *types.User diff --git a/system/automation/expr_types.go b/system/automation/expr_types.go index 5738be451..5c9ff6713 100644 --- a/system/automation/expr_types.go +++ b/system/automation/expr_types.go @@ -5,6 +5,8 @@ import ( "fmt" "io" "io/ioutil" + "net/http" + "net/url" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/expr" @@ -184,6 +186,54 @@ func CastToQueueMessage(val interface{}) (out *types.QueueMessage, err error) { } } +func CastToHttpRequest(val interface{}) (out *types.HttpRequest, err error) { + switch val := val.(type) { + case expr.Iterator: + out = &types.HttpRequest{} + return out, val.Each(func(k string, v expr.TypedValue) error { + return assignToHttpRequest(out, k, v) + }) + } + + switch val := expr.UntypedValue(val).(type) { + case *http.Request: + rr := &types.HttpRequest{} + + assignToHttpRequest(rr, "Method", val.Method) + assignToHttpRequest(rr, "URL", val.URL) + assignToHttpRequest(rr, "Header", val.Header) + assignToHttpRequest(rr, "Body", val.Body) + assignToHttpRequest(rr, "Form", val.Form) + assignToHttpRequest(rr, "PostForm", val.PostForm) + + return rr, nil + case *types.HttpRequest: + return val, nil + case nil: + return &types.HttpRequest{}, nil + default: + return &types.HttpRequest{}, fmt.Errorf("unable to cast type %T to %T", val, out) + } +} + +func CastToUrl(val interface{}) (out *types.Url, err error) { + switch val := expr.UntypedValue(val).(type) { + case *url.URL: + u := &types.Url{} + + m, _ := json.Marshal(val) + _ = json.Unmarshal(m, u) + + return u, nil + case *types.Url: + return val, nil + case nil: + return &types.Url{}, nil + default: + return &types.Url{}, fmt.Errorf("unable to cast type %T to %T", val, out) + } +} + func (doc renderedDocument) String() string { aux, _ := ioutil.ReadAll(doc.Document) return string(aux) diff --git a/system/automation/expr_types.yaml b/system/automation/expr_types.yaml index c768d0334..8eb307ccc 100644 --- a/system/automation/expr_types.yaml +++ b/system/automation/expr_types.yaml @@ -72,6 +72,29 @@ types: - { name: 'Queue', exprType: 'String', goType: 'string' } - { name: 'Payload', exprType: 'Bytes', goType: '[]byte' } + HttpRequest: + as: '*types.HttpRequest' + struct: + - { name: 'Method', exprType: 'String', goType: 'string' } + - { name: 'URL', exprType: 'Url', goType: 'url.Url' } + - { name: 'Header', exprType: 'KVV', goType: 'map[string][]string' } + - { name: 'Body', exprType: 'Reader', goType: 'io.Reader' } + - { name: 'Form', exprType: 'KVV', goType: 'map[string][]string' } + - { name: 'PostForm', exprType: 'KVV', goType: 'map[string][]string' } + + Url: + as: '*types.Url' + struct: + - { name: 'Scheme', exprType: 'String', goType: 'string' } + - { name: 'Opaque', exprType: 'String', goType: 'string' } + - { name: 'Host', exprType: 'String', goType: 'string' } + - { name: 'Path', exprType: 'String', goType: 'string' } + - { name: 'RawPath', exprType: 'String', goType: 'string' } + - { name: 'ForceQuery', exprType: 'Boolean', goType: 'bool' } + - { name: 'RawQuery', exprType: 'String', goType: 'string' } + - { name: 'Fragment', exprType: 'String', goType: 'string' } + - { name: 'RawFragment', exprType: 'String', goType: 'string' } + RbacResource: as: 'rbac.Resource' diff --git a/system/types/http_request.go b/system/types/http_request.go new file mode 100644 index 000000000..75f5442f5 --- /dev/null +++ b/system/types/http_request.go @@ -0,0 +1,28 @@ +package types + +import ( + "io" +) + +type ( + HttpRequest struct { + Method string `json:"method"` + URL *Url `json:"url"` + Header map[string][]string `json:"header"` + Body io.Reader `json:"body"` + Form map[string][]string `json:"form"` + PostForm map[string][]string `json:"post_form"` + } + + Url struct { + Scheme string + Opaque string + Host string + Path string + RawPath string + ForceQuery bool + RawQuery string + Fragment string + RawFragment string + } +)