В настоящее время я разрабатываю игру-викторину для внешнего инструмента в Moodle. Я следую спецификации IMS LTI и использую OAuth для аутентификации, как это требуется.
Недавно мне удалось аутентифицировать POST-запрос запуска от Moodle, и теперь я пытаюсь отправить оценки обратно в Moodle из моего инструмента с помощью LTI Basic Outcome Service.
Вот где я столкнулся с проблемой: я создаю сообщения POX, подписываю запрос и отправляю его, но по какой-то причине они выполняются один раз каждые несколько попыток (30% времени, более или менее). Для остальных из них Moodle отвечает сообщением о сбое POX, которое включает в себя «Подпись сообщения недействительна» в качестве описания.
Очевидно, я хочу верить, что мои запросы будут выполняться всегда, когда они мне нужны.
В качестве иллюстрации я покажу вам ниже пример успешного и неудавшегося запроса, а также базовую строку каждого (тот, который OAuth принимает для подписания):
Для успешного запроса:
- Базовая строка запроса (разрывы строк только для отображения):
POST&http%3A%2F%2F127.0.0.1%2Fmoodle%2Fmod%2Flti%2Fservice.php
&oauth_body_hash%3DhPssgohenJEvtKta2so7Y27p3kU%253D%26oauth_callback%3Dabout%253Ablank
%26oauth_consumer_key%3Dkey%26oauth_nonce%3D63cc0764c4cc4701abe28fa5fd406378
%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1525940779%26oauth_version%3D1.0
Тело ответа:
<?xml version="1.0" encoding="UTF-8"?>
<imsx_POXEnvelopeResponse xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
<imsx_POXHeader>
<imsx_POXResponseHeaderInfo>
<imsx_version>V1.0</imsx_version>
<imsx_messageIdentifier>1602471533</imsx_messageIdentifier>
<imsx_statusInfo>
<imsx_codeMajor>success</imsx_codeMajor>
<imsx_severity>status</imsx_severity>
<imsx_description>Result read</imsx_description>
<imsx_messageRefIdentifier>1339905165</imsx_messageRefIdentifier>
<imsx_operationRefIdentifier>readResultRequest</imsx_operationRefIdentifier>
</imsx_statusInfo>
</imsx_POXResponseHeaderInfo>
</imsx_POXHeader>
<imsx_POXBody>
<readResultResponse>
<result>
<resultScore>
<language>en</language>
<textString>0.5</textString>
</resultScore>
</result>
</readResultResponse>
</imsx_POXBody>
</imsx_POXEnvelopeResponse>
Для неудавшегося запроса:
- Базовая строка запроса (разрывы строк только для отображения):
POST&http%3A%2F%2F127.0.0.1%2Fmoodle%2Fmod%2Flti%2Fservice.php
&oauth_body_hash%3DYLigJE%252B8wr7rCwOITqdc1IP3zFs%253D%26oauth_callback%3Dabout%253Ablank
%26oauth_consumer_key%3Dkey%26oauth_nonce%3D6de4380ce2ab4d9a90e3fe1723dc5141
%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1525940816%26oauth_version%3D1.0
Тело ответа:
<?xml version="1.0" encoding="UTF-8"?>
<imsx_POXEnvelopeResponse xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
<imsx_POXHeader>
<imsx_POXResponseHeaderInfo>
<imsx_version>V1.0</imsx_version>
<imsx_messageIdentifier>1688463600</imsx_messageIdentifier>
<imsx_statusInfo>
<imsx_codeMajor>failure</imsx_codeMajor>
<imsx_severity>status</verity>
<imsx_description>Message signature not valid</imsx_description>
<imsx_messageRefIdentifier/>
<imsx_operationRefIdentifier>unknownRequest</imsx_operationRefIdentifier>
</imsx_statusInfo>
</imsx_POXResponseHeaderInfo>
</imsx_POXHeader>
<imsx_POXBody>
<unknownResponse/>
</imsx_POXBody>
</imsx_POXEnvelopeResponse>
Как видите, обе базовые строки очень похожи, и я могу найти различия только в тех частях, которые должны быть разными (например, отметка времени или одноразовый номер), поэтому я не могу понять, почему некоторые запросы быть отклоненным, в то время как другие принимаются.
Есть какие-нибудь идеи о причине, по которой это может происходить? Любые предложения о том, как узнать?
Заранее благодарю за помощь.
РЕДАКТИРОВАТЬ: я добавляю сюда код, который я использую в процессе подписи
private IEnumerator SendGradeRequest(XmlDocument xml)
{
string url = parametrosIniciales["lis_outcome_service_url"];
byte[] entityBody = Encoding.UTF8.GetBytes(xml.OuterXml);
string bodyHash = oAuth.GetBodyHash(entityBody);
Dictionary<string, string> oAuthParameters = oAuth.PrepareOAuthParameters(oAuth.GetSessionOAuthParameters(parametrosIniciales));
oAuthParameters.Add("oauth_body_hash", bodyHash);
Dictionary<string, string> headers = new Dictionary<string, string>();
headers.Add("Content-Type", "application/xml");
headers.Add("Authorization", oAuth.GetAuthorizationHeader(oAuthParameters));
string signature = oAuth.GetRequestSignature(url, headers, entityBody);
oAuthParameters.Add("oauth_signature", signature);
headers["Authorization"] = oAuth.GetAuthorizationHeader(oAuthParameters);
WWW web = new WWW(url, entityBody, headers);
// ... Code to manage the response
}
public string GetRequestSignature(string url, Dictionary<string, string> headers, byte[] body)
{
string baseString = GetBaseString(url, headers, body);
string key = GetKeyParts(JsonToDict(GameManager.Instance.parametrosIniciales));
return ComputeHMACSHA1(key, baseString);
}
private string GetBaseString(string url, Dictionary<string, string> headers, object body = null)
{
string[] parts = { UrlEncodeRFC3986 (GetNormalizedHttpMethod(body)),
UrlEncodeRFC3986 (GetNormalizedHttpURL(url)),
UrlEncodeRFC3986 (GetSignableParameters(url, headers, body)) };
return String.Join("&", parts);
}
private string GetKeyParts(Dictionary<string, string> oAuthParameters)
{
string consumer, token, consumerSecret, tokenSecret;
if (oAuthParameters.TryGetValue("oauth_consumer_key", out consumer))
consumerSecret = UrlEncodeRFC3986(secrets[consumer]);
else
throw new Exception("Not consumer key found. If not using any encription, this method should not be called");
if (oAuthParameters.TryGetValue("oauth_token", out token))
tokenSecret = UrlEncodeRFC3986(secrets[token]);
else
tokenSecret = "";
return consumerSecret + "&" + tokenSecret;
}
private string GetSignableParameters(string url, Dictionary<string, string> headers, object body = null)
{
List<String> parametros = new List<string>();
// 1. Parameters in the OAuth HTTP Authorization header excluding the realm parameter
string oAuthHeader;
if (headers.TryGetValue("Authorization", out oAuthHeader))
{
oAuthHeader = oAuthHeader.Substring(oAuthHeader.IndexOf(" ") + 1).Replace("\"", "");
string[] pairs = oAuthHeader.Split(',');
for (int i = 0; i < pairs.Length; i++)
{
string pair = pairs[i];
string[] temp = pair.Split(new char[] { '=' }, 2);
temp = UrlEncodeRFC3986(temp);
pairs[i] = String.Join("=", temp);
}
parametros.InsertRange(parametros.Count, pairs);
}
// 2. HTTP GET parameters added to the URLs in the query part (as defined by [RFC3986] section 3)
if (url.Contains("?"))
parametros.InsertRange(parametros.Count, url.Substring(url.IndexOf("?")).Split('&'));
// 3. Parameters in the HTTP POST request body (with a content-type of application/x-www-form-urlencoded)
if (body != null && headers["Content-Type"] == "application/x-www-form-urlencoded" && body is Dictionary<string, string>)
{
Dictionary<string, string> cuerpo = RemoveSpareParameters((Dictionary<string, string>)body);
parametros.InsertRange(parametros.Count, DictToPairsList(cuerpo, true));
}
// 4. Sort all parameters by lexicographical value
parametros.Sort();
// 5. Concatenate all parameters with '&'
return String.Join("&", parametros.ToArray());
}