用 50 行 Program.cs 寫個 ASP.NET Core CORS 上傳服務
2 |
寫 JavaScript 在別人家的網頁跑,若想將產生結果存成本機檔案,有一些做法,例如:產生一個 href 為 Data URI 的連結供點選下載(範例:用 100 行實現 HTML5 可存檔塗鴉版)、靠伺服器端程式實現自動下載 (範例:Canvas.toDataURL()另存檔案)... 等等。但想要做到不知不覺將檔案寫到本機的指定資料夾,基於安全考量,通常會被瀏覽器禁止。
偏偏我有個 Side-Project 有這種需求,我想在某個監看網頁上用 setInterval 跑一段 JavaScript 定期排程,當滿足某個條件時就擷圖備查。除了下載這條路,倒是可以寫個輕薄短小的 WebAPI 在本機跑,讓 JavaScript 透過 AJAX 上傳圖檔。但這種跨來源呼叫,必須要克服 CORS 限制,回應時得加入 Access-Control-Allow-Origin Header 才會成功。
想了一下,不如就用 ASP.NET Core 6 寫寫看吧! 我還給自己加了一個額外挑戰 - 用 dotnet new web 空白網站樣版,全部邏輯寫在 Program.cs 搞定。
先看成果,在高公局 1968 網頁執行以下程式,產生一個 .txt 跟用 Canvas 繪圖轉 .png 上傳到 ASP.NET Core 網站存檔:
function sendBlob(url, blob) {
var xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.onload = function (oEvent) {
console.log(xhr.responseText)
};
xhr.send(blob);
}
sendBlob('https://localhost:7008/?f=test.txt', new Blob(['ASP.NET Core Rocks!'], {type: 'text/plain'}));
var canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 100;
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.moveTo(50,0);
ctx.lineTo(0,100);
ctx.lineTo(100,100);
ctx.fill();
var blob = new Blob([canvas.toDataURL().split(',')[1]], { type: 'text/plain' });
sendBlob('https://localhost:7008/?f=test.png&t=base64', blob);
檔案成功寫入到本機 D:\FileStore
接著來看 ASP.NET Core 端程式,多虧 ASP.NET Core 6 功能完整,內建 CORS 支援,要從 appsettings.json 讀取檔案也很方便,完全不需參照第三方程式庫,整個專案只用了一個 Program.cs 就搞定,還不到 50 行。
var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration; // 取得 IConfiguration
// 取得檔案儲存位置
var fileStorePath = config.GetValue<string>("FileStorePath");
Directory.CreateDirectory(fileStorePath);
// 取得允許的副檔名
var allowExts = config.GetSection("AllowExts").Get<string[]>();
// 加入 CORS 服務
const string OriginsFromSetting = "OriginsFromAppSettingsJson";
builder.Services.AddCors(options => {
options.AddPolicy(
name: OriginsFromSetting,
builder => {
builder.WithOrigins(
// 轉 string[] 需要 Microsoft.Extensions.Configuration.Binder
config.GetSection("AllowOrigins").Get<string[]>());
}
);
});
var app = builder.Build();
// 啟用 CORS Middleware
app.UseCors();
app.MapGet("/", () => "Hello World!");
app.MapPost("/", async (context) => {
var fileName = context.Request.Query["f"].FirstOrDefault();
var res = "Unknown";
if (string.IsNullOrEmpty(fileName) ||
fileName.IndexOfAny(Path.GetInvalidPathChars()) > -1)
res = "Invalid filename";
else if (!allowExts.Contains(Path.GetExtension(fileName)))
res = "Invalid file type";
else {
// 只保留檔名(去除可能夾帶的路徑)加上時間序號
fileName = DateTime.Now.ToString("yyyyMMdd-HHmmss-") + Path.GetFileName(fileName);
using (var ms = new MemoryStream())
{
await context.Request.Body.CopyToAsync(ms);
var filePath = Path.Combine(fileStorePath, fileName);
var data = ms.ToArray();
if (context.Request.Query["t"].FirstOrDefault() == "base64") {
data = Convert.FromBase64String(System.Text.Encoding.UTF8.GetString(data));
}
File.WriteAllBytes(filePath, data);
res = "OK";
}
}
await context.Response.WriteAsync(res);
}).RequireCors(OriginsFromSetting);
app.Run();
雖然是自用且在本機執行,我還是加了基本安全控管。檔名部分用 Path.GetFileName() 過濾路徑防止夾帶 ..\ 進行路徑操控;用 fileName.IndexOfAny(Path.GetInvalidPathChars()) 檢查無效字元;Access-Control-Allow-Origin 不使用萬用字元,採正向表列由設定檔讀取;上傳副檔名也以白名單限制,以免被上傳 exe 等可執行檔(雖然在本機自用發生機率趨零,但就養成好習慣唄)。
appsettings.json 範例如下:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AllowOrigins": [
"https://1968.freeway.gov.tw",
"https://blog.darkthread.net"
],
"AllowExts": [
".txt", ".png"
],
"FileStorePath": "D:\\FileStore"
}
又完成一個輕巧小品,以後要蒐集 JavaScript 小程式回拋結果就方便多了。
A handy CORS upload service writing with ASP.NET Core in 50 lines Program.cs.
Comments
# by Joker
讚讚,最近也在玩minimal API 真的是很輕便,怎麼寫怎麼活。
# by 小黑
神~