Powershell Invoke-WebRequest against Azure Storage Tables
To perform rest requests to azure storage tables you must sign your request, here is sample piece of code demonstrating how can it be done in Powershell:
$accountName = 'contoso'
$accountKey = '**************************************************************************************=='
$tableName = 'mytable'
$uri = "http://$accountName.table.core.windows.net/$tableName(PartitionKey='tasksSeattle',RowKey='1')"
$date = [datetime]::UtcNow.ToString('R', [System.Globalization.CultureInfo]::InvariantCulture)
$resource = [uri] $uri | select -ExpandProperty AbsolutePath # Path without query string
$stringToSign = $date + "`n/" + $accountName + $resource
$hasher = New-Object System.Security.Cryptography.HMACSHA256
$hasher.Key = [Convert]::FromBase64String($accountKey)
$signedSignature = [Convert]::ToBase64String($hasher.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign)))
$authorizationHeader = "SharedKeyLite " + $accountName + ":" + $signedSignature
$headers = @{
Authorization = $authorizationHeader
Date = $date
}
Write-Host 'Perform first request' -ForegroundColor Cyan
$response = Invoke-WebRequest -Uri $uri -Headers $headers
$response | select StatusCode, StatusDescription, @{n='ETag';e={ $_.Headers.ETag }}
$xml = [xml]$response.Content
$xml.entry.content.properties | fl
That code will return something like:
PartitionKey : tasksSeattle
RowKey : 1
Timestamp : Timestamp
Description : Take out the trash.
DueDate : DueDate
Location : Home
Make sure to provide correct values, otherwise you will likely get Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signa ture. error.
Azure tables and conditional headers
It seems that Azure tables do not support conditional headers like If-Modified-Since
like blobs do, but there is still one trick.
If you will make Invoke-WebRequest
instead of Invoke-RestMethod
you will also get response headers. And there will be ETag header - which is entity timestamp.
$response = Invoke-WebRequest -Uri $uri -Headers $headers
$response | select StatusCode, StatusDescription, @{n='ETag';e={ $_.Headers.ETag }} | fl
$xml = [xml]$response.Content
$xml.entry.content.properties | fl
Will return:
StatusCode : 200
StatusDescription : OK
ETag : W/"datetime'2015-12-11T10%3A53%3A52.6115577Z'"
PartitionKey : tasksSeattle
RowKey : 1
Timestamp : Timestamp
Description : Take out the trash.
DueDate : DueDate
Location : Home
And now you can perform requests like:
$headers = @{
Authorization = $authorizationHeader
Date = $date
'If-None-Match' = $response.Headers.ETag
}
Invoke-RestMethod -Uri $uri -Headers $headers
Which will give you desired (304) Not Modified.
Bootstraping example table from php
composer.json
{
"repositories": [
{
"type": "pear",
"url": "http://pear.php.net"
}
],
"require": {
"pear-pear.php.net/mail_mime": "*",
"pear-pear.php.net/http_request2": "*",
"pear-pear.php.net/mail_mimedecode": "*",
"microsoft/windowsazure": "*"
}
}
sandbox.php
<?php
require_once 'vendor\autoload.php';
use WindowsAzure\Common\ServiceException;
use WindowsAzure\Common\ServicesBuilder;
use WindowsAzure\Table\Internal\ITable;
use WindowsAzure\Table\Models\EdmType;
use WindowsAzure\Table\Models\Entity;
$accountName = "contoso";
$accountKey = "**************************************************************************************==";
$connectionString = "DefaultEndpointsProtocol=https;AccountName=$accountName;AccountKey=$accountKey";
/** @var ITable $tableRestProxy */
$tableRestProxy = ServicesBuilder::getInstance()->createTableService($connectionString);
try {
$createTableResult = $tableRestProxy->createTable("mytable");
} catch(ServiceException $ex) {
if($ex->getCode() !== 409) throw $ex;
}
$entity = new Entity();
$entity->setPartitionKey("tasksSeattle");
$entity->setRowKey("1");
$entity->addProperty("Description", null, "Take out the trash.");
$entity->addProperty("DueDate", EdmType::DATETIME, new DateTime("2012-11-05T08:15:00-08:00"));
$entity->addProperty("Location", EdmType::STRING, "Home");
try {
$tableRestProxy->insertEntity("mytable", $entity);
} catch(ServiceException $ex) {
if($ex->getCode() !== 409) throw $ex;
}
Some links
Specifying Conditional Headers for Blob Service Operations
Authenticating against Azure Table Storage
Authentication for the Azure Storage Services
How to use table storage from PHP
Update from 2024
Here is one more example of how race conditions can be utilized with help of ETag
<#
docker run -it --rm --name=azurite -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
#>
$accountName = 'devstoreaccount1'
$accountKey = 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=='
$tableName = 'mactemp'
# $endpoint = "https://$accountName.table.core.windows.net"
$endpoint = "http://127.0.0.1:10002/devstoreaccount1"
function repeated_signature_stuff_extracted_for_readability($uri) {
$date = [datetime]::UtcNow.ToString('R', [System.Globalization.CultureInfo]::InvariantCulture)
$resource = [uri] $uri | Select-Object -ExpandProperty AbsolutePath # Path without query string
$stringToSign = $date + "`n/" + $accountName + $resource
$hasher = New-Object System.Security.Cryptography.HMACSHA256
$hasher.Key = [Convert]::FromBase64String($accountKey)
$signedSignature = [Convert]::ToBase64String($hasher.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign)))
$authorizationHeader = "SharedKeyLite " + $accountName + ":" + $signedSignature
return @{
Authorization = $authorizationHeader
Date = $date
'x-ms-version' = '2019-02-02'
'Content-Type' = 'application/json'
'Accept' = 'application/json;odata=nometadata'
}
}
# each example below, has repeating signature block, it is the same each time
# CREATE TABLE
$uri = "$endpoint/Tables"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
Invoke-WebRequest -Method Post -Uri $uri -Headers $headers -Body (ConvertTo-Json -InputObject @{ TableName = $tableName })
# INSERT RECORD
$uri = "$endpoint/$tableName"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
Invoke-WebRequest -Method Post -Uri $uri -Headers $headers -Body (ConvertTo-Json -InputObject @{
PartitionKey = "pk1"
RowKey = "rk1"
Foo = "bar"
Acme = 42
})
# RETRIEVE RECORD
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
$response = Invoke-WebRequest -Method Get -Uri $uri -Headers $headers
# $response.Headers # Etag, Date
$response.Content | ConvertFrom-Json | Out-Host
# UPDATE RECORD
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
Invoke-WebRequest -Method Merge -Uri $uri -Headers $headers -Body (ConvertTo-Json -InputObject @{
PartitionKey = "pk1"
RowKey = "rk1"
Foo = "BAZ"
Acme = 100500
})
# RETRIEVE RECORD - check if it was updated
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
$response = Invoke-WebRequest -Method Get -Uri $uri -Headers $headers
$response.Content | ConvertFrom-Json | Out-Host
# --------------------------------------------
# DEMO RACE CONDITIONS
# lets pretend we have two clients, each trying to update the same record at the same time
# we are going to avoid race conditions by utilizing ETag and If-Match headers
# both clients will first read the record, then update it
# pseudo code will be:
# 1. client1 get record - ok
# 2. client2 get record - ok
# 3. client2 update record - ok
# 4. client1 update record - should fail - race condition
# 1. client1 get record - ok
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
$response1 = Invoke-WebRequest -Method Get -Uri $uri -Headers $headers
$record1 = $response1.Content | ConvertFrom-Json
$record1 | Out-Host
$response1.Headers.ETag | Out-Host
# 2. client2 get record - ok
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
$response2 = Invoke-WebRequest -Method Get -Uri $uri -Headers $headers
$record2 = $response2.Content | ConvertFrom-Json
$record2 | Out-Host
$response2.Headers.ETag | Out-Host
# 3. client2 update record - ok
$record2.Foo = "UPDATED BY CLIENT 2"
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
$headers['If-Match'] = [string]$response2.Headers.ETag
Invoke-WebRequest -Method Merge -Uri $uri -Headers $headers -Body (ConvertTo-Json -InputObject $record2)
# 4. client1 update record - should fail - race condition
$record1.Foo = "UPDATED BY CLIENT 1"
$uri = "$endpoint/$tableName(PartitionKey='pk1',RowKey='rk1')"
$headers = repeated_signature_stuff_extracted_for_readability($uri)
$headers['If-Match'] = [string]$response1.Headers.ETag
Invoke-WebRequest -Method Merge -Uri $uri -Headers $headers -Body (ConvertTo-Json -InputObject $record2)
# will fail with 412 Precondition Failed error saing that UpdateConditionNotSatisfied