Floatplane channel plugin for Jellyfin 10.10.x: browse, HLS proxy, cookie session, CI release
Some checks failed
release / build-release (push) Has been cancelled
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>
This commit is contained in:
308
doc/research/reference-sources/hydra_tokenUtil.brs
Normal file
308
doc/research/reference-sources/hydra_tokenUtil.brs
Normal file
@@ -0,0 +1,308 @@
|
||||
'********************************************************************
|
||||
'** 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
|
||||
|
||||
Reference in New Issue
Block a user