-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDownloadFunctionAppContents.cs
More file actions
239 lines (207 loc) · 13.4 KB
/
DownloadFunctionAppContents.cs
File metadata and controls
239 lines (207 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
namespace Downloader
{
using System;
using System.IO;
using System.IO.Compression;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json.Nodes;
using Azure.Core;
using Azure.Identity; // <PackageReference Include="Azure.Identity" Version="1.9.0" />
public record PublishingCredentialProperties(string PublishingUserName, string PublishingPassword, string ScmUri);
public record PublishingCredential(string Id, string Name, PublishingCredentialProperties Properties);
public record VirtualFileSystemEntry(string Name, long Size, DateTimeOffset Mtime, DateTimeOffset Crtime, string Mime, string Href, string Path);
internal record SiteInfo(string TenantID, string SubscriptionID, string ResourceGroupName, string SiteName, string SlotName);
internal enum FollowPolicy { IgnoreShortcuts = 0, FollowShortcuts = 1 }
internal enum SCMAuthenticationMechanism { UseSCMApplicationScope = 0, UseAccessTokenBasic, UseAccessTokenBearer}
internal enum DownloadMethod { UseVFS = 0, UseZIP, UseFunctionZIP }
public static class DownloadFunctionAppContents
{
public static async Task Main()
{
var (siteName, authMechanism, zipMechanism) = ("downloadcontentdemo", SCMAuthenticationMechanism.UseAccessTokenBearer, DownloadMethod.UseZIP);
SiteInfo ISVSite(string resourceGroupName, string siteName) => new(TenantID: "geuer-pollmann.de", SubscriptionID: "706df49f-998b-40ec-aed3-7f0ce9c67759", ResourceGroupName: resourceGroupName, SiteName: siteName, SlotName: null);
SiteInfo CustomerSite(string resourceGroupName, string siteName) => new(TenantID: "chgeuerfte.aad.geuer-pollmann.de", SubscriptionID: "724467b5-bee4-484b-bf13-d6a5505d2b51", ResourceGroupName: resourceGroupName, SiteName: siteName, SlotName: null);
List<SiteInfo> siteInfos = new()
{
ISVSite("meteredbilling-infra-20230112", "spqpzpz3chwpnb6"),
CustomerSite("downloadfunctionappcontent", "downloadcontentdemo"),
};
SiteInfo siteInfo = siteInfos.Single(si => si.SiteName == siteName);
var (clientId, clientSecretFile) = ("7b8e9825-af72-4c2a-a2df-94929355b3b8", @"C:\Users\chgeuer\.secrets\principal-for-unencrypted-function-scanning.txt");
// https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet
// DefaultAzureCredential cred = new();
// AzureCliCredential cred = new();
ClientSecretCredential cred = new(
tenantId: siteInfo.TenantID,
clientId: clientId,
clientSecret: await File.ReadAllTextAsync(path: clientSecretFile));
AccessToken accessToken = await cred.GetTokenAsync(new(scopes: new[] { "https://management.azure.com/.default" }));
HttpClient armHttpClient = accessToken.CreateARMHttpClient();
bool needToDisableSCMBasicAuthAgain = false;
if (authMechanism == SCMAuthenticationMechanism.UseSCMApplicationScope)
{
bool scmBasicAuthAllowed = await armHttpClient.GetSCMBasicAuthAllowed(siteInfo);
if (!scmBasicAuthAllowed)
{
await armHttpClient.SetSCMSetBasicAuth(siteInfo, allow: true);
// Wait until the updated permission propagated to the SCM site.
await Task.Delay(TimeSpan.FromSeconds(2));
needToDisableSCMBasicAuthAgain = true;
}
}
try
{
HttpClient scmHttpClient = authMechanism switch
{
SCMAuthenticationMechanism.UseSCMApplicationScope => (await armHttpClient.FetchSCMCredential(siteInfo)).CreateSCMHttpClient(),
SCMAuthenticationMechanism.UseAccessTokenBasic => accessToken.CreateSCMBasicHttpClient(),
SCMAuthenticationMechanism.UseAccessTokenBearer => accessToken.CreateSCMBearerHttpClient(),
_ => throw new NotSupportedException(),
};
await (zipMechanism switch
{
DownloadMethod.UseVFS => scmHttpClient.DownloadZipUsingVFS(siteInfo, new FileInfo($"{siteInfo.SiteName}-vfs.zip").FullName),
DownloadMethod.UseZIP => scmHttpClient.DownloadZipUsingZipAPI(siteInfo, new FileInfo($"{siteInfo.SiteName}-zip.zip").FullName, directory: ""),
DownloadMethod.UseFunctionZIP => scmHttpClient.DownloadZipUsingFunctionZipAPI(siteInfo, new FileInfo($"{siteInfo.SiteName}-funczip.zip").FullName, includeCsproj: true, includeAppSettings: true),
_ => throw new NotSupportedException(),
});
}
finally
{
if (needToDisableSCMBasicAuthAgain)
{
await armHttpClient.SetSCMSetBasicAuth(siteInfo, allow: false);
}
}
}
static async Task DownloadZipUsingVFS(this HttpClient scmHttpClient, SiteInfo siteInfo, string zipFilename)
{
string vfsEndpoint = $"https://{siteInfo.SiteName}.scm.azurewebsites.net/api/vfs/";
using Stream outputStream = File.OpenWrite(zipFilename);
using ZipArchive zipArchive = new(outputStream, ZipArchiveMode.Create);
await scmHttpClient.RecurseAsync(
requestUri: vfsEndpoint,
policy: FollowPolicy.IgnoreShortcuts,
task: zipArchive.CreateEntry);
}
static async Task DownloadZipUsingZipAPI(this HttpClient scmHttpClient, SiteInfo siteInfo, string zipFilename, string directory = "")
{
var url = string.IsNullOrEmpty(directory)
? $"https://{siteInfo.SiteName}.scm.azurewebsites.net/api/zip/"
: $"https://{siteInfo.SiteName}.scm.azurewebsites.net/api/zip/{directory.Trim('/')}/"; // Must end with /
using var zipStream = await scmHttpClient.GetStreamAsync(url);
using var fileStream = File.OpenWrite(zipFilename);
await zipStream.CopyToAsync(fileStream);
}
static async Task DownloadZipUsingFunctionZipAPI(this HttpClient scmHttpClient, SiteInfo siteInfo, string zipFilename, bool includeCsproj = false, bool includeAppSettings = false)
{
using var zipStream = await scmHttpClient.GetStreamAsync($"https://{siteInfo.SiteName}.scm.azurewebsites.net/api/functions/admin/download?includeCsproj={includeCsproj}&includeAppSettings={includeAppSettings}");
using var fileStream = File.OpenWrite(zipFilename);
await zipStream.CopyToAsync(fileStream);
}
static async Task CreateEntry(this ZipArchive zipArchive, HttpClient client, VirtualFileSystemEntry vfsEntry)
{
await Console.Out.WriteLineAsync($"Adding {vfsEntry.Path}");
try
{
var zipArchiveEntry = zipArchive.CreateEntry(
entryName: vfsEntry.Path.Replace(@"C:\", ""),
compressionLevel: CompressionLevel.Optimal);
zipArchiveEntry.LastWriteTime = vfsEntry.Crtime;
using Stream zipArchiveEntryStream = zipArchiveEntry.Open();
using Stream downloadStream = await client.GetStreamAsync(requestUri: vfsEntry.Href);
await downloadStream.CopyToAsync(zipArchiveEntryStream);
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"{vfsEntry.Href} {vfsEntry.Name} {ex.Message}");
}
}
internal static string CreateURL(this SiteInfo info, string suffix)
=> string.IsNullOrEmpty(info.SlotName)
? $"/subscriptions/{info.SubscriptionID}/resourceGroups/{info.ResourceGroupName}/providers/Microsoft.Web/sites/{info.SiteName}/{suffix}"
: $"/subscriptions/{info.SubscriptionID}/resourceGroups/{info.ResourceGroupName}/providers/Microsoft.Web/sites/{info.SiteName}/slots/{info.SlotName}/{suffix}";
internal static HttpClient AddBearerCredential(this HttpClient httpClient, AccessToken accessToken)
{
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken.Token}");
return httpClient;
}
internal static HttpClient AddBasicAuthCredential(this HttpClient httpClient, string username, string password)
{
httpClient.DefaultRequestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"), Base64FormattingOptions.None)}");
return httpClient;
}
// /api/vfs Needs "Contributor"
internal static HttpClient AddAccessTokenAsBasicAuthCredential(this HttpClient httpClient, AccessToken accessToken)
=> httpClient.AddBasicAuthCredential(username: "00000000-0000-0000-0000-000000000000", password: accessToken.Token);
internal static HttpClient CreateARMHttpClient(this AccessToken accessToken)
=> new HttpClient() { BaseAddress = new("https://management.azure.com/") }.AddBearerCredential(accessToken);
internal static HttpClient CreateSCMBasicHttpClient(this AccessToken accessToken)
=> new HttpClient().AddAccessTokenAsBasicAuthCredential(accessToken);
internal static HttpClient CreateSCMBearerHttpClient(this AccessToken accessToken)
=> new HttpClient().AddBearerCredential(accessToken);
internal static HttpClient CreateSCMHttpClient(this PublishingCredential cred)
=> new HttpClient().AddBasicAuthCredential(
username: cred.Properties.PublishingUserName,
password: cred.Properties.PublishingPassword);
internal static async Task<PublishingCredential> FetchSCMCredential(this HttpClient armHttpClient, SiteInfo info)
{
// Requires action 'Microsoft.Web/sites/config/list/action'
// (Other : List Web App Security Sensitive Settings: List Web App's security sensitive settings, such as publishing credentials, app settings and connection strings)
string requestUri = info.CreateURL("config/publishingcredentials/list?api-version=2022-09-01");
HttpResponseMessage scmCredentialResponse = await armHttpClient.PostAsync(requestUri, content: null);
scmCredentialResponse.EnsureSuccessStatusCode();
return await scmCredentialResponse.Content.ReadFromJsonAsync<PublishingCredential>();
}
internal static async Task<bool> GetSCMBasicAuthAllowed(this HttpClient armHttpClient, SiteInfo info)
{
// Requires action 'Microsoft.Web/sites/basicPublishingCredentialsPolicies/read'
string requestUri = info.CreateURL("basicPublishingCredentialsPolicies/scm?api-version=2022-09-01");
string policyJsonStr = await armHttpClient.GetStringAsync(requestUri);
JsonNode json = JsonNode.Parse(policyJsonStr)!;
return (bool)json["properties"]["allow"];
}
internal static async Task SetSCMSetBasicAuth(this HttpClient armHttpClient, SiteInfo info, bool allow)
{
// az resource update \
// --subscription "${subscriptionId}" --resource-group "${resourceGroupName}" \
// --namespace Microsoft.Web --parent "sites/${siteName}" \
// --resource-type basicPublishingCredentialsPolicies --name scm --set properties.allow=true
var requestUri = info.CreateURL("basicPublishingCredentialsPolicies/scm?api-version=2022-09-01");
// Requires action 'Microsoft.Web/sites/basicPublishingCredentialsPolicies/scm/Read' and 'Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies/scm/Read'
var policyJsonStr = await armHttpClient.GetStringAsync(requestUri);
JsonNode json = JsonNode.Parse(policyJsonStr)!;
if (allow != (bool)json["properties"]["allow"])
{
json["properties"]["allow"] = allow;
// Requires 'Microsoft.Web/sites/basicPublishingCredentialsPolicies/write'
await armHttpClient.PutAsync(requestUri, new StringContent(
content: json.ToJsonString(),
encoding: Encoding.UTF8,
mediaType: "application/json"));
}
}
internal static async Task RecurseAsync(this HttpClient scmHttpClient, string requestUri, FollowPolicy policy, Func<HttpClient, VirtualFileSystemEntry, Task> task)
{
HttpResponseMessage response = await scmHttpClient.GetAsync(requestUri);
if (response == null || response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
await Console.Error.WriteLineAsync($"Not found: {requestUri}");
return;
}
response.EnsureSuccessStatusCode();
var entries = await response.Content.ReadFromJsonAsync<IEnumerable<VirtualFileSystemEntry>>();
foreach (var entry in entries)
{
await ((entry.Mime, policy) switch
{
("inode/directory", _) => scmHttpClient.RecurseAsync(entry.Href, policy, task),
("inode/shortcut", FollowPolicy.FollowShortcuts) => scmHttpClient.RecurseAsync(entry.Href, policy, task),
("inode/shortcut", FollowPolicy.IgnoreShortcuts) => Task.CompletedTask,
_ => task(scmHttpClient, entry)
});
}
}
}
}