diff --git a/.gitignore b/.gitignore index 5306450..2d44b43 100755 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ *.exe *.exe~ *.dll -*.so *.dylib # Test binary, built with `go test -c` diff --git a/Dockerfile b/Dockerfile index dace55d..a776136 100755 --- a/Dockerfile +++ b/Dockerfile @@ -4,11 +4,13 @@ FROM golang:1.18-alpine AS builder +RUN apk add --no-cache --update go gcc g++ + RUN mkdir /app ADD . /app WORKDIR /app -RUN CGO_ENABLED=0 GOOS=linux go build -o app cmd/server/main.go +RUN CGO_ENABLED=1 GOOS=linux go build -o app cmd/server/main.go # RUN go install github.com/cosmtrek/air@latest # RUN curl -sSfL https://raw.githubusercontent.com/cosmtrek/air/master/install.sh | sh -s diff --git a/cmd/server/main.go b/cmd/server/main.go index 9131728..d09e1d4 100755 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -8,6 +8,7 @@ import ( "github.com/nexentra/inteligpt/pkg/auth" "github.com/nexentra/inteligpt/pkg/comments" "github.com/nexentra/inteligpt/pkg/common/db" + "github.com/nexentra/inteligpt/middlware/jsight" "github.com/spf13/viper" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" @@ -65,6 +66,9 @@ func setupRouter() *gin.Engine { // fmt.Println(dsn) r := gin.Default() + + r.Use(jsight.Validator()) + h := db.InitDatabase(dbUrl) comments.RegisterRoutes(r, h) diff --git a/jsight/api-spec.jst b/jsight/api-spec.jst new file mode 100644 index 0000000..9a8b6c6 --- /dev/null +++ b/jsight/api-spec.jst @@ -0,0 +1,108 @@ +JSIGHT 0.3 + +INFO + Title "CloudNua: Comment Service" + Description + This is a simple CRUD HTTP service. + + [Terms of service](http://swagger.io/terms/) + [CloudNua API Support - Website](http://www.swagger.io/support) + [Send email to CloudNua API Support](mailto:support@cloudnua.io) + [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) + Version 1.0 + +SERVER @local + BaseUrl "localhost:8080" + +GET /ping + 200 + { + "message": "pong" + } + +#------------------------ COMMENTS --------------------------- + +GET /api/v1/comments + 200 + [@comment] + 404 + @error + +POST /api/v1/comments + Request + @comment + 201 // Created + @comment + 400 // Bad request + @error + 404 // Not Found + @error + +URL /api/v1/comments/{id} + Path + { + "id": 123 // Comment ID + } + +GET /api/v1/comments/{id} // Get specific comment + 200 + [@comment] + 404 // Not found + @error + +PUT /api/v1/comments/{id} // Update a comment + Request + @comment + 200 + @comment + 400 // Bad request + @error + 404 // Not Found + @error + +DELETE /api/v1/comments/{id} // Delete a comment + 200 + @comment + 400 // Bad request + @error + 404 // Not Found + @error + +#------------------------ COMMON -------------------------- +GET / + 200 any + +GET /jsight // Getting service API specification + 200 any +#------------------------ TYPES --------------------------- + +TYPE @comment +{ + "author": "johnsmith", + "slug" : "my-comment", + "title" : "My awesome comment" +} + +TYPE @error + @appError | @validationError + +TYPE @appError +{ + "code": 400, // Http status code + "message": "status not found" +} + +TYPE @validationError +{ + "reportedBy": "HTTP Request validation", + "type": "http_body_error", + "code": 32001, + "title": "HTTP body error", + "detail": "Schema does not support key \"title1\"", + "position": { // {optional: true} + "filepath": "api-spec.jst", // {optional: true} + "index": 58, // {optional: true} + "line": 4, // {optional: true} + "col": 3 // {optional: true} + } +} \ No newline at end of file diff --git a/middlware/jsight/jsight.go b/middlware/jsight/jsight.go new file mode 100644 index 0000000..9c083c7 --- /dev/null +++ b/middlware/jsight/jsight.go @@ -0,0 +1,174 @@ +package jsight + +import ( + "plugin" +) + +//------------------------- interfaces -------------------------------- + +func NewJSight(pluginPath string) JSight { + return newjsightPlugin(pluginPath) +} + +type JSight interface { + ValidateHTTPRequest(apiSpecFilePath, requestMethod, requestURI string, requestHeaders map[string][]string, requestBody []byte) JSightValidationError + ValidateHTTPResponse(apiSpecFilePath, requestMethod, requestURI string, responseStatusCode int, responseHeaders map[string][]string, responseBody []byte) JSightValidationError + ClearCache() + Stat() string +} + +type JSightValidationError interface { + ReportedBy() string + Type() string + Code() int + Title() string + Detail() string + Position() JSightPosition + Trace() []string + ToJSON() string +} + +type JSightPosition interface { + Filepath() string + Index() int + Line() int + Col() int +} + +//-------------------------- implementation --------------------------- + +type jsightPlugin struct { + validateHTTPRequestSymbol func( + apiSpecFilePath, requestMethod, requestURI string, + requestHeaders map[string][]string, + requestBody []byte, + ) error + validateHTTPResponseSymbol func( + apiSpecFilePath, requestMethod, requestURI string, + responseStatusCode int, + responseHeaders map[string][]string, + responseBody []byte, + ) error + clearCacheSymbol func() + statSymbol func() string +} + +type jsightValidationErrorStruct struct { + e jsightPluginValidationError +} + +type jsightPluginValidationError interface { + ReportedBy() string + Type() string + Code() int + Title() string + Detail() string + Position() any + Trace() []string + ToJSON() string +} + +func newjsightPlugin(pluginPath string) JSight { + j := jsightPlugin{} + + p, err := plugin.Open(pluginPath) + if err != nil { + panic(err) + } + + s, err := p.Lookup("JSightValidateHTTPRequest") + if err != nil { + panic(err) + } + j.validateHTTPRequestSymbol = s.(func(string, string, string, map[string][]string, []byte) error) + + s, err = p.Lookup("JSightValidateHTTPResponse") + if err != nil { + panic(err) + } + j.validateHTTPResponseSymbol = s.(func(string, string, string, int, map[string][]string, []byte) error) + + s, err = p.Lookup("JSightClearCache") + if err != nil { + panic(err) + } + j.clearCacheSymbol = s.(func()) + + s, err = p.Lookup("JSightStat") + if err != nil { + panic(err) + } + j.statSymbol = s.(func() string) + + return j +} + +func (j jsightPlugin) ValidateHTTPRequest( + apiSpecFilePath, requestMethod, requestURI string, + requestHeaders map[string][]string, + requestBody []byte) JSightValidationError { + e := j.validateHTTPRequestSymbol(apiSpecFilePath, requestMethod, requestURI, requestHeaders, requestBody) + if e == nil { + return nil + } + return newjsightValidationErrorStruct(e.(jsightPluginValidationError)) +} + +func (j jsightPlugin) ValidateHTTPResponse( + apiSpecFilePath, requestMethod, requestURI string, + responseCode int, + responseHeaders map[string][]string, + responseBody []byte) JSightValidationError { + e := j.validateHTTPResponseSymbol(apiSpecFilePath, requestMethod, requestURI, responseCode, responseHeaders, responseBody) + if e == nil { + return nil + } + return newjsightValidationErrorStruct(e.(jsightPluginValidationError)) +} + +func (j jsightPlugin) ClearCache() { + j.clearCacheSymbol() +} + +func (j jsightPlugin) Stat() string { + return j.statSymbol() +} + +func newjsightValidationErrorStruct(e jsightPluginValidationError) JSightValidationError { + return jsightValidationErrorStruct{e: e} +} + +func (j jsightValidationErrorStruct) ReportedBy() string { + return j.e.ReportedBy() +} + +func (j jsightValidationErrorStruct) Type() string { + return j.e.Type() +} + +func (j jsightValidationErrorStruct) Code() int { + return j.e.Code() +} + +func (j jsightValidationErrorStruct) Title() string { + return j.e.Title() +} + +func (j jsightValidationErrorStruct) Detail() string { + return j.e.Detail() +} + +func (j jsightValidationErrorStruct) Position() JSightPosition { + if j.e.Position() == nil { + return nil + } + return j.e.Position().(JSightPosition) +} + +func (j jsightValidationErrorStruct) Trace() []string { + return j.e.Trace() +} + +func (j jsightValidationErrorStruct) ToJSON() string { + return j.e.ToJSON() +} diff --git a/middlware/jsight/jsight_middleware.go b/middlware/jsight/jsight_middleware.go new file mode 100644 index 0000000..eaf36f0 --- /dev/null +++ b/middlware/jsight/jsight_middleware.go @@ -0,0 +1,88 @@ +package jsight + +import ( + "fmt" + "github.com/gin-gonic/gin" + "io" + "bytes" + "os" + "regexp" +) + +var jSight JSight + +type bodyLogWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +func (w bodyLogWriter) Write(b []byte) (int, error) { + w.body.Write(b) + return w.ResponseWriter.Write(b) +} + +func Validator() gin.HandlerFunc { + return func(c *gin.Context) { + if jSight == nil { + jSight = NewJSight("./middlware/jsight/jsightplugin-alpine.so") // For Alpine + // jSight = NewJSight("./middlware/jsight/jsightplugin.so") // For other linuxes + fmt.Println("JSight validator enabled") + fmt.Print(jSight.Stat()) + } + + // before request + + jsightSpecPath := "./jsight/api-spec.jst" + reqBody, _ := io.ReadAll(c.Request.Body) + + jSight.ClearCache() // Comment this line in production to gain performance!!! + + // validate request + err := jSight.ValidateHTTPRequest( + jsightSpecPath, + c.Request.Method, + c.Request.RequestURI, + c.Request.Header, + reqBody, + ) + + if err != nil { + c.Header("Content-Type", "application/json") + c.String(400, err.ToJSON()) + return + } + + // check, if the jsight spec was requested + matched, _ := regexp.MatchString(`.*jsight/?`, c.Request.RequestURI) + if matched { + jsightCode, _ := os.ReadFile(jsightSpecPath) + c.Writer.WriteHeader(200) + c.Writer.Write(jsightCode) + return + } + + blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} + c.Writer = blw + + c.Next() + + // before response + + // validate response + err = jSight.ValidateHTTPResponse( + jsightSpecPath, + c.Request.Method, + c.Request.RequestURI, + c.Writer.Status(), + c.Writer.Header(), + blw.body.Bytes(), + ) + + if err != nil { + c.Writer.WriteHeader(500) + c.Writer.Write([]byte("\n\nRESPONSE ERROR:\n\n")) + c.Writer.Write([]byte(err.ToJSON())) + return + } + } +} \ No newline at end of file diff --git a/middlware/jsight/jsightplugin-alpine.so b/middlware/jsight/jsightplugin-alpine.so new file mode 100644 index 0000000..00a6bd3 Binary files /dev/null and b/middlware/jsight/jsightplugin-alpine.so differ diff --git a/middlware/jsight/jsightplugin.so b/middlware/jsight/jsightplugin.so new file mode 100644 index 0000000..0497ced Binary files /dev/null and b/middlware/jsight/jsightplugin.so differ