Setup dynamic session timing & regeneration/setup flows tied to DoRequest

This commit is contained in:
2024-03-01 00:51:38 -06:00
parent e2bf31cb33
commit a0edff1e9d
3 changed files with 65 additions and 19 deletions

74
api.go
View File

@@ -10,11 +10,55 @@ import (
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/rs/zerolog/log" "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 { type Pair struct {
Code string `json:"code"` 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. // 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. // 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{ form := url.Values{
"term": {term}, "term": {term},
"studyPath": {""}, "studyPath": {""},
"studyPathText": {""}, "studyPathText": {""},
"startDatepicker": {""}, "startDatepicker": {""},
"endDatepicker": {""}, "endDatepicker": {""},
"uniqueSessionId": {sessionID}, "uniqueSessionId": {sessionId},
} }
params := map[string]string{ params := map[string]string{
@@ -100,19 +144,19 @@ func SelectTerm(term string) {
res, err := DoRequest(req) res, err := DoRequest(req)
if err != nil { 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 // Assert that the response is JSON
if !ContentTypeMatch(res, "application/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 // Acquire fwdUrl
defer res.Body.Close() defer res.Body.Close()
body, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
if err != nil { 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 { var redirectResponse struct {
@@ -124,13 +168,15 @@ func SelectTerm(term string) {
req = BuildRequest("GET", redirectResponse.FwdUrl, nil) req = BuildRequest("GET", redirectResponse.FwdUrl, nil)
res, err = DoRequest(req) res, err = DoRequest(req)
if err != nil { 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) // Assert that the response is OK (200)
if res.StatusCode != 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. // 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), "term": strconv.Itoa(term),
"offset": strconv.Itoa(offset), "offset": strconv.Itoa(offset),
"max": strconv.Itoa(max), "max": strconv.Itoa(max),
"uniqueSessionId": sessionID, "uniqueSessionId": GetSession(),
"_": Nonce(), "_": Nonce(),
}) })
@@ -190,7 +236,7 @@ func GetInstructors(search string, term string, offset int, max int) ([]Instruct
"term": term, "term": term,
"offset": strconv.Itoa(offset), "offset": strconv.Itoa(offset),
"max": strconv.Itoa(max), "max": strconv.Itoa(max),
"uniqueSessionId": sessionID, "uniqueSessionId": GetSession(),
"_": Nonce(), "_": Nonce(),
}) })
@@ -255,7 +301,7 @@ func Search(query *Query, sort string, sortDescending bool) (*SearchResult, erro
params := query.Paramify() params := query.Paramify()
params["txt_term"] = "202420" // TODO: Make this automatic but dynamically specifiable params["txt_term"] = "202420" // TODO: Make this automatic but dynamically specifiable
params["uniqueSessionId"] = sessionID params["uniqueSessionId"] = GetSession()
params["sortColumn"] = sort params["sortColumn"] = sort
params["sortDirection"] = "asc" params["sortDirection"] = "asc"
@@ -305,7 +351,7 @@ func GetSubjects(search string, term string, offset int, max int) ([]Pair, error
"term": term, "term": term,
"offset": strconv.Itoa(offset), "offset": strconv.Itoa(offset),
"max": strconv.Itoa(max), "max": strconv.Itoa(max),
"uniqueSessionId": sessionID, "uniqueSessionId": GetSession(),
"_": Nonce(), "_": Nonce(),
}) })
@@ -349,7 +395,7 @@ func GetCampuses(search string, term int, offset int, max int) ([]Pair, error) {
"term": strconv.Itoa(term), "term": strconv.Itoa(term),
"offset": strconv.Itoa(offset), "offset": strconv.Itoa(offset),
"max": strconv.Itoa(max), "max": strconv.Itoa(max),
"uniqueSessionId": sessionID, "uniqueSessionId": GetSession(),
"_": Nonce(), "_": Nonce(),
}) })
@@ -393,7 +439,7 @@ func GetInstructionalMethods(search string, term string, offset int, max int) ([
"term": term, "term": term,
"offset": strconv.Itoa(offset), "offset": strconv.Itoa(offset),
"max": strconv.Itoa(max), "max": strconv.Itoa(max),
"uniqueSessionId": sessionID, "uniqueSessionId": GetSession(),
"_": Nonce(), "_": Nonce(),
}) })

View File

@@ -129,6 +129,11 @@ func DoRequest(req *http.Request) (*http.Response, error) {
contentLengthHeader := res.Header.Get("Content-Length") contentLengthHeader := res.Header.Get("Content-Length")
contentLength := int64(-1) 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 // Get the content length
if contentLengthHeader != "" { if contentLengthHeader != "" {
contentLength, err = strconv.ParseInt(contentLengthHeader, 10, 64) contentLength, err = strconv.ParseInt(contentLengthHeader, 10, 64)

View File

@@ -308,11 +308,6 @@ func main() {
log.Fatal().Stack().Err(err).Msg("Cannot fetch terms on startup") 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 // Launch a goroutine to scrape the banner system periodically
go func() { go func() {
for { for {