module remote
import net
import net.http
import time
// The vectors below come from two independent sources:
//
// 1. the official AWS `get-vanilla` case from the SigV4 test suite, whose
// signature is published with the specification;
// 2. an S3-shaped conditional PUT, computed by a separate implementation
// written from the same specification in another language.
//
// Between them they cover the canonical request, the key derivation chain, the
// string-to-sign and the header set that S3 actually sends.
fn example_time() time.Time {
return time.Time{
year: 2015
month: 8
day: 30
hour: 12
minute: 36
second: 0
is_local: false
}
}
fn test_the_official_aws_get_vanilla_vector() {
signer := Signer{
access_key: 'AKIDEXAMPLE'
secret_key: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'
region: 'us-east-1'
service: 'service'
sign_payload: false
}
headers := signer.sign('GET', 'https://example.amazonaws.com/', '', map[string]string{},
[]u8{}, example_time())
assert headers['authorization'] == 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31'
assert headers['host'] == 'example.amazonaws.com'
assert headers['x-amz-date'] == '20150830T123600Z'
}
fn test_an_s3_conditional_put_signs_the_payload_and_the_condition() {
signer := Signer{
access_key: 'SCWACCESSKEY'
secret_key: 'scw-secret-key'
region: 'fr-par'
service: s3_service
}
headers := signer.sign('PUT', 'https://s3.fr-par.scw.cloud/bucket/repo/objects/abc', '', {
'If-None-Match': '*'
}, 'compressed-bytes'.bytes(), time.Time{
year: 2026
month: 9
day: 23
hour: 12
minute: 0
second: 0
is_local: false
})
assert headers['authorization'] == 'AWS4-HMAC-SHA256 Credential=SCWACCESSKEY/20260923/fr-par/s3/aws4_request, SignedHeaders=host;if-none-match;x-amz-content-sha256;x-amz-date, Signature=9790aa767fc3e4c36fce70c92476933a90b401200068f06c340bf1108435580a'
// The body is part of the signature: a different payload must change it.
other := signer.sign('PUT', 'https://s3.fr-par.scw.cloud/bucket/repo/objects/abc', '', {
'If-None-Match': '*'
}, 'different-bytes'.bytes(), time.Time{
year: 2026
month: 9
day: 23
hour: 12
minute: 0
second: 0
is_local: false
})
assert other['authorization'] != headers['authorization']
}
fn test_urls_are_split_into_host_and_encoded_path() {
parts := split_url('https://s3.fr-par.scw.cloud/bucket/repo/objects/abc')
assert parts.host == 's3.fr-par.scw.cloud'
assert parts.path == '/bucket/repo/objects/abc'
// The URL arrives as it will be sent, already encoded: splitting it must
// not encode it a second time.
spaces := split_url('https://example.com/bucket/a%20file.txt')
assert spaces.path == '/bucket/a%20file.txt'
// A repo named after a folder with a space: the path that is signed is the
// path that is sent, encoded once.
s3 := new_s3('https://s3.fr-par.scw.cloud', 'bucket', 'fr-par', 'key', 'secret')!
assert split_url(s3.url_for('my repo/head', '')).path == '/bucket/my%20repo/head'
// An endpoint with a path prefix keeps it.
prefixed := split_url('https://example.com/prefix/bucket/key?x=1')
assert prefixed.host == 'example.com'
assert prefixed.path == '/prefix/bucket/key'
}
fn test_canonical_query_is_sorted_and_encoded() {
assert canonical_query('') == ''
// The query arrives raw, so a slash in a key is encoded here and only here.
assert canonical_query('prefix=b/a&list-type=2') == 'list-type=2&prefix=b%2Fa'
assert canonical_query('prefix=repo/objects/x&continuation-token=a=b') == 'continuation-token=a%3Db&prefix=repo%2Fobjects%2Fx'
assert canonical_query('prefix=a b') == 'prefix=a%20b'
}
// A server that follows RFC 7232 compares If-Match against the entity-tag as
// it sent it, quotes included. Koh must hand the ETag back untouched, or every
// head swap after the first is refused and offers never land again.
struct StrictHead {}
fn (mut h StrictHead) handle(req http.Request) http.Response {
mut resp := http.Response{
status_code: 200
status_msg: 'OK'
}
if req.method == .put {
if_match := req.header.get_custom('If-Match', http.HeaderQueryConfig{}) or { '' }
if if_match != '"3b1f"' {
resp.status_code = 412
resp.status_msg = 'Precondition Failed'
}
return resp
}
resp.body = 'head-id'
resp.header.add_custom('ETag', '"3b1f"') or {}
return resp
}
fn test_an_etag_goes_back_exactly_as_the_server_sent_it() {
mut port := 0
mut server := &http.Server{}
for candidate in [19311, 19312, 19313, 19314, 19315] {
listener := net.listen_tcp(.ip, '127.0.0.1:${candidate}') or { continue }
server = &http.Server{
listener: listener
handler: StrictHead{}
show_startup_message: false
}
port = candidate
break
}
assert port != 0, 'no free port for the test server'
spawn server.listen_and_serve()
server.wait_till_running()!
defer {
server.stop()
}
mut backend := new_s3('http://127.0.0.1:${port}', 'bucket', 'fr-par', 'ak', 'sk')!
head := backend.get('repo/head')!
assert head.tag == '"3b1f"'
assert backend.put_if_match('repo/head', 'next'.bytes(), head.tag)!
}
fn test_list_objects_responses_are_read() {
body := 'false' +
'repo/objects/aaa' +
'repo/objects/bbb'
assert elements(body, 'Key') == ['repo/objects/aaa', 'repo/objects/bbb']
assert first_element(body, 'NextContinuationToken') == none
assert !body.contains('true')
paged := 'true' +
'aGVsbG8=' +
'repo/objects/c&c'
assert first_element(paged, 'NextContinuationToken') or { '' } == 'aGVsbG8='
assert elements(paged, 'Key') == ['repo/objects/c&c']
assert paged.contains('true')
}
fn test_the_constructor_refuses_settings_that_cannot_work() {
if _ := new_s3('', 'bucket', 'region', 'ak', 'sk') {
assert false, 'a missing endpoint must be refused'
}
if _ := new_s3('s3.fr-par.scw.cloud', 'bucket', 'region', 'ak', 'sk') {
assert false, 'an endpoint without a scheme must be refused'
}
if _ := new_s3('https://s3.fr-par.scw.cloud', 'bucket', 'region', '', '') {
assert false, 'missing keys must be refused'
}
backend := new_s3('https://s3.fr-par.scw.cloud/', 'bucket', 'fr-par', 'ak', 'sk')!
assert backend.root == 'https://s3.fr-par.scw.cloud'
assert backend.url_for('repo/objects/abc', '') == 'https://s3.fr-par.scw.cloud/bucket/repo/objects/abc'
assert backend.url_for('', 'list-type=2&prefix=x') == 'https://s3.fr-par.scw.cloud/bucket?list-type=2&prefix=x'
}