From a0edff1e9d8e613110aea1d1305a5a48144fee53 Mon Sep 17 00:00:00 2001 From: Xevion Date: Fri, 1 Mar 2024 00:51:38 -0600 Subject: [PATCH] Setup dynamic session timing & regeneration/setup flows tied to DoRequest --- api.go | 74 +++++++++++++++++++++++++++++++++++++++++++----------- helpers.go | 5 ++++ main.go | 5 ---- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/api.go b/api.go index b059fd4..eb031ca 100644 --- a/api.go +++ b/api.go @@ -10,11 +10,55 @@ import ( "strconv" "strings" + "time" + "github.com/redis/go-redis/v9" "github.com/rs/zerolog/log" ) -var sessionID string = RandomString(5) + Nonce() +var ( + latestSession string = "" + sessionTime time.Time + expiryTime time.Duration = 25 * time.Minute +) + +// ResetSessionTimer resets the session timer to the current time. +// This is only used by the DoRequest handler when Banner API calls are detected, which would reset the session timer. +func ResetSessionTimer() { + // Only reset the session time if the session is still valid + if time.Since(sessionTime) <= expiryTime { + sessionTime = time.Now() + } +} + +// GenerateSession generates a new session ID (nonce) for use with the Banner API. +// Don't use this function directly, use GetSession instead. +func GenerateSession() string { + return RandomString(5) + Nonce() +} + +// GetSession retrieves the current session ID if it's still valid. +// If the session ID is invalid or has expired, a new one is generated and returned. +// SessionIDs are valid for 30 minutes, but we'll be conservative and regenerate every 25 minutes. +func GetSession() string { + // Check if a reset is required + if latestSession == "" || time.Since(sessionTime) >= expiryTime { + // Generate a new session identifier + latestSession = GenerateSession() + + // Select the current term + term := Default(time.Now()).ToString() + log.Info().Str("term", term).Str("sessionID", latestSession).Msg("Setting selected term") + err := SelectTerm(term, latestSession) + if err != nil { + log.Fatal().Stack().Err(err).Msg("Failed to select term while generating session ID") + } + + sessionTime = time.Now() + } + + return latestSession +} type Pair struct { Code string `json:"code"` @@ -81,14 +125,14 @@ func GetTerms(search string, page int, max int) ([]BannerTerm, error) { // SelectTerm selects the given term in the Banner system. // This function completes the initial term selection process, which is required before any other API calls can be made with the session ID. -func SelectTerm(term string) { +func SelectTerm(term string, sessionId string) error { form := url.Values{ "term": {term}, "studyPath": {""}, "studyPathText": {""}, "startDatepicker": {""}, "endDatepicker": {""}, - "uniqueSessionId": {sessionID}, + "uniqueSessionId": {sessionId}, } params := map[string]string{ @@ -100,19 +144,19 @@ func SelectTerm(term string) { res, err := DoRequest(req) if err != nil { - log.Panic().Stack().Err(err).Msg("Failed to select term") + return fmt.Errorf("failed to select term: %w", err) } // Assert that the response is JSON if !ContentTypeMatch(res, "application/json") { - log.Panic().Stack().Str("content-type", res.Header.Get("Content-Type")).Msg("Response was not JSON") + return fmt.Errorf("response was not JSON: %w", res.Header.Get("Content-Type")) } // Acquire fwdUrl defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { - log.Panic().Stack().Err(err).Msg("Failed to read response body") + return fmt.Errorf("failed to read response body: %w", err) } var redirectResponse struct { @@ -124,13 +168,15 @@ func SelectTerm(term string) { req = BuildRequest("GET", redirectResponse.FwdUrl, nil) res, err = DoRequest(req) if err != nil { - log.Panic().Stack().Err(err).Msg("Redirect request failed") + return fmt.Errorf("failed to follow redirect: %w", err) } // Assert that the response is OK (200) if res.StatusCode != 200 { - log.Panic().Stack().Int("status", res.StatusCode).Msg("Unexpected status code from redirect request") + return fmt.Errorf("redirect response was not 200: %w", res.StatusCode) } + + return nil } // GetPartOfTerms retrieves and parses the part of term information for a given term. @@ -146,7 +192,7 @@ func GetPartOfTerms(search string, term int, offset int, max int) ([]BannerTerm, "term": strconv.Itoa(term), "offset": strconv.Itoa(offset), "max": strconv.Itoa(max), - "uniqueSessionId": sessionID, + "uniqueSessionId": GetSession(), "_": Nonce(), }) @@ -190,7 +236,7 @@ func GetInstructors(search string, term string, offset int, max int) ([]Instruct "term": term, "offset": strconv.Itoa(offset), "max": strconv.Itoa(max), - "uniqueSessionId": sessionID, + "uniqueSessionId": GetSession(), "_": Nonce(), }) @@ -255,7 +301,7 @@ func Search(query *Query, sort string, sortDescending bool) (*SearchResult, erro params := query.Paramify() params["txt_term"] = "202420" // TODO: Make this automatic but dynamically specifiable - params["uniqueSessionId"] = sessionID + params["uniqueSessionId"] = GetSession() params["sortColumn"] = sort params["sortDirection"] = "asc" @@ -305,7 +351,7 @@ func GetSubjects(search string, term string, offset int, max int) ([]Pair, error "term": term, "offset": strconv.Itoa(offset), "max": strconv.Itoa(max), - "uniqueSessionId": sessionID, + "uniqueSessionId": GetSession(), "_": Nonce(), }) @@ -349,7 +395,7 @@ func GetCampuses(search string, term int, offset int, max int) ([]Pair, error) { "term": strconv.Itoa(term), "offset": strconv.Itoa(offset), "max": strconv.Itoa(max), - "uniqueSessionId": sessionID, + "uniqueSessionId": GetSession(), "_": Nonce(), }) @@ -393,7 +439,7 @@ func GetInstructionalMethods(search string, term string, offset int, max int) ([ "term": term, "offset": strconv.Itoa(offset), "max": strconv.Itoa(max), - "uniqueSessionId": sessionID, + "uniqueSessionId": GetSession(), "_": Nonce(), }) diff --git a/helpers.go b/helpers.go index bb2260b..5ab8b29 100644 --- a/helpers.go +++ b/helpers.go @@ -129,6 +129,11 @@ func DoRequest(req *http.Request) (*http.Response, error) { contentLengthHeader := res.Header.Get("Content-Length") contentLength := int64(-1) + // If this request was a Banner API request, reset the session timer + if strings.HasPrefix(req.URL.Path, "StudentRegistrationSsb/ssb/classSearch/") { + ResetSessionTimer() + } + // Get the content length if contentLengthHeader != "" { contentLength, err = strconv.ParseInt(contentLengthHeader, 10, 64) diff --git a/main.go b/main.go index 883cdbb..286ac8e 100644 --- a/main.go +++ b/main.go @@ -308,11 +308,6 @@ func main() { log.Fatal().Stack().Err(err).Msg("Cannot fetch terms on startup") } - // Term Select Pre-Search POST - term := Default(time.Now()).ToString() - log.Info().Str("term", term).Str("sessionID", sessionID).Msg("Setting selected term") - SelectTerm(term) - // Launch a goroutine to scrape the banner system periodically go func() { for {