'See https://github.com/AnywhereSoftware/B4A/blob/b22087de45d1556b105b71c36a6555243ddf6211/Libs_OkHttp/src/anywheresoftware/b4h/okhttp/OkHttpClientWrapper.java#L265
'See https://www.b4x.com/android/forum/threads/http-digest-authorization-fails-in-b4a.158831/post-975228
Public Sub AuthenticateUrl(Url As String, Username As String, Password As String) As ResumableSub
Dim j As HttpJob
Dim initialCall As HttpJob
initialCall.Initialize("",Me)
initialCall.Download(Url)
Wait For (initialCall) JobDone(initialCall As HttpJob)
If initialCall.Success = False And initialCall.Response.StatusCode = 401 Then
Log(initialCall.Response.GetHeaders)
Dim headers As List = initialCall.Response.GetHeaders.Get("www-authenticate") ' looks like all headers are lowercased when retrieved
If headers <> Null And headers.Size <> 0 Then
If headers.Size > 1 Then Log($"[WARNING] URL (${Url}) returned multiple WWW-Authenticate challenges. Processing only first challenge entry"$)
Dim header As String = headers.Get(0)
Log($"[Debug] WWW-Authenticate header: ${header}"$)
If header.StartsWith("Digest") Then
Dim directives As String = header.SubString("Digest".Length+1)
Log($"[Debug] WWW-Authenticate directives: ${directives}"$)
Dim retVals() As String = HandleDigest(initialCall.Response, directives, Username, Password)
If retVals(1) <> "" Then
j = initialCall
j.ErrorMessage = retVals(1)
Else
initialCall.Release
Dim authenticationCall As HttpJob
authenticationCall.Initialize("",Me)
authenticationCall.Download(Url)
authenticationCall.GetRequest.SetHeader("Authorization", retVals(0))
Wait For (authenticationCall) JobDone(authenticationCall As HttpJob)
j = authenticationCall
If j.Success = False Then
Log($"[ERROR] StatusCode=${authenticationCall.Response.StatusCode}"$)
Log($"[Debug] ${authenticationCall.Response.GetHeaders}"$)
End If
End If
Else
j = initialCall
j.ErrorMessage = $"[ERROR] Wrong authentication scheme. Expected [Digest]"$
End If
Else
j = initialCall
j.ErrorMessage = $"URL (${Url}) did not have a WWW-Authenticate header"$
End If
Else
' The proper response was not received
j = initialCall
If j.Success Then ' In case it was a successful non-proper response, overwrite Success and ErrorMessage
j.Success = False
j.ErrorMessage = $"URL (${Url}) did not respond with a 401 status code"$
End If
End If
Return j
End Sub
'See https://github.com/AnywhereSoftware/B4A/blob/b22087de45d1556b105b71c36a6555243ddf6211/Libs_OkHttp/src/anywheresoftware/b4h/okhttp/OkHttpClientWrapper.java#L290
'See https://www.b4x.com/android/forum/threads/http-digest-authorization-fails-in-b4a.158831/post-975228
Private Sub HandleDigest(response As OkHttpResponse, directives As String, username As String, password As String) As String()
Dim digestInfo(2) As String
Dim sb As StringBuilder
sb.Initialize
Dim rawParams() As String = Regex.Split(",", directives)
If rawParams.Length > 0 Then
Dim params As Map
params.Initialize
For x = 0 To rawParams.Length - 1
Log($"[Debug] Directive #${x}: ${rawParams(x)}"$)
Dim keyValue() As String = Regex.Split("=", rawParams(x))
If keyValue.Length = 1 Then
params.Put(keyValue(0).Trim, "")
else if keyValue.Length = 2 Then
params.Put(keyValue(0).Trim, keyValue(1).Trim)
Else
Log($"[Warning] Unhandled WWW-Authenticate directive: ${rawParams(x)}"$)
End If
Next
'Technically RFC7616 requires a qop (see https://datatracker.ietf.org/doc/html/rfc7616#section-3.3),
' but @Erel's code makes it optional, so it's optional here
' If params.GetDefault("qop","") = "" Then
' digestInfo(1) = "[ERROR] Digest authentication requires the qop directive"
' else if params.GetDefault("nonce","") = "" Then
If params.GetDefault("nonce","") = "" Then
digestInfo(1) = "[ERROR] Digest authentication requires the nonce directive"
Else
Dim algorithms As List = Array As String ("MD5", "SHA-256")
'algorithms.Initialize2(Array As String ("MD5", "SHA-256"))
Dim algorithm As String = params.GetDefault("algorithm", "MD5")
If algorithms.IndexOf(algorithm) <> - 1 Then
Dim md As MessageDigest
Dim bc As ByteConverter
Try
'https://datatracker.ietf.org/doc/html/rfc7616#section-3.4.1
'Create H(A1) -
Dim realm As String = params.GetDefault("realm", "")
Dim unq_realm As String = realm.Replace($"""$,"")
Dim A1 As String = $"${username}:${unq_realm}:${password}"$
Log($"A1 = ${A1}"$)
Dim H_A1 As String = bc.HexFromBytes(md.GetMessageDigest(A1.GetBytes("ISO-8859-1"), algorithm)).ToLowerCase
'Create H(A2)
Dim jo As JavaObject = response
jo = jo.GetField("response")
Dim joRequest As JavaObject = jo.RunMethod("request", Null)
Dim method As String = joRequest.RunMethod("method", Null)
Dim uri As String = requestPath(joRequest.RunMethod("url", Null))
Dim A2 As String = $"${method}:${uri}"$
Log($"A2 = ${A2}"$)
Dim H_A2 As String = bc.HexFromBytes(md.GetMessageDigest(A2.GetBytes("ASCII"), algorithm)).ToLowerCase
'Build the response directive
Dim digestResponseValue As String ' contains the text that will be hashed/hex-encoded
Dim NC As String = "00000001"
'Dim NC As String = "00000002"
Dim cnonce As String
Dim nonce As String = params.Get("nonce") ' Already checked if available
'nonce = $""812682c2e8c20123364385b8723b0fed""$
Dim unq_nonce As String = nonce.Replace($"""$,"")
Dim qop As String = params.GetDefault("qop", "")
If qop = "" Then
digestResponseValue = $"${H_A1}:${unq_nonce}:${H_A2}"$
Else
If qop.Contains("auth") Then
qop = "auth"
cnonce =bc.HexFromBytes( md.GetMessageDigest($"${DateTime.Now}"$.As(String).GetBytes("ASCII"), algorithm)).ToLowerCase
'cnonce = "c2c6d5e0f80c4ef4"
digestResponseValue = $"${H_A1}:${unq_nonce}:${NC}:${cnonce}:${qop}:${H_A2}"$
Else
digestInfo(1) = $"[ERROR] No suported qop option found. Server returned qop="${qop}""$
End If
End If
Log($"digestResponsValue = ${digestResponseValue}"$)
If digestInfo(1) = "" Then
Dim digestResponse As String = bc.HexFromBytes(md.GetMessageDigest(digestResponseValue.GetBytes("ASCII"), algorithm)).ToLowerCase
Dim sb As StringBuilder
sb.Initialize
sb.Append($"Digest username="${username}",realm=${realm},algorithm=${algorithm},nonce=${nonce},uri="${uri}","$)
If qop <> "" Then
sb.Append($"qop=${qop},nc=${NC},cnonce="${cnonce}","$)
End If
sb.Append($"response="${digestResponse}""$)
If params.GetDefault("opaque","") <> "" Then
sb.Append($", opaque=${params.Get("opaque")}"$)
End If
digestInfo(0) = sb.ToString
Log($"[Debug] return digest value=${digestInfo(0)}"$)
End If
'digestInfo(1) = $"[ERROR] Incomplete implemenation"$
Catch
digestInfo(1) = $"[ERROR] Exception error during digest creation: ${LastException.Message}"$
End Try
'
Else
digestInfo(1) = $"[ERROR] Unsupported digest algorithm: ${algorithm}"$
End If
End If
Else
digestInfo(1) = $"[ERROR] No directives given for digest authentication scheme"$
End If
If digestInfo(1) <> "" Then Log(digestInfo(1))
Return digestInfo
End Sub
'See https://github.com/square/okhttp/blob/master/okhttp/src/main/kotlin/okhttp3/internal/http/RequestLine.kt
Private Sub requestPath(url As JavaObject) As String
Dim path As String = url.RunMethod("encodedPath", Null)
Dim query As String = url.RunMethod("encodedQuery", Null)
Return IIf(query <> "null", $"${path}?${query}"$, path)
End Sub