Some checks failed
release / build-release (push) Has been cancelled
- IChannel: creators -> sub-channels -> video posts (public listing API, search mapped) - IRequiresMediaInfoCallback -> signed proxy URLs; stream controller rewrites HLS so the AES-128 key is fetched server-side with the session cookie, segments stay on the CDN - delivery/info cached per token lifetime and throttled (~2/min) - config page: paste sails.sid + validate, extra creators, quality, search test - scripts/package.sh -> plugin folder + zip + md5 + manifest.json; Gitea release workflow - research docs, HAR analysis (HAR itself gitignored), install-pi5.sh Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
309 lines
15 KiB
Plaintext
309 lines
15 KiB
Plaintext
'********************************************************************
|
|
'** TokenUtil - OAuth Token Management
|
|
'********************************************************************
|
|
'** Testing token validity:
|
|
'** - Time-based: isTokenExpired() / isAuthenticated() use stored token_expires_at
|
|
'** (from server's expires_in). Fast, no network; does not detect server revocation.
|
|
'** - Server-based: Run validateTokenTask; when done, check task.isValid. True only
|
|
'** if the server returns 200 for an authenticated request (catches revocation).
|
|
'********************************************************************
|
|
|
|
function TokenUtil() as Object
|
|
tokenUtilObj = {
|
|
|
|
'** Get access token, refreshing if necessary
|
|
'@param skipRefresh
|
|
'@return access token string or invalid if not available
|
|
getAccessToken: function(skipRefresh = false as Boolean) as Dynamic
|
|
registry = RegistryUtil()
|
|
accessToken = registry.read("access_token", "hydravion")
|
|
|
|
if accessToken = invalid
|
|
print "[TOKEN] No access token found in registry"
|
|
return invalid
|
|
end if
|
|
|
|
' Check if token is expired (or about to expire)
|
|
if m.isTokenExpired()
|
|
if skipRefresh
|
|
' Called from render thread - return existing token, refresh will happen in Task
|
|
print "[TOKEN] Access token expired but skipping refresh (render thread), will refresh when used in Task"
|
|
return accessToken
|
|
end if
|
|
|
|
print "[TOKEN] Access token expired or expiring soon, attempting refresh..."
|
|
' Try to refresh
|
|
if m.refreshToken()
|
|
' Get the new token
|
|
newToken = registry.read("access_token", "hydravion")
|
|
print "[TOKEN] Successfully refreshed token"
|
|
return newToken
|
|
else
|
|
' Refresh failed, token invalid
|
|
print "[TOKEN] Token refresh failed, user must re-authenticate"
|
|
return invalid
|
|
end if
|
|
end if
|
|
|
|
' Log token status for debugging (only occasionally to avoid spam)
|
|
m.logTokenStatus()
|
|
|
|
return accessToken
|
|
end function
|
|
|
|
'** Check if access token is expired (time-based, no network)
|
|
' Uses token_expires_at stored from server's expires_in when token was issued/refreshed.
|
|
' For authoritative validity (e.g. detect revocation), use validateTokenTask instead.
|
|
'@return true if expired or missing, false if valid
|
|
isTokenExpired: function() as Boolean
|
|
registry = RegistryUtil()
|
|
expiresAt = registry.read("token_expires_at", "hydravion")
|
|
|
|
if expiresAt = invalid
|
|
print "[TOKEN] No expiration time found, token considered expired"
|
|
return true
|
|
end if
|
|
|
|
currentTime = CreateObject("roDateTime")
|
|
currentTimeSeconds = currentTime.AsSeconds()
|
|
expiresAtSeconds = Val(expiresAt)
|
|
|
|
refreshBufferSeconds = 300
|
|
timeUntilExpiration = expiresAtSeconds - currentTimeSeconds
|
|
|
|
if timeUntilExpiration <= refreshBufferSeconds
|
|
print "[TOKEN] Token expires in " + timeUntilExpiration.ToStr() + " seconds (buffer: " + refreshBufferSeconds.ToStr() + "), will refresh"
|
|
return true
|
|
end if
|
|
|
|
return false
|
|
end function
|
|
|
|
'** Refresh the access token using refresh token
|
|
'@return true if successful, false otherwise
|
|
refreshToken: function() as Boolean
|
|
registry = RegistryUtil()
|
|
refreshToken = registry.read("refresh_token", "hydravion")
|
|
|
|
if refreshToken = invalid
|
|
print "[TOKEN] No refresh token available"
|
|
return false
|
|
end if
|
|
|
|
refreshExpiresAt = registry.read("refresh_token_expires_at", "hydravion")
|
|
if refreshExpiresAt <> invalid
|
|
currentTime = CreateObject("roDateTime")
|
|
currentTimeSeconds = currentTime.AsSeconds()
|
|
refreshExpiresAtSeconds = Val(refreshExpiresAt)
|
|
|
|
if currentTimeSeconds >= refreshExpiresAtSeconds
|
|
print "[TOKEN] Stored refresh token expiration time has passed, but attempting refresh anyway (backend may still accept it)"
|
|
else
|
|
timeUntilRefreshExpiration = refreshExpiresAtSeconds - currentTimeSeconds
|
|
days = timeUntilRefreshExpiration \ 86400
|
|
hours = (timeUntilRefreshExpiration mod 86400) \ 3600
|
|
if days > 0
|
|
print "[TOKEN] Refresh token valid, expires in " + days.ToStr() + " days " + hours.ToStr() + " hours"
|
|
else
|
|
minutes = timeUntilRefreshExpiration \ 60
|
|
print "[TOKEN] Refresh token valid, expires in " + minutes.ToStr() + " minutes"
|
|
end if
|
|
end if
|
|
else
|
|
print "[TOKEN] No refresh token expiration time found (may be from old login), attempting refresh"
|
|
end if
|
|
|
|
' Request new token
|
|
appInfo = createObject("roAppInfo")
|
|
version = appInfo.getVersion()
|
|
useragent = "Hydravion (Roku) v" + version
|
|
|
|
apiConfigObj = ApiConfig()
|
|
url = apiConfigObj.buildRealmAuthUrl("openid-connect/token")
|
|
https = CreateObject("roUrlTransfer")
|
|
https.RetainBodyOnError(true)
|
|
port = CreateObject("roMessagePort")
|
|
https.SetMessagePort(port)
|
|
https.SetUrl(url)
|
|
https.setCertificatesFile("common:/certs/ca-bundle.crt")
|
|
https.AddHeader("Content-Type", "application/x-www-form-urlencoded")
|
|
https.AddHeader("Accept", "application/json")
|
|
https.AddHeader("User-Agent", useragent)
|
|
https.initClientCertificates()
|
|
|
|
' Send refresh token request
|
|
postData = "grant_type=refresh_token&client_id=hydravion&refresh_token=" + https.Escape(refreshToken)
|
|
|
|
if https.AsyncPostFromString(postData)
|
|
while (true)
|
|
event = wait(10000, port)
|
|
if type(event) = "roUrlEvent"
|
|
code = event.GetResponseCode()
|
|
if code = 200
|
|
response = ParseJSON(event.GetString())
|
|
if response <> invalid and response.access_token <> invalid
|
|
' Store new tokens
|
|
registry.write("access_token", response.access_token, "hydravion")
|
|
|
|
' Update expiration time
|
|
currentTime = CreateObject("roDateTime")
|
|
currentTimeSeconds = currentTime.AsSeconds()
|
|
tokenExpirationTime = currentTimeSeconds + response.expires_in
|
|
registry.write("token_expires_at", tokenExpirationTime.ToStr(), "hydravion")
|
|
|
|
' Update refresh token if provided
|
|
if response.refresh_token <> invalid
|
|
registry.write("refresh_token", response.refresh_token, "hydravion")
|
|
end if
|
|
|
|
' Update refresh token expiration
|
|
if response.refresh_expires_in <> invalid and response.refresh_expires_in > 0
|
|
refreshExpirationTime = currentTimeSeconds + response.refresh_expires_in
|
|
registry.write("refresh_token_expires_at", refreshExpirationTime.ToStr(), "hydravion")
|
|
refreshDays = response.refresh_expires_in \ 86400
|
|
refreshHours = (response.refresh_expires_in mod 86400) \ 3600
|
|
if refreshDays > 0
|
|
print "[TOKEN] Refresh token expires in " + refreshDays.ToStr() + " days (from API)"
|
|
else
|
|
refreshMinutes = response.refresh_expires_in \ 60
|
|
print "[TOKEN] Refresh token expires in " + refreshMinutes.ToStr() + " minutes (from API)"
|
|
end if
|
|
else
|
|
refreshExpirationTime = currentTimeSeconds + (30 * 24 * 60 * 60) ' 30 days in seconds
|
|
registry.write("refresh_token_expires_at", refreshExpirationTime.ToStr(), "hydravion")
|
|
print "[TOKEN] Refresh token has 30-day idle limit (refresh_expires_in=0), extending expiration by 30 days from now"
|
|
end if
|
|
|
|
accessMinutes = response.expires_in \ 60
|
|
print "[TOKEN] Token refreshed successfully, new access token expires in " + accessMinutes.ToStr() + " minutes"
|
|
return true
|
|
else
|
|
print "[TOKEN] Invalid refresh response"
|
|
m.clearTokens()
|
|
return false
|
|
end if
|
|
else
|
|
responseBody = event.GetString()
|
|
print "[TOKEN] Token refresh error: " + code.ToStr() + " - " + responseBody
|
|
' If refresh fails, clear tokens (likely expired or invalid)
|
|
m.clearTokens()
|
|
return false
|
|
end if
|
|
else if event = invalid
|
|
https.AsyncCancel()
|
|
m.clearTokens()
|
|
return false
|
|
end if
|
|
end while
|
|
end if
|
|
|
|
return false
|
|
end function
|
|
|
|
'** Clear all stored tokens
|
|
clearTokens: function() as Void
|
|
registry = RegistryUtil()
|
|
registry.delete("access_token", "hydravion")
|
|
registry.delete("refresh_token", "hydravion")
|
|
registry.delete("token_expires_at", "hydravion")
|
|
registry.delete("refresh_token_expires_at", "hydravion")
|
|
' Also clear old sails cookie if it exists
|
|
registry.delete("sails", "hydravion")
|
|
end function
|
|
|
|
'** Check if user is authenticated (has valid token)
|
|
'@return true if authenticated, false otherwise
|
|
' Note: Does not attempt refresh (to avoid render thread issues)
|
|
' Actual refresh will happen when token is used in Task context
|
|
isAuthenticated: function() as Boolean
|
|
registry = RegistryUtil()
|
|
accessToken = registry.read("access_token", "hydravion")
|
|
|
|
if accessToken = invalid
|
|
return false
|
|
end if
|
|
|
|
' Check if token is expired (but don't refresh here - that happens in Task context)
|
|
if m.isTokenExpired()
|
|
' Token exists but is expired - still consider authenticated
|
|
' Refresh will happen when token is actually used in a Task
|
|
return true
|
|
end if
|
|
|
|
return true
|
|
end function
|
|
|
|
'** Log token status for debugging/testing
|
|
' Only logs occasionally to avoid spam
|
|
logTokenStatus: function() as Void
|
|
' Use a simple counter to log only every 10th call
|
|
if m.logCounter = invalid
|
|
m.logCounter = 0
|
|
end if
|
|
m.logCounter = m.logCounter + 1
|
|
|
|
' Log every 10th call (roughly every 10 API calls)
|
|
if m.logCounter mod 10 = 0
|
|
registry = RegistryUtil()
|
|
expiresAt = registry.read("token_expires_at", "hydravion")
|
|
if expiresAt <> invalid
|
|
currentTime = CreateObject("roDateTime")
|
|
currentTimeSeconds = currentTime.AsSeconds()
|
|
expiresAtSeconds = Val(expiresAt)
|
|
timeUntilExpiration = expiresAtSeconds - currentTimeSeconds
|
|
minutes = timeUntilExpiration \ 60
|
|
seconds = timeUntilExpiration mod 60
|
|
print "[TOKEN] Token valid, expires in " + minutes.ToStr() + "m " + seconds.ToStr() + "s"
|
|
end if
|
|
end if
|
|
end function
|
|
|
|
'** Get token status for debugging/testing
|
|
'@return object with token status information
|
|
getTokenStatus: function() as Object
|
|
registry = RegistryUtil()
|
|
status = {
|
|
hasAccessToken: false
|
|
hasRefreshToken: false
|
|
accessTokenExpiresAt: invalid
|
|
refreshTokenExpiresAt: invalid
|
|
timeUntilAccessExpiration: invalid
|
|
timeUntilRefreshExpiration: invalid
|
|
isAccessTokenExpired: false
|
|
isRefreshTokenExpired: false
|
|
}
|
|
|
|
accessToken = registry.read("access_token", "hydravion")
|
|
status.hasAccessToken = (accessToken <> invalid)
|
|
|
|
refreshToken = registry.read("refresh_token", "hydravion")
|
|
status.hasRefreshToken = (refreshToken <> invalid)
|
|
|
|
expiresAt = registry.read("token_expires_at", "hydravion")
|
|
if expiresAt <> invalid
|
|
status.accessTokenExpiresAt = expiresAt
|
|
currentTime = CreateObject("roDateTime")
|
|
currentTimeSeconds = currentTime.AsSeconds()
|
|
expiresAtSeconds = Val(expiresAt)
|
|
status.timeUntilAccessExpiration = expiresAtSeconds - currentTimeSeconds
|
|
status.isAccessTokenExpired = m.isTokenExpired()
|
|
end if
|
|
|
|
refreshExpiresAt = registry.read("refresh_token_expires_at", "hydravion")
|
|
if refreshExpiresAt <> invalid
|
|
status.refreshTokenExpiresAt = refreshExpiresAt
|
|
currentTime = CreateObject("roDateTime")
|
|
currentTimeSeconds = currentTime.AsSeconds()
|
|
refreshExpiresAtSeconds = Val(refreshExpiresAt)
|
|
status.timeUntilRefreshExpiration = refreshExpiresAtSeconds - currentTimeSeconds
|
|
status.isRefreshTokenExpired = (currentTimeSeconds >= refreshExpiresAtSeconds)
|
|
end if
|
|
|
|
return status
|
|
end function
|
|
}
|
|
|
|
return tokenUtilObj
|
|
end function
|
|
|