-
-
Notifications
You must be signed in to change notification settings - Fork 242
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Added NugetPackage for KeyVault keys. * Added support for TransIp DNS provider. * Fixed whitespace issues. Love the dotnet format tool. * Apply suggestions from code review Co-authored-by: Tatsuro Shibamura <[email protected]> * Fixed PR remarks (#179): Naming conventions (Async/CamelCase), TokenCredential in constructor of provider, scopes for single line for-each. * Update code formatting and remove unused using * Update README Co-authored-by: Tatsuro Shibamura <[email protected]>
- Loading branch information
1 parent
171befa
commit 65fbcca
Showing
6 changed files
with
374 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace KeyVault.Acmebot.Options | ||
{ | ||
public class TransIpOptions | ||
{ | ||
public string CustomerName { get; set; } | ||
|
||
public string PrivateKeyName { get; set; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,356 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Net.Http; | ||
using System.Net.Http.Headers; | ||
using System.Security.Cryptography; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
using Azure.Identity; | ||
using Azure.Security.KeyVault.Keys.Cryptography; | ||
|
||
using KeyVault.Acmebot.Internal; | ||
using KeyVault.Acmebot.Options; | ||
|
||
using Newtonsoft.Json; | ||
using Newtonsoft.Json.Linq; | ||
|
||
namespace KeyVault.Acmebot.Providers | ||
{ | ||
public class TransIpProvider : IDnsProvider | ||
{ | ||
public TransIpProvider(AcmebotOptions acmeOptions, TransIpOptions options, IAzureEnvironment environment) | ||
{ | ||
var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions | ||
{ | ||
AuthorityHost = new Uri(environment.ActiveDirectory) | ||
}); | ||
|
||
var keyUri = new Uri(new Uri(acmeOptions.VaultBaseUrl), $"/keys/{options.PrivateKeyName}"); | ||
var cryptoClient = new CryptographyClient(keyUri, credential); | ||
|
||
_transIpClient = new TransIpClient(options.CustomerName, cryptoClient); | ||
} | ||
|
||
private readonly TransIpClient _transIpClient; | ||
|
||
public int PropagationSeconds => 360; | ||
|
||
public async Task CreateTxtRecordAsync(DnsZone zone, string relativeRecordName, IEnumerable<string> values) | ||
{ | ||
var records = values.Select(value => new DnsEntry | ||
{ | ||
Name = relativeRecordName, | ||
Type = "TXT", | ||
Expire = 60, | ||
Content = value | ||
}); | ||
|
||
foreach (var record in records) | ||
{ | ||
await _transIpClient.AddRecordAsync(zone.Name, record); | ||
} | ||
} | ||
|
||
public async Task DeleteTxtRecordAsync(DnsZone zone, string relativeRecordName) | ||
{ | ||
var records = await _transIpClient.ListRecordsAsync(zone.Name); | ||
|
||
var recordsToDelete = records.Where(r => r.Name == relativeRecordName && r.Type == "TXT"); | ||
|
||
foreach (var record in recordsToDelete) | ||
{ | ||
await _transIpClient.DeleteRecordAsync(zone.Name, record); | ||
} | ||
} | ||
|
||
public async Task<IReadOnlyList<DnsZone>> ListZonesAsync() | ||
{ | ||
var zones = await _transIpClient.ListZonesAsync(); | ||
|
||
return zones.Select(d => new DnsZone { Id = d.Name, Name = d.Name }).ToArray(); | ||
} | ||
|
||
private class TransIpClient | ||
{ | ||
public TransIpClient(string customerName, CryptographyClient cryptoClient) | ||
{ | ||
_customerName = customerName; | ||
_cryptoClient = cryptoClient; | ||
|
||
_httpClient = new HttpClient | ||
{ | ||
BaseAddress = new Uri("https://api.transip.nl/v6/") | ||
}; | ||
|
||
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); | ||
} | ||
|
||
private readonly HttpClient _httpClient; | ||
private readonly string _customerName; | ||
private readonly CryptographyClient _cryptoClient; | ||
|
||
private TransIpToken _token; | ||
|
||
public async Task<IReadOnlyList<Domain>> ListZonesAsync() | ||
{ | ||
await EnsureLoggedInAsync(); | ||
|
||
var response = await _httpClient.GetAsync("domains"); | ||
|
||
response.EnsureSuccessStatusCode(); | ||
|
||
var domains = await response.Content.ReadAsAsync<ListDomainsResult>(); | ||
|
||
return domains.Domains; | ||
} | ||
|
||
public async Task<IReadOnlyList<DnsEntry>> ListRecordsAsync(string zoneName) | ||
{ | ||
await EnsureLoggedInAsync(); | ||
|
||
var response = await _httpClient.GetAsync($"domains/{zoneName}/dns"); | ||
|
||
response.EnsureSuccessStatusCode(); | ||
|
||
var entries = await response.Content.ReadAsAsync<ListDnsEntriesResponse>(); | ||
|
||
return entries.DnsEntries; | ||
} | ||
|
||
public async Task DeleteRecordAsync(string zoneName, DnsEntry entry) | ||
{ | ||
await EnsureLoggedInAsync(); | ||
|
||
var request = new DnsEntryRequest | ||
{ | ||
DnsEntry = entry | ||
}; | ||
|
||
var response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Delete, $"domains/{zoneName}/dns") | ||
{ | ||
Content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json") | ||
}); | ||
|
||
response.EnsureSuccessStatusCode(); | ||
} | ||
|
||
public async Task AddRecordAsync(string zoneName, DnsEntry entry) | ||
{ | ||
await EnsureLoggedInAsync(); | ||
|
||
var request = new DnsEntryRequest | ||
{ | ||
DnsEntry = entry | ||
}; | ||
|
||
var response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"domains/{zoneName}/dns") | ||
{ | ||
Content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json") | ||
}); | ||
|
||
response.EnsureSuccessStatusCode(); | ||
} | ||
|
||
private async Task EnsureLoggedInAsync() | ||
{ | ||
if (_token?.IsValid() == true) | ||
{ | ||
return; | ||
} | ||
|
||
if (_token is null) | ||
{ | ||
_token = LoadToken(); | ||
|
||
if (_token?.IsValid() == true && _customerName.Equals(_token.CustomerName)) | ||
{ | ||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.Token); | ||
|
||
var testResponse = await _httpClient.GetAsync("api-test"); | ||
|
||
if (testResponse.IsSuccessStatusCode) | ||
{ | ||
return; | ||
} | ||
} | ||
|
||
} | ||
|
||
await CreateNewTokenAsync(); | ||
} | ||
|
||
private async Task CreateNewTokenAsync() | ||
{ | ||
var nonce = new byte[16]; | ||
|
||
RandomNumberGenerator.Fill(nonce); | ||
|
||
var request = new TokenRequest | ||
{ | ||
Login = _customerName, | ||
Nonce = Convert.ToBase64String(nonce) | ||
}; | ||
|
||
(string signature, string body) = await SignRequestAsync(request); | ||
|
||
var response = await new HttpClient().SendAsync( | ||
new HttpRequestMessage(HttpMethod.Post, new Uri(_httpClient.BaseAddress, "auth")) | ||
{ | ||
Headers = { { "Signature", signature } }, | ||
Content = new StringContent(body, Encoding.UTF8, "application/json") | ||
}); | ||
|
||
response.EnsureSuccessStatusCode(); | ||
|
||
var tokenResponse = await response.Content.ReadAsAsync<TokenResponse>(); | ||
|
||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.Token); | ||
|
||
_token = new TransIpToken | ||
{ | ||
CustomerName = _customerName, | ||
Token = tokenResponse.Token, | ||
Expires = DateTimeOffset.FromUnixTimeSeconds(tokenResponse.GetTokenExpiration()) | ||
}; | ||
|
||
StoreToken(_token); | ||
} | ||
|
||
private async Task<(string token, string body)> SignRequestAsync(object request) | ||
{ | ||
var body = JsonConvert.SerializeObject(request); | ||
|
||
using var hasher = SHA512.Create(); | ||
var bytes = hasher.ComputeHash(Encoding.UTF8.GetBytes(body)); | ||
|
||
var signature = await _cryptoClient.SignAsync(SignatureAlgorithm.RS512, bytes); | ||
|
||
return (Convert.ToBase64String(signature.Signature), body); | ||
} | ||
|
||
private void StoreToken(TransIpToken token) | ||
{ | ||
var fullPath = Environment.ExpandEnvironmentVariables(@"%HOME%\.acme\transip_token.json"); | ||
var directoryPath = Path.GetDirectoryName(fullPath); | ||
|
||
if (!Directory.Exists(directoryPath)) | ||
{ | ||
Directory.CreateDirectory(directoryPath); | ||
} | ||
|
||
var json = JsonConvert.SerializeObject(token, Formatting.Indented); | ||
|
||
File.WriteAllText(fullPath, json); | ||
} | ||
|
||
private TransIpToken LoadToken() | ||
{ | ||
var fullPath = Environment.ExpandEnvironmentVariables(@"%HOME%\.acme\transip_token.json"); | ||
|
||
if (!File.Exists(fullPath)) | ||
{ | ||
return null; | ||
} | ||
|
||
var json = File.ReadAllText(fullPath); | ||
|
||
return JsonConvert.DeserializeObject<TransIpToken>(json); | ||
} | ||
} | ||
|
||
private class TransIpToken | ||
{ | ||
public string CustomerName { get; set; } | ||
|
||
public string Token { get; set; } | ||
|
||
public DateTimeOffset Expires { get; set; } | ||
|
||
public bool IsValid() | ||
{ | ||
return !string.IsNullOrEmpty(Token) && Expires - DateTimeOffset.Now > TimeSpan.FromMinutes(1); | ||
} | ||
} | ||
|
||
private class TokenResponse | ||
{ | ||
[JsonProperty("token")] | ||
public string Token { get; set; } | ||
|
||
public long GetTokenExpiration() | ||
{ | ||
var token = Token.Split('.')[1]; | ||
token = token.PadRight(token.Length + (4 - token.Length % 4) % 4, '='); | ||
|
||
var tokenBytes = Convert.FromBase64String(token); | ||
|
||
var tokenObject = JObject.Parse(Encoding.UTF8.GetString(tokenBytes)); | ||
|
||
return tokenObject.Value<long>("exp"); | ||
} | ||
} | ||
|
||
private class TokenRequest | ||
{ | ||
[JsonProperty("login")] | ||
public string Login { get; set; } | ||
|
||
[JsonProperty("nonce")] | ||
public string Nonce { get; set; } | ||
|
||
[JsonProperty("read_only")] | ||
public bool ReadOnly { get; set; } | ||
|
||
[JsonProperty("expiration_time")] | ||
public string ExpirationTime { get; set; } = "4 weeks"; | ||
|
||
[JsonProperty("label")] | ||
public string Label { get; set; } = "KeyVault.Acmebot." + DateTime.UtcNow; | ||
|
||
[JsonProperty("global_key")] | ||
public bool GlobalKey { get; set; } = true; | ||
} | ||
|
||
private class ListDomainsResult | ||
{ | ||
[JsonProperty("domains")] | ||
public IReadOnlyList<Domain> Domains { get; set; } | ||
} | ||
|
||
private class Domain | ||
{ | ||
[JsonProperty("name")] | ||
public string Name { get; set; } | ||
} | ||
|
||
private class ListDnsEntriesResponse | ||
{ | ||
[JsonProperty("dnsEntries")] | ||
public IReadOnlyList<DnsEntry> DnsEntries { get; set; } | ||
} | ||
|
||
private class DnsEntryRequest | ||
{ | ||
[JsonProperty("dnsEntry")] | ||
public DnsEntry DnsEntry { get; set; } | ||
} | ||
|
||
private class DnsEntry | ||
{ | ||
[JsonProperty("name")] | ||
public string Name { get; set; } | ||
|
||
[JsonProperty("expire")] | ||
public int Expire { get; set; } | ||
|
||
[JsonProperty("type")] | ||
public string Type { get; set; } | ||
|
||
[JsonProperty("content")] | ||
public string Content { get; set; } | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.