以下の問題が発生していましたが解決したので記録しておきます。
同じような問題を抱える開発者もいると思うので参考になれば幸いです。
問題
SignalRの接続の際に以下のエラーが発生する場合がある。
The remote server returned an error: (429) Too Many Requests.
このエラーは不定期で発生し、エラーが発生しない場合もある。
Azure Functionsのログストリームで確認すると明らかに不自然な挙動がある。
Executing 'Negotiate' (Reason='This function was programmatically called via the host APIs.', Id=ユニークid1)
Executed 'Negotiate' (Succeeded, Id=ユニークid1, Duration=5ms)
Executing 'Negotiate' (Reason='This function was programmatically called via the host APIs.', Id=ユニークid2)
Executed 'Negotiate' (Succeeded, Id=ユニークid2, Duration=5ms)
Executing 'Negotiate' (Reason='This function was programmatically called via the host APIs.', ユニークid3)
Executed 'Negotiate' (Succeeded, Id=ユニークid3, Duration=5ms)
上記のエラーが出る場合はこのようなアクセスが集中している状態です。
典型的なDoS攻撃に見えます。
前提
Xamarin.FormsでのSignalRの実装は以下のサンプルを利用しています。
https://docs.microsoft.com/en-us/samples/xamarin/xamarin-forms-samples/webservices-azuresignalr/
サンプルは以下の構成になっています。
ChatServerではMicrosoft.Azure.WebJobs.Extensions.SignalRS 1.1.0を使用。
NegotiateのAuthorizationLevelがAnonymousになっている。
public static class Negotiate
{
[FunctionName("Negotiate")]
public static SignalRConnectionInfo GetSignalRInfo(
[HttpTrigger(AuthorizationLevel.Anonymous,"get",Route = "negotiate")]
HttpRequest req,
[SignalRConnectionInfo(HubName = "simplechat")]
SignalRConnectionInfo connectionInfo)
{
return connectionInfo;
}
}
ChatClient側Negotiateの呼び出し
public async Task ConnectAsync()
{
//(略)
string negotiateJson = await client.GetStringAsync($"{Constants.HostName}/api/negotiate");
NegotiateInfo negotiate = JsonConvert.DeserializeObject<NegotiateInfo>(negotiateJson);
HubConnection connection = new HubConnectionBuilder()
.AddNewtonsoftJsonProtocol()
.WithUrl(negotiate.Url, options =>
{
options.AccessTokenProvider = async () => negotiate.AccessToken;
})
.Build();
//(略)
}
解決策
Azure Functionsのセキュリティには関数キー(アプリキーとは別なので注意)というものがあります。
https://docs.microsoft.com/ja-jp/azure/azure-functions/security-concepts
※関数のアクセス キーあたりを参照
それを踏まえて実装
ChatServer側はAuthorizationLevel.AnonymousをFunctionに変更するだけ(Talkも修正してください)
public static class Negotiate
{
[FunctionName("Negotiate")]
public static SignalRConnectionInfo GetSignalRInfo(
[HttpTrigger(AuthorizationLevel.Function,"get",Route = "negotiate")]
HttpRequest req,
[SignalRConnectionInfo(HubName = "simplechat")]
SignalRConnectionInfo connectionInfo)
{
return connectionInfo;
}
}
ChatClient側はAzure Function Keyを設定します(埋め込みはよくないので検討してください)
public async Task ConnectAsync()
{
//(略)
string negotiateJson = await client.GetStringAsync($"{Constants.HostName}/api/negotiate?code={AZURE_FUNCTION_NEGOTIATE_KEY}");
NegotiateInfo negotiate = JsonConvert.DeserializeObject<NegotiateInfo>(negotiateJson);
HubConnection connection = new HubConnectionBuilder()
.AddNewtonsoftJsonProtocol()
.WithUrl(negotiate.Url, options =>
{
options.AccessTokenProvider = async () => negotiate.AccessToken;
})
.Build();
//(略)
}
結論
もともとサンプルを基に実装している段階でセキュアキーの設定がないことに???となっていたのですが、
参考にできるページがみつからなくて四苦八苦していました。
とはいえわかってしまえば当たり前な実装手法です。
今回は内部テストの段階で判明したので大事には至りませんでしたが、そのまま稼働させてたらと思うとぞっとします。
本当はサンプル内にコメントがほしいですがGitHubにissueできる英語力はないので他の人にお任せしますw