This commit is contained in:
2026-09-05 10:00:36 +07:00
parent 25ca9b96a2
commit 9ad732c436
8 changed files with 319 additions and 24 deletions
+51 -11
View File
@@ -4,7 +4,7 @@ export
LifecycleExpiration, build_lifecycle_xml,
GarageStorage, put_file, get_file, list_files, delete_file,
set_lifecycle_expiration, get_lifecycle, delete_lifecycle,
s3_sign_url
s3_sign_url, s3_sign_url_web
using AWS
@service S3
@@ -24,7 +24,7 @@ storage1 = GeneralUtils.GarageStorage(
"https://s3-api.yiem.cc",
"GKb080154a2e5b19100b1b2c6e", # key ID (create at garage-ui.yiem.cc)
"a2c6b1379c2f731d3e6e5a408dd4d6cffca7511717675f55114ff94828febca1", # key ID's secret key
"testbucket"
"sommpanion"
)
# Safe to run inside concurrent HTTP handlers (e.g., Oxygen.jl, HTTP.jl)
@@ -347,6 +347,9 @@ function s3_sign_url(
scope = "$date_stamp/$region/$service/$terminator"
headers = OrderedDict{String,String}("Host" => URIs.URI(endpoint).host)
if verb == "PUT"
headers["Content-Type"] = content_type
end
sort!(headers; by=name -> lowercase(name))
canonical_header_names = join(map(name -> lowercase(name), collect(keys(headers))), ";")
@@ -355,11 +358,11 @@ function s3_sign_url(
"X-Amz-Algorithm" => "$scheme-$algorithm",
"X-Amz-Credential" => "$(creds.access_key_id)/$scope",
"X-Amz-Date" => datetime_stamp,
"X-Amz-Security-Token" => isempty(creds.token) ? "" : creds.token,
"X-Amz-SignedHeaders" => canonical_header_names,
)
sort!(query; by=name -> lowercase(name))
if !isempty(creds.token)
query["X-Amz-Security-Token"] = creds.token
end
canonical_headers = join(
map(
@@ -370,12 +373,24 @@ function s3_sign_url(
resource_path = "/$bucket/$key"
# Build canonical query string: sort by key=value pair lexicographically,
# URL-encoding each key and value (matching Garage's uri_encode)
sorted_q = sort(
[URIs.escapeuri(k) * "=" * URIs.escapeuri(v) for (k, v) in pairs(query)],
by=lowercase,
)
canonical_query = join(sorted_q, "&")
canonical_request = string(
"$verb\n",
"$(URIs.escapeuri(resource_path))\n",
"$(URIs.escapeuri(query))\n",
"$canonical_headers\n",
"$canonical_header_names\n",
resource_path,
"\n",
canonical_query,
"\n",
canonical_headers,
"\n",
canonical_header_names,
"\n",
"UNSIGNED-PAYLOAD",
)
@@ -386,8 +401,8 @@ function s3_sign_url(
bytes2hex(digest(MD_SHA256, canonical_request)),
)
key_secret = string(scheme, creds.secret_key)
key_date = digest(MD_SHA256, date_stamp, key_secret)
key_secret_bytes = Vector{UInt8}(string(scheme, creds.secret_key))
key_date = digest(MD_SHA256, date_stamp, key_secret_bytes)
key_region = digest(MD_SHA256, region, key_date)
key_service = digest(MD_SHA256, service, key_region)
key_signing = digest(MD_SHA256, terminator, key_service)
@@ -399,6 +414,31 @@ function s3_sign_url(
return "$(endpoint)/$bucket/$key?$query_string"
end
""" Generate a pre-signed URL using the Garage S3 web endpoint (s3-web).
Useful for direct browser access — URLs look like
`https://s3-web.yiem.cc/bucket/key?X-Amz-Signature=...`
Arguments and keyword arguments are the same as `s3_sign_url`.
"""
function s3_sign_url_web(
storage::GarageStorage,
key::String,
seconds::Int=3600;
verb::String="GET",
content_type::String="application/octet-stream",
signature_version::String="v4",
)::String
web_endpoint = replace(storage.config.endpoint, "s3-api" => "s3-web")
web_creds = SimpleCredentials(
storage.config.credentials.access_key_id,
storage.config.credentials.secret_key,
storage.config.credentials.token,
)
web_config = GarageConfig(web_endpoint, storage.config.region, web_creds)
web_storage = GarageStorage(web_config, storage.bucket)
return s3_sign_url(web_storage, key, seconds; verb, content_type, signature_version)
end
# ===================================================================
# 3. Bucket Lifecycle Configuration