diff --git a/src/shortenerTools/Domain/StorageTableHelper.cs b/src/shortenerTools/Domain/StorageTableHelper.cs index 96e9f73d..938f744b 100644 --- a/src/shortenerTools/Domain/StorageTableHelper.cs +++ b/src/shortenerTools/Domain/StorageTableHelper.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Table; @@ -85,6 +87,32 @@ public async Task> GetAllStatsByVanity(string vanity) return lstShortUrl; } + /// + /// Returns the ShortUrlEntity of the + /// + /// + /// ShortUrlEntity + public async Task GetShortUrlEntityByVanity(string vanity) + { + var tblUrls = GetUrlsTable(); + TableContinuationToken token = null; + ShortUrlEntity shortUrlEntity = null; + do + { + TableQuery query = new TableQuery().Where( + filter: TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, vanity)); + var queryResult = await tblUrls.ExecuteQuerySegmentedAsync(query, token); + shortUrlEntity = queryResult.Results.FirstOrDefault(); + } while (token != null); + + return shortUrlEntity; + } + + public async Task IfShortUrlEntityExistByVanity(string vanity) + { + ShortUrlEntity shortUrlEntity = await GetShortUrlEntityByVanity(vanity); + return (shortUrlEntity != null); + } public async Task IfShortUrlEntityExist(ShortUrlEntity row) { diff --git a/src/shortenerTools/Domain/Utility.cs b/src/shortenerTools/Domain/Utility.cs index 85fbd60b..8cce1bd6 100644 --- a/src/shortenerTools/Domain/Utility.cs +++ b/src/shortenerTools/Domain/Utility.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Security.Cryptography; using System.Threading.Tasks; using System.Security.Claims; using Microsoft.AspNetCore.Mvc; @@ -8,15 +9,21 @@ namespace Cloud5mins.domain { public static class Utility { - private const string Alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; - private static readonly int Base = Alphabet.Length; + //reshuffled for randomisation, same unique characters just jumbled up, you can replace with your own version + private const string ConversionCode = "FjTG0s5dgWkbLf_8etOZqMzNhmp7u6lUJoXIDiQB9-wRxCKyrPcv4En3Y21aASHV"; + private static readonly int Base = ConversionCode.Length; + //sets the length of the unique code to add to vanity + private const int MinVanityCodeLength = 5; public static async Task GetValidEndUrl(string vanity, StorageTableHelper stgHelper) { - if(string.IsNullOrEmpty(vanity)) + if (string.IsNullOrEmpty(vanity)) { var newKey = await stgHelper.GetNextTableId(); - string getCode() => Encode(newKey); + string getCode() => Encode(newKey); + if (await stgHelper.IfShortUrlEntityExistByVanity(getCode())) + return await GetValidEndUrl(vanity, stgHelper); + return string.Join(string.Empty, getCode()); } else @@ -28,19 +35,31 @@ public static async Task GetValidEndUrl(string vanity, StorageTableHelpe public static string Encode(int i) { if (i == 0) - return Alphabet[0].ToString(); - var s = string.Empty; - while (i > 0) - { - s += Alphabet[i % Base]; - i = i / Base; - } + return ConversionCode[0].ToString(); + + return GenerateUniqueRandomToken(i); + } - return string.Join(string.Empty, s.Reverse()); + public static string GetShortUrl(string host, string vanity) + { + return host + "/" + vanity; } - public static string GetShortUrl(string host, string vanity){ - return host + "/" + vanity; + // generates a unique, random, and alphanumeric token for the use as a url + //(not entirely secure but not sequential so generally not guessable) + public static string GenerateUniqueRandomToken(int uniqueId) + { + using (var generator = new RNGCryptoServiceProvider()) + { + //minimum size I would suggest is 5, longer the better but we want short URLs! + var bytes = new byte[MinVanityCodeLength]; + generator.GetBytes(bytes); + var chars = bytes + .Select(b => ConversionCode[b % ConversionCode.Length]); + var token = new string(chars.ToArray()); + var reversedToken = string.Join(string.Empty, token.Reverse()); + return uniqueId + reversedToken; + } } public static IActionResult CatchUnauthorize(ClaimsPrincipal principal, ILogger log) diff --git a/src/shortenerTools/Properties/PublishProfiles/patientcommsshortenertools2fxfh - Zip Deploy.pubxml b/src/shortenerTools/Properties/PublishProfiles/patientcommsshortenertools2fxfh - Zip Deploy.pubxml new file mode 100644 index 00000000..fc49338b --- /dev/null +++ b/src/shortenerTools/Properties/PublishProfiles/patientcommsshortenertools2fxfh - Zip Deploy.pubxml @@ -0,0 +1,18 @@ + + + + + ZipDeploy + AzureWebSite + Release + Any CPU + http://patientcommsshortenertools2fxfh.azurewebsites.net + False + /subscriptions/1b64105e-78ea-4dde-b2b0-4031b5611c06/resourceGroups/PatientCommsGroupv2/providers/Microsoft.Web/sites/patientcommsshortenertools2fxfh + $patientcommsshortenertools2fxfh + <_SavePWD>True + https://patientcommsshortenertools2fxfh.scm.azurewebsites.net/ + + \ No newline at end of file diff --git a/src/shortenerTools/Properties/ServiceDependencies/patientcommsshortenertools2fxfh - Zip Deploy/profile.arm.json b/src/shortenerTools/Properties/ServiceDependencies/patientcommsshortenertools2fxfh - Zip Deploy/profile.arm.json new file mode 100644 index 00000000..b2d16d2e --- /dev/null +++ b/src/shortenerTools/Properties/ServiceDependencies/patientcommsshortenertools2fxfh - Zip Deploy/profile.arm.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_dependencyType": "function.windows.consumption" + }, + "parameters": { + "resourceGroupName": { + "type": "string", + "defaultValue": "PatientCommsGroupv2", + "metadata": { + "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." + } + }, + "resourceGroupLocation": { + "type": "string", + "defaultValue": "uksouth", + "metadata": { + "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." + } + }, + "resourceName": { + "type": "string", + "defaultValue": "patientcommsshortenertools2fxfh", + "metadata": { + "description": "Name of the main resource to be created by this template." + } + }, + "resourceLocation": { + "type": "string", + "defaultValue": "[parameters('resourceGroupLocation')]", + "metadata": { + "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('resourceGroupLocation')]", + "apiVersion": "2019-10-01" + }, + { + "type": "Microsoft.Resources/deployments", + "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "apiVersion": "2019-10-01", + "dependsOn": [ + "[parameters('resourceGroupName')]" + ], + "properties": { + "mode": "Incremental", + "expressionEvaluationOptions": { + "scope": "inner" + }, + "parameters": { + "resourceGroupName": { + "value": "[parameters('resourceGroupName')]" + }, + "resourceGroupLocation": { + "value": "[parameters('resourceGroupLocation')]" + }, + "resourceName": { + "value": "[parameters('resourceName')]" + }, + "resourceLocation": { + "value": "[parameters('resourceLocation')]" + } + }, + "template": { + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceGroupName": { + "type": "string" + }, + "resourceGroupLocation": { + "type": "string" + }, + "resourceName": { + "type": "string" + }, + "resourceLocation": { + "type": "string" + } + }, + "variables": { + "storage_name": "[toLower(concat('storage', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId))))]", + "storage_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Storage/storageAccounts/', variables('storage_name'))]", + "function_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/sites/', parameters('resourceName'))]" + }, + "resources": [ + { + "location": "[parameters('resourceGroupLocation')]", + "name": "[variables('storage_name')]", + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2017-10-01", + "tags": { + "[concat('hidden-related:', concat('/providers/Microsoft.Web/sites/', parameters('resourceName')))]": "empty" + }, + "properties": { + "supportsHttpsTrafficOnly": true + }, + "sku": { + "name": "Standard_LRS" + }, + "kind": "Storage" + }, + { + "location": "[parameters('resourceLocation')]", + "name": "[parameters('resourceName')]", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "dependsOn": [ + "[variables('storage_ResourceId')]" + ], + "kind": "functionapp", + "properties": { + "name": "[parameters('resourceName')]", + "kind": "functionapp", + "httpsOnly": true, + "reserved": false + }, + "identity": { + "type": "SystemAssigned" + }, + "resources": [ + { + "name": "appsettings", + "type": "config", + "apiVersion": "2015-08-01", + "dependsOn": [ + "[variables('function_ResourceId')]" + ], + "properties": { + "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storage_name'), ';AccountKey=', listKeys(variables('storage_ResourceId'), '2017-10-01').keys[0].value, ';EndpointSuffix=', 'core.windows.net')]", + "WEBSITE_CONTENTSHARE": "[toLower(parameters('resourceName'))]", + "AzureWebJobsDashboard": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storage_name'), ';AccountKey=', listKeys(variables('storage_ResourceId'), '2017-10-01').keys[0].value, ';EndpointSuffix=', 'core.windows.net')]", + "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storage_name'), ';AccountKey=', listKeys(variables('storage_ResourceId'), '2017-10-01').keys[0].value, ';EndpointSuffix=', 'core.windows.net')]", + "FUNCTIONS_EXTENSION_VERSION": "~3", + "FUNCTIONS_WORKER_RUNTIME": "dotnet" + } + } + ] + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/src/shortenerTools/UrlRedirect/UrlRedirect.cs b/src/shortenerTools/UrlRedirect/UrlRedirect.cs index a945abbc..358d0572 100644 --- a/src/shortenerTools/UrlRedirect/UrlRedirect.cs +++ b/src/shortenerTools/UrlRedirect/UrlRedirect.cs @@ -27,7 +27,7 @@ public static async Task Run( { var config = new ConfigurationBuilder() .SetBasePath(context.FunctionAppDirectory) - .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true) + .AddJsonFile("settings.json", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build();