dvAzureStringToSign function
Builds the string Azure signs for a Shared Key request.
resourcePath is the encoded path as it goes on the wire, not the decoded
one. Exposed for testing: the ordering rules are the part that goes wrong,
and a wrong one is only visible as a 403 from the far end.
Implementation
String dvAzureStringToSign({
required String method,
required String account,
required String resourcePath,
required Map<String, String> queryParameters,
required Map<String, String> headers,
int contentLength = 0,
}) {
String header(String name) => headers[name.toLowerCase()] ?? '';
// Azure wants a blank here for an empty body, not a zero. Sending "0" is a
// well-formed request that fails to authenticate.
final String length = contentLength == 0 ? '' : '$contentLength';
final List<String> canonicalHeaders = headers.entries
.where((MapEntry<String, String> e) => e.key.startsWith('x-ms-'))
.map((MapEntry<String, String> e) =>
'${e.key.toLowerCase()}:${e.value.trim()}')
.toList()
..sort();
final List<String> canonicalQuery = queryParameters.entries
.map((MapEntry<String, String> e) =>
'${e.key.toLowerCase()}:${e.value}')
.toList()
..sort();
return <String>[
method.toUpperCase(),
header('content-encoding'),
header('content-language'),
length,
header('content-md5'),
header('content-type'),
// The Date header is empty because x-ms-date carries it, and sending both
// makes Azure sign one and read the other.
'',
header('if-modified-since'),
header('if-match'),
header('if-none-match'),
header('if-unmodified-since'),
header('range'),
...canonicalHeaders,
'/$account$resourcePath',
...canonicalQuery,
].join('\n');
}