s3_test.v

6.8 KB · raw

1module remote23import net4import net.http5import time67// The vectors below come from two independent sources:8//9//   1. the official AWS `get-vanilla` case from the SigV4 test suite, whose10//      signature is published with the specification;11//   2. an S3-shaped conditional PUT, computed by a separate implementation12//      written from the same specification in another language.13//14// Between them they cover the canonical request, the key derivation chain, the15// string-to-sign and the header set that S3 actually sends.16fn example_time() time.Time {17	return time.Time{18		year:     201519		month:    820		day:      3021		hour:     1222		minute:   3623		second:   024		is_local: false25	}26}2728fn test_the_official_aws_get_vanilla_vector() {29	signer := Signer{30		access_key:   'AKIDEXAMPLE'31		secret_key:   'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'32		region:       'us-east-1'33		service:      'service'34		sign_payload: false35	}36	headers := signer.sign('GET', 'https://example.amazonaws.com/', '', map[string]string{},37		[]u8{}, example_time())38	assert headers['authorization'] == 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31'39	assert headers['host'] == 'example.amazonaws.com'40	assert headers['x-amz-date'] == '20150830T123600Z'41}4243fn test_an_s3_conditional_put_signs_the_payload_and_the_condition() {44	signer := Signer{45		access_key: 'SCWACCESSKEY'46		secret_key: 'scw-secret-key'47		region:     'fr-par'48		service:    s3_service49	}50	headers := signer.sign('PUT', 'https://s3.fr-par.scw.cloud/bucket/repo/objects/abc', '', {51		'If-None-Match': '*'52	}, 'compressed-bytes'.bytes(), time.Time{53		year:     202654		month:    955		day:      2356		hour:     1257		minute:   058		second:   059		is_local: false60	})61	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'62	// The body is part of the signature: a different payload must change it.63	other := signer.sign('PUT', 'https://s3.fr-par.scw.cloud/bucket/repo/objects/abc', '', {64		'If-None-Match': '*'65	}, 'different-bytes'.bytes(), time.Time{66		year:     202667		month:    968		day:      2369		hour:     1270		minute:   071		second:   072		is_local: false73	})74	assert other['authorization'] != headers['authorization']75}7677fn test_urls_are_split_into_host_and_encoded_path() {78	parts := split_url('https://s3.fr-par.scw.cloud/bucket/repo/objects/abc')79	assert parts.host == 's3.fr-par.scw.cloud'80	assert parts.path == '/bucket/repo/objects/abc'8182	// The URL arrives as it will be sent, already encoded: splitting it must83	// not encode it a second time.84	spaces := split_url('https://example.com/bucket/a%20file.txt')85	assert spaces.path == '/bucket/a%20file.txt'8687	// A repo named after a folder with a space: the path that is signed is the88	// path that is sent, encoded once.89	s3 := new_s3('https://s3.fr-par.scw.cloud', 'bucket', 'fr-par', 'key', 'secret')!90	assert split_url(s3.url_for('my repo/head', '')).path == '/bucket/my%20repo/head'9192	// An endpoint with a path prefix keeps it.93	prefixed := split_url('https://example.com/prefix/bucket/key?x=1')94	assert prefixed.host == 'example.com'95	assert prefixed.path == '/prefix/bucket/key'96}9798fn test_canonical_query_is_sorted_and_encoded() {99	assert canonical_query('') == ''100	// The query arrives raw, so a slash in a key is encoded here and only here.101	assert canonical_query('prefix=b/a&list-type=2') == 'list-type=2&prefix=b%2Fa'102	assert canonical_query('prefix=repo/objects/x&continuation-token=a=b') == 'continuation-token=a%3Db&prefix=repo%2Fobjects%2Fx'103	assert canonical_query('prefix=a b') == 'prefix=a%20b'104}105106// A server that follows RFC 7232 compares If-Match against the entity-tag as107// it sent it, quotes included. Koh must hand the ETag back untouched, or every108// head swap after the first is refused and offers never land again.109struct StrictHead {}110111fn (mut h StrictHead) handle(req http.Request) http.Response {112	mut resp := http.Response{113		status_code: 200114		status_msg:  'OK'115	}116	if req.method == .put {117		if_match := req.header.get_custom('If-Match', http.HeaderQueryConfig{}) or { '' }118		if if_match != '"3b1f"' {119			resp.status_code = 412120			resp.status_msg = 'Precondition Failed'121		}122		return resp123	}124	resp.body = 'head-id'125	resp.header.add_custom('ETag', '"3b1f"') or {}126	return resp127}128129fn test_an_etag_goes_back_exactly_as_the_server_sent_it() {130	mut port := 0131	mut server := &http.Server{}132	for candidate in [19311, 19312, 19313, 19314, 19315] {133		listener := net.listen_tcp(.ip, '127.0.0.1:${candidate}') or { continue }134		server = &http.Server{135			listener:             listener136			handler:              StrictHead{}137			show_startup_message: false138		}139		port = candidate140		break141	}142	assert port != 0, 'no free port for the test server'143	spawn server.listen_and_serve()144	server.wait_till_running()!145	defer {146		server.stop()147	}148	mut backend := new_s3('http://127.0.0.1:${port}', 'bucket', 'fr-par', 'ak', 'sk')!149	head := backend.get('repo/head')!150	assert head.tag == '"3b1f"'151	assert backend.put_if_match('repo/head', 'next'.bytes(), head.tag)!152}153154fn test_list_objects_responses_are_read() {155	body := '<ListBucketResult><IsTruncated>false</IsTruncated>' +156		'<Contents><Key>repo/objects/aaa</Key></Contents>' +157		'<Contents><Key>repo/objects/bbb</Key></Contents></ListBucketResult>'158	assert elements(body, 'Key') == ['repo/objects/aaa', 'repo/objects/bbb']159	assert first_element(body, 'NextContinuationToken') == none160	assert !body.contains('<IsTruncated>true</IsTruncated>')161162	paged := '<ListBucketResult><IsTruncated>true</IsTruncated>' +163		'<NextContinuationToken>aGVsbG8=</NextContinuationToken>' +164		'<Contents><Key>repo/objects/c&amp;c</Key></Contents></ListBucketResult>'165	assert first_element(paged, 'NextContinuationToken') or { '' } == 'aGVsbG8='166	assert elements(paged, 'Key') == ['repo/objects/c&c']167	assert paged.contains('<IsTruncated>true</IsTruncated>')168}169170fn test_the_constructor_refuses_settings_that_cannot_work() {171	if _ := new_s3('', 'bucket', 'region', 'ak', 'sk') {172		assert false, 'a missing endpoint must be refused'173	}174	if _ := new_s3('s3.fr-par.scw.cloud', 'bucket', 'region', 'ak', 'sk') {175		assert false, 'an endpoint without a scheme must be refused'176	}177	if _ := new_s3('https://s3.fr-par.scw.cloud', 'bucket', 'region', '', '') {178		assert false, 'missing keys must be refused'179	}180	backend := new_s3('https://s3.fr-par.scw.cloud/', 'bucket', 'fr-par', 'ak', 'sk')!181	assert backend.root == 'https://s3.fr-par.scw.cloud'182	assert backend.url_for('repo/objects/abc', '') == 'https://s3.fr-par.scw.cloud/bucket/repo/objects/abc'183	assert backend.url_for('', 'list-type=2&prefix=x') == 'https://s3.fr-par.scw.cloud/bucket?list-type=2&prefix=x'184}