github.com/pelicanplatform/pelican@v1.0.5/origin_ui/origin_api.go (about) 1 /*************************************************************** 2 * 3 * Copyright (C) 2023, Pelican Project, Morgridge Institute for Research 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); you 6 * may not use this file except in compliance with the License. You may 7 * obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 ***************************************************************/ 18 19 package origin_ui 20 21 import ( 22 "context" 23 "fmt" 24 "strings" 25 "sync" 26 "time" 27 28 "github.com/gin-gonic/gin" 29 "github.com/pelicanplatform/pelican/director" 30 "github.com/pelicanplatform/pelican/metrics" 31 "github.com/pkg/errors" 32 log "github.com/sirupsen/logrus" 33 ) 34 35 var ( 36 // Mutex for safe concurrent access to the timer 37 timerMutex sync.Mutex 38 // Timer for tracking timeout 39 directorTimeoutTimer *time.Timer 40 // Duration to wait before timeout 41 // TODO: Do we want to make this a configurable value? 42 directorTimeoutDuration = 30 * time.Second 43 exitLoop = make(chan struct{}) 44 ) 45 46 // Check the Bearer token from requests sent from the director to ensure 47 // it's has correct authorization 48 func directorRequestAuthHandler(ctx *gin.Context) { 49 authHeader := ctx.Request.Header.Get("Authorization") 50 51 // Check if the Authorization header was provided 52 if authHeader == "" { 53 // Use AbortWithStatusJSON to stop invoking the next chain 54 ctx.AbortWithStatusJSON(401, gin.H{"error": "Authorization header is missing"}) 55 return 56 } 57 58 // Check if the Authorization type is Bearer 59 if !strings.HasPrefix(authHeader, "Bearer ") { 60 ctx.AbortWithStatusJSON(401, gin.H{"error": "Authorization header is not Bearer type"}) 61 return 62 } 63 64 // Extract the token from the Authorization header 65 token := strings.TrimPrefix(authHeader, "Bearer ") 66 valid, err := director.VerifyDirectorTestReportToken(token) 67 68 if err != nil { 69 log.Warningln(fmt.Sprintf("Error when verifying Bearer token: %s", err)) 70 ctx.AbortWithStatusJSON(401, gin.H{"error": fmt.Sprintf("Error when verifying Bearer token: %s", err)}) 71 return 72 } 73 74 if !valid { 75 log.Warningln("Can't validate Bearer token") 76 ctx.AbortWithStatusJSON(401, gin.H{"error": "Can't validate Bearer token"}) 77 return 78 } 79 ctx.Next() 80 } 81 82 // Reset the timer safely 83 func resetDirectorTimeoutTimer() { 84 timerMutex.Lock() 85 defer timerMutex.Unlock() 86 87 if directorTimeoutTimer == nil { 88 directorTimeoutTimer = time.NewTimer(directorTimeoutDuration) 89 go func() { 90 for { 91 select { 92 case <-directorTimeoutTimer.C: 93 // Timer fired because no message was received in time. 94 log.Warningln("No director test report received within the time limit") 95 metrics.SetComponentHealthStatus(metrics.OriginCache_Director, metrics.StatusCritical, "No director test report received within the time limit") 96 // Reset the timer for the next period. 97 timerMutex.Lock() 98 directorTimeoutTimer.Reset(directorTimeoutDuration) 99 timerMutex.Unlock() 100 case <-exitLoop: 101 log.Infoln("Gracefully terminating the director-health test timeout loop...") 102 return 103 } 104 } 105 }() 106 } else { 107 if !directorTimeoutTimer.Stop() { 108 <-directorTimeoutTimer.C 109 } 110 directorTimeoutTimer.Reset(directorTimeoutDuration) 111 } 112 } 113 114 // Director will periodiclly upload/download files to/from all connected 115 // origins and test the health status of origins. It will send a request 116 // reporting such status to this endpoint, and we will update origin internal 117 // health status metric to reflect the director connection status. 118 func directorTestResponse(ctx *gin.Context) { 119 dt := director.DirectorTest{} 120 if err := ctx.ShouldBind(&dt); err != nil { 121 log.Errorf("Invalid director test response") 122 ctx.JSON(400, gin.H{"error": "Invalid director test response"}) 123 return 124 } 125 // We will let the timer go timeout if director didn't send a valid json request 126 resetDirectorTimeoutTimer() 127 if dt.Status == "ok" { 128 metrics.SetComponentHealthStatus(metrics.OriginCache_Director, metrics.StatusOK, fmt.Sprintf("Director timestamp: %v", dt.Timestamp)) 129 ctx.JSON(200, gin.H{"msg": "Success"}) 130 } else if dt.Status == "error" { 131 metrics.SetComponentHealthStatus(metrics.OriginCache_Director, metrics.StatusCritical, dt.Message) 132 ctx.JSON(200, gin.H{"msg": "Success"}) 133 } else { 134 log.Errorf("Invalid director test response, status: %s", dt.Status) 135 ctx.JSON(400, gin.H{"error": fmt.Sprintf("Invalid director test response status: %s", dt.Status)}) 136 } 137 } 138 139 // Configure API endpoints for origin that are not tied to UI 140 func ConfigureOriginAPI(router *gin.Engine, ctx context.Context, wg *sync.WaitGroup) error { 141 if router == nil { 142 return errors.New("Origin configuration passed a nil pointer") 143 } 144 145 metrics.SetComponentHealthStatus(metrics.OriginCache_Director, metrics.StatusWarning, "Initializing origin, unknown status for director") 146 // start the timer for the director test report timeout 147 resetDirectorTimeoutTimer() 148 149 go func() { 150 // Gracefully stop the timer at the exit of the program 151 defer wg.Done() 152 <-ctx.Done() 153 timerMutex.Lock() 154 defer timerMutex.Unlock() 155 // Terminate the infinite loop to reset the timer 156 close(exitLoop) 157 if directorTimeoutTimer != nil { 158 directorTimeoutTimer.Stop() 159 directorTimeoutTimer = nil 160 } 161 log.Infoln("Gracefully stopping the director-health test timeout timer...") 162 }() 163 164 group := router.Group("/api/v1.0/origin-api") 165 group.POST("/directorTest", directorRequestAuthHandler, directorTestResponse) 166 167 return nil 168 }