pkg/server/server.go (382 lines of code) (raw):

// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // 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. package server import ( "errors" "fmt" "math" "net/http" "os" "path" "strconv" "strings" "github.com/GoogleCloudPlatform/khi/pkg/common/filter" "github.com/GoogleCloudPlatform/khi/pkg/inspection" "github.com/GoogleCloudPlatform/khi/pkg/inspection/metadata" inspection_task "github.com/GoogleCloudPlatform/khi/pkg/inspection/task" "github.com/GoogleCloudPlatform/khi/pkg/parameters" "github.com/GoogleCloudPlatform/khi/pkg/popup" "github.com/GoogleCloudPlatform/khi/pkg/server/config" "github.com/GoogleCloudPlatform/khi/pkg/server/upload" "github.com/gin-contrib/cors" "github.com/gin-contrib/static" "github.com/gin-gonic/gin" ) type ServerConfig struct { ViewerMode bool StaticFolderPath string ResourceMonitor ResourceMonitor ServerBasePath string UploadFileStore *upload.UploadFileStore } func redirectMiddleware(exactPath string, redirectTo string) gin.HandlerFunc { return func(ctx *gin.Context) { if ctx.Request.URL.Path == exactPath { ctx.Redirect(302, redirectTo) return } ctx.Next() } } func CreateKHIServer(inspectionServer *inspection.InspectionTaskServer, serverConfig *ServerConfig) *gin.Engine { engine := instanciateGinServer(parameters.Debug.Verbose != nil && *parameters.Debug.Verbose) corsConfig := cors.DefaultConfig() corsConfig.AllowAllOrigins = true appHtmlPath := path.Join(serverConfig.StaticFolderPath, "/index.html") basePathWithoutTrailingSlash := strings.TrimSuffix(serverConfig.ServerBasePath, "/") engine.Use(redirectMiddleware(basePathWithoutTrailingSlash+"/", basePathWithoutTrailingSlash+"/session/0")) // Request for `/` shouldn't be handled by `static.Serve`, redirect `/session/0` to be handled by patternToString engine.Use(static.Serve(basePathWithoutTrailingSlash+"/", static.LocalFile(serverConfig.StaticFolderPath, false))) engine.Use(gin.Recovery()) engine.Use(cors.New(corsConfig)) router := engine.Group(basePathWithoutTrailingSlash) // frontend uses Angular router. All frontend routing path should return the app html router.GET("/session/*wild", func(ctx *gin.Context) { ctx.Header("Content-Type", "text/html") file, err := os.ReadFile(appHtmlPath) if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } originalIndexHTML := string(file) replacedIndexHtml, err := replaceDynamicPartOfIndex(originalIndexHTML) if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } ctx.Writer.Write([]byte(replacedIndexHtml)) }) // GET /api/v3/config // Returns configuration map used in frontend. router.GET("/api/v3/config", func(ctx *gin.Context) { ctx.JSON(http.StatusOK, config.NewGetConfigResponseFromParameters()) }) if !serverConfig.ViewerMode { // GET /api/v3/inspection/types // Returns the list of inspection types available on the inspection server. router.GET("/api/v3/inspection/types", func(ctx *gin.Context) { ctx.JSON(http.StatusOK, &GetInspectionTypesResponse{ Types: inspectionServer.GetAllInspectionTypes(), }) }) // GET /api/v3/inspection // Returns the all started inspections on the inspection server. router.GET("/api/v3/inspection", func(ctx *gin.Context) { inspections := inspectionServer.GetAllRunners() responseInspections := map[string]SerializedMetadata{} for _, inspection := range inspections { if inspection.Started() { md, err := inspection.GetCurrentMetadata() if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } m, err := metadata.GetSerializableSubsetMapFromMetadataSet(md, filter.NewEnabledFilter(metadata.LabelKeyIncludedInTaskListFlag, false)) if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } responseInspections[inspection.ID] = m } } ctx.JSON(http.StatusOK, &GetInspectionsResponse{ Inspections: responseInspections, ServerStat: &ServerStat{ TotalMemoryAvailable: serverConfig.ResourceMonitor.GetUsedMemory(), }, }) }) // POST /api/v3/inspection/tasks router.POST("/api/v3/inspection/types/:typeID", func(ctx *gin.Context) { typeID := ctx.Param("typeID") inspectionId, err := inspectionServer.CreateInspection(typeID) if err != nil { // only the not found error is expected here ctx.String(http.StatusNotFound, err.Error()) return } ctx.JSON(http.StatusAccepted, &PostInspectionResponse{InspectionID: inspectionId}) }) // PUT /api/v3/inspection/<inspection-id>/features router.PUT("/api/v3/inspection/:inspectionID/features", func(ctx *gin.Context) { inspectionID := ctx.Param("inspectionID") task := inspectionServer.GetInspection(inspectionID) if task == nil { ctx.String(http.StatusNotFound, fmt.Sprintf("inspecton %s was not found", inspectionID)) return } var reqBody PutInspectionFeatureRequest if err := ctx.ShouldBindJSON(&reqBody); err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } err := task.SetFeatureList(reqBody.Features) if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } ctx.String(http.StatusAccepted, "ok") }) // PATCH /api/v3/inspection/<inspection-id>/features router.PATCH("/api/v3/inspection/:inspectionID/features", func(ctx *gin.Context) { inspectionID := ctx.Param("inspectionID") task := inspectionServer.GetInspection(inspectionID) if task == nil { ctx.String(http.StatusNotFound, fmt.Sprintf("inspecton %s was not found", inspectionID)) return } var reqBody PatchInspectionFeatureRequest if err := ctx.ShouldBindJSON(&reqBody); err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } err := task.UpdateFeatureMap(reqBody.Features) if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } ctx.String(http.StatusAccepted, "ok") }) // GET /api/v3/inspection/<inspection-id>/features router.GET("/api/v3/inspection/:inspectionID/features", func(ctx *gin.Context) { inspectionID := ctx.Param("inspectionID") task := inspectionServer.GetInspection(inspectionID) if task == nil { ctx.String(http.StatusNotFound, fmt.Sprintf("inspecton %s was not found", inspectionID)) return } features, err := task.FeatureList() if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } ctx.JSON(http.StatusOK, GetInspectionFeatureResponse{ Features: features, }) }) router.POST("/api/v3/inspection/:inspectionID/dryrun", func(ctx *gin.Context) { inspectionID := ctx.Param("inspectionID") currentTask := inspectionServer.GetInspection(inspectionID) if currentTask == nil { ctx.String(http.StatusNotFound, fmt.Sprintf("inspecton %s was not found", inspectionID)) return } var reqBody PostInspectionDryRunRequest if err := ctx.ShouldBindJSON(&reqBody); err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } result, err := currentTask.DryRun(ctx, &inspection_task.InspectionRequest{ Values: reqBody, }) if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } ctx.JSON(http.StatusOK, result) }) router.POST("/api/v3/inspection/:inspectionID/run", func(ctx *gin.Context) { inspectionID := ctx.Param("inspectionID") currentTask := inspectionServer.GetInspection(inspectionID) if currentTask == nil { ctx.String(http.StatusNotFound, fmt.Sprintf("inspecton %s was not found", inspectionID)) return } var reqBody PostInspectionDryRunRequest if err := ctx.ShouldBindJSON(&reqBody); err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } err := currentTask.Run(ctx, &inspection_task.InspectionRequest{ Values: reqBody, }) if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } ctx.String(http.StatusAccepted, "ok") }) router.POST("/api/v3/inspection/:inspectionID/cancel", func(ctx *gin.Context) { inspectionID := ctx.Param("inspectionID") currentTask := inspectionServer.GetInspection(inspectionID) if currentTask == nil { ctx.String(http.StatusNotFound, fmt.Sprintf("inspecton %s was not found", inspectionID)) return } err := currentTask.Cancel() if err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } ctx.String(http.StatusOK, "ok") }) router.GET("/api/v3/inspection/:inspectionID/metadata", func(ctx *gin.Context) { inspectionID := ctx.Param("inspectionID") currentTask := inspectionServer.GetInspection(inspectionID) if currentTask == nil { ctx.String(http.StatusNotFound, fmt.Sprintf("inspecton %s was not found", inspectionID)) return } result, err := currentTask.Metadata() if err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } ctx.JSON(http.StatusOK, result) }) router.GET("/api/v3/inspection/:inspectionID/data", func(ctx *gin.Context) { inspectionID := ctx.Param("inspectionID") currentTask := inspectionServer.GetInspection(inspectionID) if currentTask == nil { ctx.String(http.StatusNotFound, fmt.Sprintf("inspecton %s was not found", inspectionID)) return } // parse range queries var rangeStart int64 var maxSize int64 = math.MaxInt64 startQueryStr := ctx.Query("start") maxSizeQueryStr := ctx.Query("maxSize") if startQueryStr != "" { var err error rangeStart, err = strconv.ParseInt(startQueryStr, 10, 64) if err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } } if maxSizeQueryStr != "" { var err error maxSize, err = strconv.ParseInt(maxSizeQueryStr, 10, 64) if err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } } result, err := currentTask.Result() if err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } inspectionDataReader, err := result.ResultStore.GetRangeReader(rangeStart, maxSize) if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } defer inspectionDataReader.Close() fileSize, err := result.ResultStore.GetInspectionResultSizeInBytes() if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } ctx.DataFromReader(http.StatusOK, int64(math.Min(float64(maxSize), float64(fileSize-int(rangeStart)))), "application/octet-stream", inspectionDataReader, map[string]string{}) }) router.GET("/api/v3/popup", func(ctx *gin.Context) { currentPopup := popup.Instance.GetCurrentPopup() if currentPopup == nil { ctx.String(http.StatusOK, "") return } ctx.JSON(http.StatusOK, currentPopup) }) router.POST("/api/v3/popup/validate", func(ctx *gin.Context) { request := &popup.PopupAnswerResponse{} if err := ctx.ShouldBindJSON(request); err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } result, err := popup.Instance.Validate(request) if errors.Is(err, popup.NoCurrentPopup) { ctx.String(http.StatusNotFound, err.Error()) return } if errors.Is(err, popup.CurrentPopupIsntMatchingWithGivenId) { ctx.String(http.StatusBadRequest, err.Error()) return } if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } ctx.JSON(http.StatusOK, result) }) router.POST("/api/v3/popup/answer", func(ctx *gin.Context) { request := &popup.PopupAnswerResponse{} if err := ctx.ShouldBindJSON(request); err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } err := popup.Instance.Answer(request) if errors.Is(err, popup.NoCurrentPopup) { ctx.String(http.StatusNotFound, err.Error()) return } if errors.Is(err, popup.CurrentPopupIsntMatchingWithGivenId) { ctx.String(http.StatusBadRequest, err.Error()) return } if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } ctx.String(http.StatusOK, "") }) router.POST("/api/v3/upload", func(ctx *gin.Context) { localUploadFileStoreProvider, convertible := serverConfig.UploadFileStore.StoreProvider.(*upload.LocalUploadFileStoreProvider) if !convertible { ctx.String(http.StatusBadRequest, "invalid operation. Current UploadFileStore.StoreProvider is not supporting to be written directly") return } file, err := ctx.FormFile("file") if err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } id := ctx.Request.FormValue("upload-token-id") if id == "" { ctx.String(http.StatusBadRequest, "missing upload-token-id") return } token := &upload.DirectUploadToken{ID: id} if parameters.Server.MaxUploadFileSizeInBytes != nil && *parameters.Server.MaxUploadFileSizeInBytes < int(file.Size) { ctx.String(http.StatusBadRequest, fmt.Sprintf("file size exceeds the limit (%d bytes)", *parameters.Server.MaxUploadFileSizeInBytes)) return } err = serverConfig.UploadFileStore.SetResultOnStartingUpload(token) if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return } multipart, err := file.Open() if err != nil { ctx.String(http.StatusBadRequest, err.Error()) return } defer multipart.Close() err = localUploadFileStoreProvider.Write(token, multipart) if err != nil { serverConfig.UploadFileStore.SetResultOnCompletedUpload(token, err) ctx.String(http.StatusInternalServerError, err.Error()) return } serverConfig.UploadFileStore.SetResultOnCompletedUpload(token, nil) ctx.String(http.StatusOK, "") }) } return engine } // instanciateGinServer generates a new instance of *gin.Engine with provided debug mode flag. func instanciateGinServer(debugMode bool) *gin.Engine { if debugMode { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) } engine := gin.New() if debugMode { engine.Use(gin.Logger()) } return engine }