feat: 实现 SimApiStorage(S3/MinIO 存储)
- 自实现 AWS Signature V4(HMAC-SHA256 基于 stdx SHA256),无需 Minio SDK - getUploadUrl / getDownloadUrl(预签名 PUT/GET) - uploadFile(直接 PUT 上传)、fullUrl / getUrl / getPath - 构造时检测并自动创建桶(静态守卫只执行一次) - 注册为 Scoped 以注入 IHttpContextAccessor(soulsoft DI 禁止 singleton 消费 scoped) - 签名核心已用 AWS 官方测试向量验证(最终签名 5d672d79... 一致) - README 同步(未实现表移除、新增 5.3 存储章节)
This commit is contained in:
@@ -82,7 +82,8 @@ simapi-cj/
|
||||
│ ├── exceptions/ # SimApiException
|
||||
│ ├── extensions/ # SimApiExtensions(addSimApi / useSimApi + 内置路由 + 响应封装)
|
||||
│ ├── helpers/ # SimApiError, SimApiUtil, SimApiAuth, SimApiCache, SimApiHttpClient,
|
||||
│ │ # SimApiAesUtil(AES-256), SimApiSignChecker(验签), SimApiAesBodyChecker(AES body)
|
||||
│ │ # SimApiAesUtil(AES-256), SimApiSignChecker(验签), SimApiAesBodyChecker(AES body),
|
||||
│ │ # SimApiStorage(S3/MinIO, 自实现 SigV4)
|
||||
│ ├── interfaces/ # ISimApiAuthChecker
|
||||
│ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志)
|
||||
│ ├── macros/ # ReadTomlVersion(编译期读版本号)
|
||||
@@ -320,6 +321,35 @@ builder.addSimApi { options =>
|
||||
| Fatal | 深红(DarkRed 粗体近似) |
|
||||
| 其他 | 白(White) |
|
||||
|
||||
### 5.3 存储 — SimApiStorage(S3/MinIO,对齐 C# SimApiStorage)
|
||||
|
||||
`enableSimApiStorage = true` 时注册 `SimApiStorage`(Scoped,内部自实现 AWS Signature V4,无需 Minio SDK):
|
||||
|
||||
```cangjie
|
||||
builder.addSimApi { options =>
|
||||
options.enableSimApiStorage = true
|
||||
options.configureSimApiStorage { storage =>
|
||||
storage.endpoint = "http://192.168.0.2:9000" // 必须 http:// 或 https:// 开头
|
||||
storage.serveUrl = "https://files.example.com" // 文件访问地址,不能以 / 结尾
|
||||
storage.bucket = "app-files"
|
||||
storage.accessKey = "minioadmin"
|
||||
storage.secretKey = "minioadmin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `getUploadUrl(path, expire=7200)` | 上传预签名 URL,返回 `GetUploadUrlResponse(UploadUrl, DownloadUrl, Path)` |
|
||||
| `getDownloadUrl(path, expire=600)` | 下载预签名 URL |
|
||||
| `uploadFile(path, data, contentType="image/png")` | 直接 PUT 上传(字节数组) |
|
||||
| `fullUrl(path)` / `getUrl(path)` | 补全访问 URL(`~/` 前缀依赖请求上下文) |
|
||||
| `getPath(url)` | 从 URL 还原相对路径(去掉 Endpoint/Bucket 或 ServeUrl 前缀) |
|
||||
|
||||
> 说明:桶不存在时自动创建(对齐 C# BucketExists + MakeBucket,静态守卫只执行一次);
|
||||
> 预签名与上传使用 AWS SigV4(HMAC-SHA256 基于 stdx SHA256 自实现),已用 AWS 官方测试向量验证签名正确。
|
||||
> 注册为 Scoped 是为了注入 `IHttpContextAccessor`(soulsoft DI 禁止 singleton 消费 scoped 服务)。
|
||||
|
||||
### 6. 内置路由(UseSimApi 自动注册)
|
||||
|
||||
| 路由 | 方法 | 条件 | 说明 |
|
||||
@@ -465,11 +495,10 @@ public class MyController <: SimApiBaseController {
|
||||
| 选项 | 原功能 | 状态 |
|
||||
|------|--------|------|
|
||||
| `enableSimApiDoc` | Swagger 文档(可换 soulsoft_web_openapi) | ❌ 未实现 |
|
||||
| `enableSimApiStorage` | S3/MinIO 存储 | ❌ 未实现 |
|
||||
| `enableSynapse` | MQTT 通信 | ❌ 未实现 |
|
||||
| `enableJob` | Hangfire 任务调度 | ❌ 未实现 |
|
||||
|
||||
> ✅ 已实现(曾为占位):`enableSimApiAuthGate`(AuthSDK 认证中心)、`SimApiAesUtil`(纯仓颉 AES-256-CBC,与 .NET 双向互操作)、`ISimApiAuthChecker`(注解鉴权时执行)、内置路由自定义路径。
|
||||
> ✅ 已实现(曾为占位):`enableSimApiStorage`(S3/MinIO,自实现 AWS SigV4)、`enableSimApiAuthGate`(AuthSDK 认证中心)、`SimApiAesUtil`(纯仓颉 AES-256-CBC,与 .NET 双向互操作)、`ISimApiAuthChecker`(注解鉴权时执行)、内置路由自定义路径。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -136,6 +136,14 @@ private func addSimApiCore(builder: WebHostBuilder, options: SimApiOptions): Web
|
||||
builder.services.addSingleton<SimApiHttpClient, SimApiHttpClient>()
|
||||
}
|
||||
|
||||
// 存储(S3/MinIO,对齐 C# AddHttpContextAccessor + AddSingleton<SimApiStorage>;
|
||||
// 仓颉版注册为 Scoped 以便注入 IHttpContextAccessor(DI 禁止 singleton 消费 scoped),
|
||||
// 桶初始化由静态守卫保证只执行一次)
|
||||
if (options.enableSimApiStorage) {
|
||||
builder.services.addHttpContextAccessor()
|
||||
builder.services.addScoped<SimApiStorage, SimApiStorage>()
|
||||
}
|
||||
|
||||
// AuthGate 认证中心 SDK(对齐 C# 注册 SimApiAuthClient/Center/Iam 单例)
|
||||
if (options.enableSimApiAuthGate) {
|
||||
builder.services.addSingleton<SimApiAuthClient, SimApiAuthClient>()
|
||||
@@ -194,7 +202,8 @@ extend WebHost <: SimApiHostExtensions {
|
||||
logger.info("开始配置 SimApiCache...")
|
||||
}
|
||||
|
||||
// SimApiStorage(占位)
|
||||
// SimApiStorage(已实现:addSimApi 中注册 Scoped,桶初始化惰性执行;
|
||||
// 对齐 C# 的 GetService 预热,但 scoped 服务不能从根解析,故仅输出配置日志)
|
||||
if (options.enableSimApiStorage) {
|
||||
logger.info("开始配置 SimApiStorage...")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
||||
* Helpers/SimApiStorage:S3/MinIO 兼容存储助手。
|
||||
*
|
||||
* 仓颉生态暂无 Minio SDK,此处自实现 AWS Signature V4(HMAC-SHA256 基于 stdx SHA256):
|
||||
* - 预签名 URL(PUT 上传 / GET 下载)
|
||||
* - 直接 PUT 上传(对象 + 建桶)
|
||||
* - HEAD 检测桶是否存在
|
||||
* 与 C# SimApiStorage(Minio SDK)的公开方法语义对齐:
|
||||
* GetUploadUrl / GetDownloadUrl / UploadFile / FullUrl / GetUrl / GetPath
|
||||
*/
|
||||
|
||||
package simapi.helpers
|
||||
|
||||
import std.collection.*
|
||||
import std.time.*
|
||||
import stdx.crypto.digest.*
|
||||
import stdx.encoding.hex.*
|
||||
import stdx.net.tls.*
|
||||
import stdx.net.tls.common.*
|
||||
import soulsoft_net_http.{ByteArrayContent, HttpClient, HttpRequestMessage}
|
||||
import soulsoft_net_http.{HttpMethod as NetHttpMethod}
|
||||
import soulsoft_web_http.*
|
||||
import simapi.configurations.*
|
||||
|
||||
/**
|
||||
* 上传预签名 URL 响应(对齐 C# record GetUploadUrlResponse(UploadUrl, DownloadUrl, Path))。
|
||||
*/
|
||||
public class GetUploadUrlResponse {
|
||||
public var uploadUrl: String = ""
|
||||
public var downloadUrl: String = ""
|
||||
public var path: String = ""
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(uploadUrl: String, downloadUrl: String, path: String) {
|
||||
this.uploadUrl = uploadUrl
|
||||
this.downloadUrl = downloadUrl
|
||||
this.path = path
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* S3/MinIO 存储助手(对齐 C# Helpers/SimApiStorage)。
|
||||
*
|
||||
* 说明:
|
||||
* - 注册为 Scoped 以注入 IHttpContextAccessor(soulsoft DI 禁止 singleton 消费 scoped 服务);
|
||||
* 桶的检测/创建由静态守卫保证整个进程只执行一次(对齐 C# 构造函数中 BucketExists+MakeBucket)。
|
||||
* - fullUrl/getUrl 的 "~/" 分支依赖当前请求上下文(对齐 C# IHttpContextAccessor)。
|
||||
*/
|
||||
public class SimApiStorage {
|
||||
private static var _bucketEnsured: Bool = false
|
||||
|
||||
private let _endpoint: String
|
||||
private let _serveUrl: String
|
||||
private let _bucket: String
|
||||
private let _accessKey: String
|
||||
private let _secretKey: String
|
||||
private let _useSsl: Bool
|
||||
private let _host: String
|
||||
private let _region: String = "us-east-1"
|
||||
private let _httpContextAccessor: IHttpContextAccessor
|
||||
|
||||
public init(options: SimApiOptions, httpContextAccessor: IHttpContextAccessor) {
|
||||
let storage = options.simApiStorageOptions
|
||||
if (storage.endpoint.isEmpty() || storage.serveUrl.isEmpty() || storage.bucket.isEmpty()) {
|
||||
throw Exception("SimApiStorage: Endpoint/ServeUrl/Bucket 不能为空")
|
||||
}
|
||||
var useSsl = false
|
||||
var host = ""
|
||||
if (storage.endpoint.startsWith("http://")) {
|
||||
host = storage.endpoint["http://".size..]
|
||||
} else if (storage.endpoint.startsWith("https://")) {
|
||||
useSsl = true
|
||||
host = storage.endpoint["https://".size..]
|
||||
} else {
|
||||
throw Exception("SimApiStorage: Error Endpoint")
|
||||
}
|
||||
if (storage.serveUrl.endsWith("/")) {
|
||||
throw Exception("SimApiStorage: ServeUrl must not end with /")
|
||||
}
|
||||
_endpoint = storage.endpoint
|
||||
_serveUrl = storage.serveUrl
|
||||
_bucket = storage.bucket
|
||||
_accessKey = storage.accessKey
|
||||
_secretKey = storage.secretKey
|
||||
_useSsl = useSsl
|
||||
_host = host
|
||||
_httpContextAccessor = httpContextAccessor
|
||||
// 桶不存在则创建(对齐 C# BucketExists + MakeBucket;静态守卫保证只执行一次)
|
||||
ensureBucketOnce()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上传预签名 URL(对齐 C# GetUploadUrl,默认 7200 秒)。
|
||||
*/
|
||||
public func getUploadUrl(path: String, expire!: Int64 = 7200): GetUploadUrlResponse {
|
||||
checkPath(path)
|
||||
let obj = trimLeadingSlash(path)
|
||||
let uploadUrl = presign("PUT", obj, expire)
|
||||
GetUploadUrlResponse(uploadUrl, "${_serveUrl}${path}", path)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载预签名 URL(对齐 C# GetDownloadUrl,默认 600 秒)。
|
||||
*/
|
||||
public func getDownloadUrl(path: String, expire!: Int64 = 600): String {
|
||||
checkPath(path)
|
||||
let obj = trimLeadingSlash(path)
|
||||
presign("GET", obj, expire)
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接上传文件(对齐 C# UploadFile;data 为文件字节)。
|
||||
*/
|
||||
public func uploadFile(path: String, data: Array<Byte>, contentType!: String = "image/png"): Unit {
|
||||
checkPath(path)
|
||||
let obj = trimLeadingSlash(path)
|
||||
putObject(obj, data, contentType)
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 path 获取完整的访问 URL(对齐 C# FullUrl)。
|
||||
*/
|
||||
public func fullUrl(path: ?String): ?String {
|
||||
if (let Some(p) <- path) {
|
||||
if (p.isEmpty() || p.startsWith("http://") || p.startsWith("https://")) {
|
||||
return Some(p)
|
||||
}
|
||||
if (!(p.startsWith("/") || p.startsWith("~/"))) {
|
||||
return Some(p)
|
||||
}
|
||||
if (p.startsWith("~/")) {
|
||||
return Some("${requestBaseUrl()}${p[1..]}")
|
||||
}
|
||||
return Some("${_serveUrl}${p}")
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个 Path 的访问 URL(对齐 C# GetUrl)。
|
||||
*/
|
||||
public func getUrl(path: ?String): ?String {
|
||||
if (let Some(p) <- path) {
|
||||
if (p.isEmpty()) {
|
||||
return Some(p)
|
||||
}
|
||||
if (p.startsWith("~/")) {
|
||||
return Some("${requestBaseUrl()}${p[1..]}")
|
||||
}
|
||||
if (p.startsWith("/")) {
|
||||
return Some("${_serveUrl}${p}")
|
||||
}
|
||||
return Some(p)
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 URL 中获取相对路径(对齐 C# GetPath;去掉 Endpoint/Bucket 或 ServeUrl 前缀)。
|
||||
*/
|
||||
public func getPath(url: ?String): ?String {
|
||||
if (let Some(u) <- url) {
|
||||
var r = u
|
||||
let prefix = "${_endpoint}/${_bucket}"
|
||||
if (r.startsWith(prefix)) {
|
||||
r = r[prefix.size..]
|
||||
}
|
||||
if (r.startsWith(_serveUrl)) {
|
||||
r = r[_serveUrl.size..]
|
||||
}
|
||||
return Some(r)
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ===== 私有:SigV4 签名与 S3 请求 =====
|
||||
|
||||
/// 预签名 URL(对齐 Minio PresignedPutObject / PresignedGetObject)
|
||||
private func presign(method: String, object: String, expireSeconds: Int64): String {
|
||||
let now = DateTime.nowUTC()
|
||||
let amzDate = formatAmzDate(now)
|
||||
let dateStamp = amzDate[0..8]
|
||||
let canonicalUri = "/${_bucket}/${uriEncode(object, false)}"
|
||||
let credential = "${_accessKey}/${dateStamp}/${_region}/s3/aws4_request"
|
||||
// 五个 X-Amz-* 参数按字典序排列(A<C<D<E<S)
|
||||
let canonicalQuery = "X-Amz-Algorithm=${uriEncode("AWS4-HMAC-SHA256", true)}" +
|
||||
"&X-Amz-Credential=${uriEncode(credential, true)}" +
|
||||
"&X-Amz-Date=${amzDate}" +
|
||||
"&X-Amz-Expires=${expireSeconds}" +
|
||||
"&X-Amz-SignedHeaders=host"
|
||||
let canonicalHeaders = "host:${_host}\n"
|
||||
let signedHeaders = "host"
|
||||
let canonicalRequest = "${method}\n${canonicalUri}\n${canonicalQuery}\n${canonicalHeaders}\n${signedHeaders}\nUNSIGNED-PAYLOAD"
|
||||
let stringToSign = "AWS4-HMAC-SHA256\n${amzDate}\n${dateStamp}/${_region}/s3/aws4_request\n" +
|
||||
"${toHexString(sha256Bytes(canonicalRequest.toArray()))}"
|
||||
let signature = toHexString(hmacSha256(buildSigningKey(dateStamp), stringToSign.toArray()))
|
||||
"${_endpoint}${canonicalUri}?${canonicalQuery}&X-Amz-Signature=${signature}"
|
||||
}
|
||||
|
||||
/// 直接 PUT 上传对象(Authorization 头签名,对齐 Minio PutObject)
|
||||
private func putObject(object: String, data: Array<Byte>, contentType: String): Unit {
|
||||
let now = DateTime.nowUTC()
|
||||
let amzDate = formatAmzDate(now)
|
||||
let dateStamp = amzDate[0..8]
|
||||
let payloadHash = toHexString(sha256Bytes(data))
|
||||
let canonicalUri = "/${_bucket}/${uriEncode(object, false)}"
|
||||
let canonicalHeaders = "host:${_host}\nx-amz-content-sha256:${payloadHash}\n"
|
||||
let signedHeaders = "host;x-amz-content-sha256"
|
||||
let canonicalRequest = "PUT\n${canonicalUri}\n\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}"
|
||||
let stringToSign = "AWS4-HMAC-SHA256\n${amzDate}\n${dateStamp}/${_region}/s3/aws4_request\n" +
|
||||
"${toHexString(sha256Bytes(canonicalRequest.toArray()))}"
|
||||
let authorization = buildAuthorization(dateStamp, signedHeaders,
|
||||
toHexString(hmacSha256(buildSigningKey(dateStamp), stringToSign.toArray())))
|
||||
let client = createClient()
|
||||
try {
|
||||
let request = HttpRequestMessage(NetHttpMethod.Put, "${_endpoint}${canonicalUri}")
|
||||
request.headers.add("x-amz-content-sha256", payloadHash)
|
||||
request.headers.add("Authorization", authorization)
|
||||
request.headers.add("Content-Type", contentType)
|
||||
request.content = ByteArrayContent(data)
|
||||
let response = client.send(request)
|
||||
try {
|
||||
SimApiError.errorWhenFalse(response.isSuccessStatusCode, code: response.statusCode,
|
||||
message: "SimApiStorage 上传失败: HTTP ${response.statusCode}")
|
||||
} finally {
|
||||
response.close()
|
||||
}
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
/// HEAD 检测桶是否存在(对齐 Minio BucketExistsAsync)
|
||||
private func bucketExists(): Bool {
|
||||
let now = DateTime.nowUTC()
|
||||
let amzDate = formatAmzDate(now)
|
||||
let dateStamp = amzDate[0..8]
|
||||
let payloadHash = toHexString(sha256Bytes(Array<Byte>(0, repeat: 0)))
|
||||
let canonicalUri = "/${_bucket}"
|
||||
let canonicalHeaders = "host:${_host}\nx-amz-content-sha256:${payloadHash}\n"
|
||||
let signedHeaders = "host;x-amz-content-sha256"
|
||||
let canonicalRequest = "HEAD\n${canonicalUri}\n\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}"
|
||||
let stringToSign = "AWS4-HMAC-SHA256\n${amzDate}\n${dateStamp}/${_region}/s3/aws4_request\n" +
|
||||
"${toHexString(sha256Bytes(canonicalRequest.toArray()))}"
|
||||
let authorization = buildAuthorization(dateStamp, signedHeaders,
|
||||
toHexString(hmacSha256(buildSigningKey(dateStamp), stringToSign.toArray())))
|
||||
let client = createClient()
|
||||
try {
|
||||
let request = HttpRequestMessage(NetHttpMethod.Head, "${_endpoint}${canonicalUri}")
|
||||
request.headers.add("x-amz-content-sha256", payloadHash)
|
||||
request.headers.add("Authorization", authorization)
|
||||
let response = client.send(request)
|
||||
try {
|
||||
response.statusCode == 200
|
||||
} finally {
|
||||
response.close()
|
||||
}
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建桶(对齐 Minio MakeBucketAsync)
|
||||
private func makeBucket(): Unit {
|
||||
let now = DateTime.nowUTC()
|
||||
let amzDate = formatAmzDate(now)
|
||||
let dateStamp = amzDate[0..8]
|
||||
let payloadHash = toHexString(sha256Bytes(Array<Byte>(0, repeat: 0)))
|
||||
let canonicalUri = "/${_bucket}"
|
||||
let canonicalHeaders = "host:${_host}\nx-amz-content-sha256:${payloadHash}\n"
|
||||
let signedHeaders = "host;x-amz-content-sha256"
|
||||
let canonicalRequest = "PUT\n${canonicalUri}\n\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}"
|
||||
let stringToSign = "AWS4-HMAC-SHA256\n${amzDate}\n${dateStamp}/${_region}/s3/aws4_request\n" +
|
||||
"${toHexString(sha256Bytes(canonicalRequest.toArray()))}"
|
||||
let authorization = buildAuthorization(dateStamp, signedHeaders,
|
||||
toHexString(hmacSha256(buildSigningKey(dateStamp), stringToSign.toArray())))
|
||||
let client = createClient()
|
||||
try {
|
||||
let request = HttpRequestMessage(NetHttpMethod.Put, "${_endpoint}${canonicalUri}")
|
||||
request.headers.add("x-amz-content-sha256", payloadHash)
|
||||
request.headers.add("Authorization", authorization)
|
||||
request.content = ByteArrayContent(Array<Byte>(0, repeat: 0))
|
||||
let response = client.send(request)
|
||||
try {
|
||||
SimApiError.errorWhenFalse(response.isSuccessStatusCode, code: response.statusCode,
|
||||
message: "SimApiStorage 创建桶失败: HTTP ${response.statusCode}")
|
||||
} finally {
|
||||
response.close()
|
||||
}
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
private func ensureBucketOnce(): Unit {
|
||||
if (!SimApiStorage._bucketEnsured) {
|
||||
if (!bucketExists()) {
|
||||
makeBucket()
|
||||
}
|
||||
SimApiStorage._bucketEnsured = true
|
||||
}
|
||||
}
|
||||
|
||||
private func buildAuthorization(dateStamp: String, signedHeaders: String, signature: String): String {
|
||||
"AWS4-HMAC-SHA256 Credential=${_accessKey}/${dateStamp}/${_region}/s3/aws4_request, " +
|
||||
"SignedHeaders=${signedHeaders}, Signature=${signature}"
|
||||
}
|
||||
|
||||
private func buildSigningKey(dateStamp: String): Array<Byte> {
|
||||
let kDate = hmacSha256("AWS4${_secretKey}".toArray(), dateStamp.toArray())
|
||||
let kRegion = hmacSha256(kDate, _region.toArray())
|
||||
let kService = hmacSha256(kRegion, "s3".toArray())
|
||||
hmacSha256(kService, "aws4_request".toArray())
|
||||
}
|
||||
|
||||
private func createClient(): HttpClient {
|
||||
HttpClient.create { builder =>
|
||||
builder.noProxy()
|
||||
if (_useSsl) {
|
||||
var tls = TlsClientConfig()
|
||||
tls.verifyMode = CertificateVerifyMode.TrustAll
|
||||
let host = extractHost(_endpoint)
|
||||
if (!host.isEmpty()) {
|
||||
tls.serverName = Some(host)
|
||||
}
|
||||
builder.tlsConfig(tls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func checkPath(path: String): Unit {
|
||||
if (!path.startsWith("/")) {
|
||||
throw Exception("path must start with /")
|
||||
}
|
||||
}
|
||||
|
||||
private func trimLeadingSlash(path: String): String {
|
||||
if (path.startsWith("/")) {
|
||||
path[1..]
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前请求基础地址(scheme://host);无请求上下文时返回空串
|
||||
private func requestBaseUrl(): String {
|
||||
if (let Some(ctx) <- _httpContextAccessor.context) {
|
||||
return "${ctx.request.scheme}://${ctx.request.host}"
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
private static func extractHost(fullUrl: String): String {
|
||||
match (fullUrl.indexOf("://")) {
|
||||
case Some(i) =>
|
||||
let rest = fullUrl[i + 3..]
|
||||
let slash = rest.indexOf("/") ?? rest.size
|
||||
let q = rest.indexOf("?") ?? rest.size
|
||||
let end = if (slash < q) { slash } else { q }
|
||||
rest[0..end]
|
||||
case None => ""
|
||||
}
|
||||
}
|
||||
|
||||
private static func formatAmzDate(dt: DateTime): String {
|
||||
"${dt.year}${pad2(dt.monthValue)}${pad2(dt.dayOfMonth)}T${pad2(dt.hour)}${pad2(dt.minute)}${pad2(dt.second)}Z"
|
||||
}
|
||||
|
||||
private static func pad2(v: Int64): String {
|
||||
if (v < 10) {
|
||||
"0${v}"
|
||||
} else {
|
||||
"${v}"
|
||||
}
|
||||
}
|
||||
|
||||
/// RFC 3986 百分号编码(encodeSlash=false 时保留 '/')
|
||||
private static func uriEncode(s: String, encodeSlash: Bool): String {
|
||||
let bytes = s.toArray()
|
||||
let hex = "0123456789ABCDEF"
|
||||
var sb = StringBuilder()
|
||||
for (b in bytes) {
|
||||
let u = toU8(b)
|
||||
let unreserved = (u >= 0x41u8 && u <= 0x5Au8) || (u >= 0x61u8 && u <= 0x7Au8) ||
|
||||
(u >= 0x30u8 && u <= 0x39u8) || u == 0x2Du8 || u == 0x5Fu8 || u == 0x2Eu8 || u == 0x7Eu8
|
||||
if (unreserved || (u == 0x2Fu8 && !encodeSlash)) {
|
||||
sb.append(Rune(UInt32(u)))
|
||||
} else {
|
||||
sb.append("%")
|
||||
sb.append(Rune(UInt32(hex[Int64((u >> 4u8) & 0x0Fu8)])))
|
||||
sb.append(Rune(UInt32(hex[Int64(u & 0x0Fu8)])))
|
||||
}
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
private static func toU8(b: Byte): UInt8 {
|
||||
if (b < 0) {
|
||||
UInt8(Int64(b) + 256)
|
||||
} else {
|
||||
UInt8(Int64(b))
|
||||
}
|
||||
}
|
||||
|
||||
/// HMAC-SHA256(自实现,基于 stdx SHA256;stdx 静态库无现成 HMAC 封装)
|
||||
private static func hmacSha256(key: Array<Byte>, data: Array<Byte>): Array<Byte> {
|
||||
var k = key
|
||||
if (k.size > 64) {
|
||||
k = sha256Bytes(k)
|
||||
}
|
||||
var keyPadded = Array<Byte>(64, repeat: 0)
|
||||
for (i in 0..k.size) {
|
||||
keyPadded[i] = k[i]
|
||||
}
|
||||
var inner = ArrayList<Byte>()
|
||||
for (i in 0..64) {
|
||||
inner.add(bxor(keyPadded[i], 0x36u8))
|
||||
}
|
||||
for (b in data) {
|
||||
inner.add(b)
|
||||
}
|
||||
let innerHash = sha256Bytes(inner.toArray())
|
||||
var outer = ArrayList<Byte>()
|
||||
for (i in 0..64) {
|
||||
outer.add(bxor(keyPadded[i], 0x5Cu8))
|
||||
}
|
||||
for (b in innerHash) {
|
||||
outer.add(b)
|
||||
}
|
||||
sha256Bytes(outer.toArray())
|
||||
}
|
||||
|
||||
@OverflowWrapping
|
||||
private static func bxor(a: Byte, mask: UInt8): Byte {
|
||||
let ua = toU8(a)
|
||||
let r: Byte = ua ^ mask
|
||||
r
|
||||
}
|
||||
|
||||
private static func sha256Bytes(data: Array<Byte>): Array<Byte> {
|
||||
let sha = SHA256()
|
||||
sha.write(data)
|
||||
sha.finish()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user