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' }