From 8218ab468d66d79b15643a965e5c313951674944 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 8 Sep 2017 12:38:09 -0400 Subject: [PATCH] azure functions project --- bitwarden-core.sln | 9 +- util/Function/BlockIp.cs | 70 ++++++++++ util/Function/Function.csproj | 23 +++ util/Function/KeepAlive.cs | 17 +++ util/Function/Models/AccessRuleResponse.cs | 8 ++ .../Models/AccessRuleResultResponse.cs | 15 ++ util/Function/Models/ListResult.cs | 10 ++ util/Function/NewHelpdeskTicket.cs | 132 ++++++++++++++++++ util/Function/UnblockIp.cs | 88 ++++++++++++ util/Function/host.json | 2 + util/Function/local.settings.json | 7 + 11 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 util/Function/BlockIp.cs create mode 100644 util/Function/Function.csproj create mode 100644 util/Function/KeepAlive.cs create mode 100644 util/Function/Models/AccessRuleResponse.cs create mode 100644 util/Function/Models/AccessRuleResultResponse.cs create mode 100644 util/Function/Models/ListResult.cs create mode 100644 util/Function/NewHelpdeskTicket.cs create mode 100644 util/Function/UnblockIp.cs create mode 100644 util/Function/host.json create mode 100644 util/Function/local.settings.json diff --git a/bitwarden-core.sln b/bitwarden-core.sln index ffceb40e38..1d79415e79 100644 --- a/bitwarden-core.sln +++ b/bitwarden-core.sln @@ -41,7 +41,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jobs", "src\Jobs\Jobs.cspro EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper", "..\Dapper\Dapper\Dapper.csproj", "{6951E73D-1761-41F6-B5D3-BEF4C2F73EA3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BillingUpdater", "util\BillingUpdater\BillingUpdater.csproj", "{A0FBA4DF-2F24-45A6-B188-EBDBD2FAF445}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BillingUpdater", "util\BillingUpdater\BillingUpdater.csproj", "{A0FBA4DF-2F24-45A6-B188-EBDBD2FAF445}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Function", "util\Function\Function.csproj", "{A6C44A84-8E51-4C64-B9C4-7B7C23253345}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -95,6 +97,10 @@ Global {A0FBA4DF-2F24-45A6-B188-EBDBD2FAF445}.Debug|Any CPU.Build.0 = Debug|Any CPU {A0FBA4DF-2F24-45A6-B188-EBDBD2FAF445}.Release|Any CPU.ActiveCfg = Release|Any CPU {A0FBA4DF-2F24-45A6-B188-EBDBD2FAF445}.Release|Any CPU.Build.0 = Release|Any CPU + {A6C44A84-8E51-4C64-B9C4-7B7C23253345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6C44A84-8E51-4C64-B9C4-7B7C23253345}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6C44A84-8E51-4C64-B9C4-7B7C23253345}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6C44A84-8E51-4C64-B9C4-7B7C23253345}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -111,6 +117,7 @@ Global {7DCEBD8F-E5F3-4A3C-BD35-B64341590B74} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D} {6951E73D-1761-41F6-B5D3-BEF4C2F73EA3} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D} {A0FBA4DF-2F24-45A6-B188-EBDBD2FAF445} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} + {A6C44A84-8E51-4C64-B9C4-7B7C23253345} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/util/Function/BlockIp.cs b/util/Function/BlockIp.cs new file mode 100644 index 0000000000..83a5abc753 --- /dev/null +++ b/util/Function/BlockIp.cs @@ -0,0 +1,70 @@ +using System; +using System.Configuration; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Bit.Function.Models; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Host; +using Newtonsoft.Json; + +namespace Bit.Function +{ + public static class BlockIp + { + [FunctionName("BlockIp")] + public static void Run( + [QueueTrigger("blockip", Connection = "")]string myQueueItem, + out string outputQueueItem, + TraceWriter log) + { + outputQueueItem = BlockIpAsync(myQueueItem).GetAwaiter().GetResult(); + log.Info($"C# Queue trigger function processed: {myQueueItem}, outputted: {outputQueueItem}"); + } + + private static async Task BlockIpAsync(string ipAddress) + { + var ipWhitelist = ConfigurationManager.AppSettings["WhitelistedIps"]; + if(ipWhitelist != null && ipWhitelist.Split(',').Contains(ipAddress)) + { + return null; + } + + var xAuthEmail = ConfigurationManager.AppSettings["X-Auth-Email"]; + var xAuthKey = ConfigurationManager.AppSettings["X-Auth-Key"]; + var zoneId = ConfigurationManager.AppSettings["ZoneId"]; + + using(var client = new HttpClient()) + { + client.BaseAddress = new Uri("https://api.cloudflare.com"); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Add("X-Auth-Email", xAuthEmail); + client.DefaultRequestHeaders.Add("X-Auth-Key", xAuthKey); + + var response = await client.PostAsJsonAsync( + $"/client/v4/zones/{zoneId}/firewall/access_rules/rules", + new + { + mode = "block", + configuration = new + { + target = "ip", + value = ipAddress + }, + notes = $"Rate limit abuse on {DateTime.UtcNow.ToString()}." + }); + + var responseString = await response.Content.ReadAsStringAsync(); + var responseJson = JsonConvert.DeserializeObject(responseString); + + if(!responseJson.Success) + { + return null; + } + + // Uncomment whenever we can delay the returned message. Functions do not support that at this time. + return null; //responseJson.Result?.Id; + } + } + } +} diff --git a/util/Function/Function.csproj b/util/Function/Function.csproj new file mode 100644 index 0000000000..9e1658460b --- /dev/null +++ b/util/Function/Function.csproj @@ -0,0 +1,23 @@ + + + net461 + Bit.Function + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + diff --git a/util/Function/KeepAlive.cs b/util/Function/KeepAlive.cs new file mode 100644 index 0000000000..a21a31c17d --- /dev/null +++ b/util/Function/KeepAlive.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Host; + +namespace Bit.Function +{ + public static class KeepAlive + { + [FunctionName("KeepAlive")] + public static void Run( + [TimerTrigger("0 */15 * * * *")]TimerInfo myTimer, + TraceWriter log) + { + log.Info($"C# Timer trigger function executed at: {DateTime.Now}"); + } + } +} diff --git a/util/Function/Models/AccessRuleResponse.cs b/util/Function/Models/AccessRuleResponse.cs new file mode 100644 index 0000000000..7466c391e5 --- /dev/null +++ b/util/Function/Models/AccessRuleResponse.cs @@ -0,0 +1,8 @@ +namespace Bit.Function.Models +{ + public class AccessRuleResponse + { + public bool Success { get; set; } + public AccessRuleResultResponse Result { get; set; } + } +} diff --git a/util/Function/Models/AccessRuleResultResponse.cs b/util/Function/Models/AccessRuleResultResponse.cs new file mode 100644 index 0000000000..8a568ba948 --- /dev/null +++ b/util/Function/Models/AccessRuleResultResponse.cs @@ -0,0 +1,15 @@ +namespace Bit.Function.Models +{ + public class AccessRuleResultResponse + { + public string Id { get; set; } + public string Notes { get; set; } + public ConfigurationResponse Configuration { get; set; } + + public class ConfigurationResponse + { + public string Target { get; set; } + public string Value { get; set; } + } + } +} diff --git a/util/Function/Models/ListResult.cs b/util/Function/Models/ListResult.cs new file mode 100644 index 0000000000..6be4cd892b --- /dev/null +++ b/util/Function/Models/ListResult.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Bit.Function.Models +{ + public class ListResult + { + public bool Success { get; set; } + public List Result { get; set; } + } +} diff --git a/util/Function/NewHelpdeskTicket.cs b/util/Function/NewHelpdeskTicket.cs new file mode 100644 index 0000000000..8934f24086 --- /dev/null +++ b/util/Function/NewHelpdeskTicket.cs @@ -0,0 +1,132 @@ +using System; +using System.Configuration; +using System.Net; +using System.Net.Http; +using System.Net.Mail; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Host; + +namespace Bit.Function +{ + public static class NewHelpdeskTicket + { + [FunctionName("NewHelpdeskTicket")] + public static HttpResponseMessage Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "api/newhelpdeskticket")]HttpRequestMessage req, + TraceWriter log) + { + //ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | + // SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; + + var data = req.Content.ReadAsFormDataAsync().Result; + if(data == null) + { + return req.CreateResponse(HttpStatusCode.BadRequest, "No data provided."); + } + + if(string.IsNullOrWhiteSpace(data["name"])) + { + return req.CreateResponse(HttpStatusCode.BadRequest, "Name is required."); + } + + if(data["name"].Length > 50) + { + return req.CreateResponse(HttpStatusCode.BadRequest, "Name must be less than 50 characters."); + } + + if(string.IsNullOrWhiteSpace(data["email"])) + { + return req.CreateResponse(HttpStatusCode.BadRequest, "Email is required."); + } + + if(data["email"].Length > 50) + { + return req.CreateResponse(HttpStatusCode.BadRequest, "Email must be less than 50 characters."); + } + + if(!data["email"].Contains("@") || !data["email"].Contains(".")) + { + return req.CreateResponse(HttpStatusCode.BadRequest, "Email is not valid."); + } + + if(string.IsNullOrWhiteSpace(data["message"])) + { + return req.CreateResponse(HttpStatusCode.BadRequest, "Message is required."); + } + + //if(!await SubmitApiAsync(data["name"], data["email"], data["message"], log)) + //{ + // return req.CreateResponse(HttpStatusCode.BadRequest, "Ticket failed to create."); + //} + + SubmitEmail(data["name"], data["email"], data["message"], log); + + return req.CreateResponse(HttpStatusCode.OK, "Ticket created."); + } + + private async static Task SubmitApiAsync(string name, string email, string message, TraceWriter log) + { + using(var client = new HttpClient()) + { + client.BaseAddress = new Uri("https://bitwarden.freshdesk.com/api/v2"); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Add("Authorization", MakeFreshdeskApiAuthHeader(log)); + + var response = await client.PostAsJsonAsync("tickets", + new + { + name = name, + email = email, + status = 2, + priority = 2, + source = 1, + subject = "bitwarden.com Website Contact", + description = FormatMessage(message) + }); + + return response.IsSuccessStatusCode; + } + } + + private static void SubmitEmail(string name, string email, string message, TraceWriter log) + { + var sendgridApiKey = ConfigurationManager.AppSettings["SendgridApiKey"]; + var client = new SmtpClient("smtp.sendgrid.net", /*465*/ 587) + { + //EnableSsl = true, + Credentials = new NetworkCredential("apikey", sendgridApiKey) + }; + + var fromAddress = new MailAddress(email, name, Encoding.UTF8); + var mailMessage = new MailMessage(fromAddress, new MailAddress("bitwardencomsupport@bitwarden.freshdesk.com")) + { + Subject = "bitwarden.com Website Contact", + Body = FormatMessage(message), + IsBodyHtml = true + }; + + client.SendCompleted += (s, e) => + { + client.Dispose(); + mailMessage.Dispose(); + }; + client.SendAsync(mailMessage, null); + } + + private static string FormatMessage(string message) + { + return message.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "
"); + } + + private static string MakeFreshdeskApiAuthHeader(TraceWriter log) + { + var freshdeskApiKey = ConfigurationManager.AppSettings["FreshdeskApiKey"]; + var b64Creds = Convert.ToBase64String( + Encoding.GetEncoding("ISO-8859-1").GetBytes(freshdeskApiKey + ":X")); + return b64Creds; + } + } +} diff --git a/util/Function/UnblockIp.cs b/util/Function/UnblockIp.cs new file mode 100644 index 0000000000..2de82de337 --- /dev/null +++ b/util/Function/UnblockIp.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Net.Http; +using System.Threading.Tasks; +using Bit.Function.Models; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Host; +using Newtonsoft.Json; + +namespace Bit.Function +{ + public static class UnblockIp + { + [FunctionName("UnblockIp")] + public static void Run( + [QueueTrigger("unblockip", Connection = "")]string myQueueItem, + TraceWriter log) + { + log.Info($"C# Queue trigger function processed: {myQueueItem}"); + UnblockIpAsync(myQueueItem, log).Wait(); + } + + private static async Task UnblockIpAsync(string id, TraceWriter log) + { + if(id == null) + { + return; + } + + var zoneId = ConfigurationManager.AppSettings["ZoneId"]; + var xAuthEmail = ConfigurationManager.AppSettings["X-Auth-Email"]; + var xAuthKey = ConfigurationManager.AppSettings["X-Auth-Key"]; + + if(id.Contains(".") || id.Contains(":")) + { + // IP address messages. + using(var client = new HttpClient()) + { + client.BaseAddress = new Uri("https://api.cloudflare.com"); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Add("X-Auth-Email", xAuthEmail); + client.DefaultRequestHeaders.Add("X-Auth-Key", xAuthKey); + + var response = await client.GetAsync($"/client/v4/zones/{zoneId}/firewall/access_rules/rules?" + + $"configuration_target=ip&configuration_value={id}"); + + var responseString = await response.Content.ReadAsStringAsync(); + var responseJson = JsonConvert.DeserializeObject(responseString); + + if(!responseJson.Success) + { + return; + } + + foreach(var rule in responseJson.Result) + { + if(rule.Configuration?.Value != id) + { + continue; + } + + log.Info($"Unblock IP {id}, {rule.Id}"); + await DeleteRuleAsync(zoneId, xAuthEmail, xAuthKey, rule.Id); + } + } + } + else + { + log.Info($"Unblock Id {id}"); + await DeleteRuleAsync(zoneId, xAuthEmail, xAuthKey, id); + } + } + + private static async Task DeleteRuleAsync(string zoneId, string xAuthEmail, string xAuthKey, string id) + { + var path = $"/client/v4/zones/{zoneId}/firewall/access_rules/rules/{id}"; + using(var client = new HttpClient()) + { + client.BaseAddress = new Uri("https://api.cloudflare.com"); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Add("X-Auth-Email", xAuthEmail); + client.DefaultRequestHeaders.Add("X-Auth-Key", xAuthKey); + await client.DeleteAsync(path); + } + } + } +} diff --git a/util/Function/host.json b/util/Function/host.json new file mode 100644 index 0000000000..7a73a41bfd --- /dev/null +++ b/util/Function/host.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/util/Function/local.settings.json b/util/Function/local.settings.json new file mode 100644 index 0000000000..8f901f1169 --- /dev/null +++ b/util/Function/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "", + "AzureWebJobsDashboard": "" + } +} \ No newline at end of file