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  }