Object Storage API

개요

네이버 클라우드 플랫폼 Object Storage는 스토리지 관리와 사용에 필요한 Amazon S3 API를 제공합니다. S3 API를 이용하여 네이버 클라우드 플랫폼 Object Storage에서 생성되는 요청은 AWS의 인증(Authorization) 헤더 구현체를 사용하여 인증되어야 합니다. 네이버 클라우드 플랫폼 Object Storage는 2가지의 인증 방식을 지원합니다.

  • Signature Version 2
  • Signature Version 4 (본 가이드에서는 해당 인증 방식을 설명합니다.)

지원하는 Object Storage API

계정 관련 오퍼레이션

계정 레벨에서는 해당 계정에 속한 버킷 목록을 조회하는 오퍼레이션만 지원합니다. 계정의 버킷 개수는 1,000 개로 제한됩니다.

오퍼레이션 설명
GET Account(List Buckets) 계정에 속한 버킷 목록을 조회합니다.

버킷 오퍼레이션

버킷을 생성, 삭제, 조회, 제어하는 오퍼레이션을 설명합니다.

버킷 내의 오브젝트 목록 조회 GET Bucket (List Objects) Version 2 는 지원하지 않습니다.

오퍼레이션 설명
PUT Bucket 버킷을 생성합니다. 계정의 버킷 개수는 1,000 개로 제한됩니다.
GET Bucket(List Objects) 버킷에 포함된 오브젝트를 조회합니다. 한 번에 최대 1,000 개의 오브젝트가 나열됩니다.
HEAD Bucket 버킷 헤더를 조회합니다.
DELETE Bucket 빈 버킷을 삭제합니다.
PUT Bucket ACL 버킷에 적용할 접근 제어 목록(ACL)을 생성합니다.
GET Bucket ACL 버킷에 적용된 접근 제어 목록(ACL)을 조회합니다.
PUT Bucket CORS 버킷에 적용할 CORS 설정을 생성합니다.
GET Bucket CORS 버킷에 적용된 CORS 설정을 조회합니다.
DELETE Bucket CORS 버킷에 적용된 CORS 설정을 삭제합니다.
List Multipart Uploads 완료되지 않았거나 취소된 멀티파트 업로드를 조회합니다.

오브젝트 오퍼레이션

오브젝트를 생성, 삭제, 조회, 제어하는 오퍼레이션을 설명합니다.

오퍼레이션 설명
PUT Object 버킷에 오브젝트를 추가(업로드)합니다.
PUT Object (Copy) 오브젝트 복사본을 생성합니다.
GET Object 오브젝트를 조회(다운로드)합니다.
HEAD Object 오브젝트 헤더를 조회합니다.
DELETE Object 버킷에서 오브젝트를 삭제합니다.
DELETE Multiple Objects 버킷에서 멀티파트 오브젝트를 삭제합니다.
PUT Object ACL 오브젝트에 적용할 접근 제어 목록(ACL)을 생성합니다.
GET Object ACL 오브젝트에 적용된 접근 제어 목록(ACL)을 조회합니다.
OPTIONS Object CORS 설정을 확인하여 특정 요청을 전송할 수 있는지 조회합니다.
Initiate Multipart Upload 업로드할 파트 집합에 지정할 업로드 ID를 생성합니다.
Upload Part 업로드 ID와 연관된 오브젝트의 파트를 업로드합니다.
Complete Multipart Upload 업로드 ID와 연관된 파트로 분리된 오브젝트를 결합합니다.
Abort Multipart Upload 업로드를 중단하고 업로드 ID와 연관된 파트를 삭제합니다.

Object Storage API 호출 절차

Object Storage API 호출은 다음과 같은 단계로 진행되어야 합니다.

1. 인증키 생성하기
2. 호출 도메인 확인
3. 인증

인증키 생성하기

NAVER CLOUD PLATFORM 계정이 생성되면 기본적으로 인증키가 한개 발급됩니다. 발급된 인증키는 네이버 클라우드 플랫폼 홈페이지[마이페이지] > [계정관리] > [인증키관리]에서 확인할 수 있습니다. 인증키는 계정 생성 시 자동으로 발급되는 것 외에 사용자가 하나 더 생성할 수 있어서 두 개까지 발급받을 수 있습니다.

참고
인증키를 ‘사용 중지’로 설정하거나 삭제하면 유효하지 않은 키로 인식됩니다.


API 인증키는 Access Key ID와 Secret Key 한 쌍으로 구성되어 있습니다. 한 쌍의 API 인증키는 API를 인증할 때 파라미터로 직접 전달됩니다.

  1. 네이버 클라우드 플랫폼 홈페이지에서 로그인을 합니다.
  2. [마이페이지] > [계정관리] > [인증키관리] 메뉴로 접속하시면 “신규 API 인증키 생성” 버튼을 클릭합니다.
    • 기존에 생성하신 인증키가 있으실 경우에는 해당 인증키를 사용하실 수 있습니다.
  3. API 인증키 관리에서 발급받은 자신의 Access Key ID와 Secret Key를 확인합니다.

호출 도메인 확인

HTTP/HTTPS 프로토콜을 모두 지원하지만, 데이터 보호를 위해 HTTPS 사용을 권장합니다.

참고
제공 리전은 지속적으로 확대해갈 예정입니다.


리전 별 호출 도메인

리전 리전 이름 호출 도메인
한국 kr-standard https://kr.object.ncloudstorage.com
미국서부(New) us-standard https://us.object.ncloudstorage.com
싱가포르(New) sg-standard https://sg.object.ncloudstorage.com
일본(New) jp-standard https://jp.object.ncloudstorage.com
독일(New) de-standard https://de.object.ncloudstorage.com

인증

인증 헤더(Authorization)를 생성하기 위해 다음의 절차를 수행합니다.

1. 표준화 요청 생성 (Canonical Request)
2. 서명할 문자열 생성 (String to Sign)
3. 서명키 생성 (Signing Key)
4. 서명 생성 (Signature)
5. 인증 헤더 생성 (Authorization)


표준화 요청 생성 (Canonical Request)

표준화 요청(Canonical Request)의 구성은 다음과 같습니다.

<HTTPMethod>\n
<CanonicalURI>\n
<CanonicalQueryString>\n
<CanonicalHeaders>\n
<SignedHeaders>\n
<HashedPayload>
HTTPMethod

사용하려는 HTTP 메서드를 선언합니다. (예: PUT)

CanonicalURI

접근하려는 리소스를 표준화된 방식(URI-encoded)으로 정의합니다. (예: /path/object)

CanonicalQueryString

요청 파라미터가 있는 경우에는 표준화된 방식(URI-encoded)으로 정의하고 알파벳순으로 나열합니다.

UriEncode("marker")+"="+UriEncode("someMarker")+"&"+
UriEncode("max-keys")+"="+UriEncode("20") + "&" +
UriEncode("prefix")+"="+UriEncode("somePrefix")
CanonicalHeaders

요청에 포함된 헤더 이름과 값을 표준화된 방식으로 정의합니다. 헤더 이름은 모두 소문자로 변환하고, 헤더 값의 앞뒤 공백을 제거합니다. 각 헤더 항목의 끝에 개행 문자\n를 삽입합니다. 이때 헤더 이름으로 알파벳순으로 정렬되어야 합니다. host 헤더와 x-amz-로 시작하는 헤더는 필수 대상이고, 보다 정확한 데이터 정합성 확인을 위해 모든 헤더를 포함시키는 것을 권장합니다. Signature Version 4 인증의 모든 요청에서는 x-amz-content-sha256 헤더가 포함되어야 합니다. 데이터(payload)에 대한 해시값을 입력해야 하고 2개의 옵션을 제공합니다.
- 서명된 데이터(signed payload) : 데이터(payload)의 SHA256 해시의 16진수값을 입력합니다. 만약 데이터(payload)가 없다면 빈 문자열의 해시값을 입력해야 합니다.
- 서명되지 않은 데이터(unsigned payload) : 데이터(payload)를 서명하지 않으려면 x-amz-content-sha256 헤더의 값으로 UNSIGNED-PAYLOAD 문자열을 입력합니다.

Lowercase(<HeaderName1>)+":"+Trim(<value>)+"\n"
Lowercase(<HeaderName2>)+":"+Trim(<value>)+"\n"
SignedHeaders

요청에 포함된 헤더 이름을 나열합니다. 헤더 이름은 모두 소문자로 변환하고 알파벳순으로 정렬합니다. 각 헤더 이름의 구분자로 세미콜론(“;”)을 입력합니다.

host;x-amz-content-sha256;x-amz-date
HashedPayload

데이터(payload)의 SHA256 해시의 16진수값을 입력합니다. (x-amz-content-sha256 헤더의 값과 동일) 만약 요청 데이터(payload)가 없다면 빈 문자열의 해시값을 입력해야 합니다. 데이터(payload)를 서명하지 않으려면 UNSIGNED-PAYLOAD 문자열을 입력합니다.

Hex(SHA256Hash(<payload>)
or
Hex(SHA256Hash(""))
or
UNSIGNED-PAYLOAD



서명할 문자열 생성 (String to Sign)

서명할 문자열(String to Sign)의 구성은 다음과 같습니다.

AWS4-HMAC-SHA256\n
<Timestamp>\n
<Scope>\n
Hex(SHA256Hash(<CanonicalRequest>))
Timestamp

UTC(Coordinated Universal Time)를 기준으로 ISO 8601 표준 규정에 따라 표시되어야 합니다(예: 20161128T152924Z).

Scope

Scope 문자열은 다음과 같이 구성됩니다. <Date>/<Region>/s3/aws4_request
- Date : YYYYMMDD 포맷으로 입력합니다.
- Region : 리전 이름을 입력합니다. 호출 도메인 확인에서 확인할 수 있습니다. 리전 이름은 필수 입력값이지만 네이버 클라우드 플랫폼 Object Storage에서는 도메인으로 리전을 구분하기 때문에 리전 이름의 정합성을 확인하지는 않습니다.

CanonicalRequest

생성한 표준화 요청(Canonical Request)의 SHA256 해시의 16진수값을 입력합니다.

서명키 생성 (Signing Key)

서명키(Signing Key)의 구성은 다음과 같습니다.

kSecret     = <SecretKey>
kDate       = HMAC-SHA256("AWS4" + kSecret, <Date>)
kRegion     = HMAC-SHA256(kDate, <Region>)
kService    = HMAC-SHA256(kRegion, "s3")
kSigning    = HMAC-SHA256(kService, "aws4_request")
SecretKey

API 인증키 중 Secret Key를 입력합니다. 네이버 클라우드 플랫폼 홈페이지[마이페이지] > [계정관리] > [인증키관리]에서 확인할 수 있습니다.

Date

YYYYMMDD 포맷으로 입력합니다.

Region

리전 이름을 입력합니다. 호출 도메인 확인에서 확인할 수 있습니다. 리전 이름은 필수 입력값이지만 네이버 클라우드 플랫폼 Object Storage에서는 도메인으로 리전을 구분하기 때문에 리전 이름의 정합성을 확인하지는 않습니다.

서명 생성 (Signature)

서명(Signature)는 서명키(Signing Key)와 서명할 문자열(String to Sign)의 HMAC-SHA256 해시를 계산하여 16진수값으로 표시합니다.

Hex(HMAC-SHA256(<SigningKey>, <StringToSign>))



인증 헤더 생성 (Authorization)

인증 헤더(Authorization)의 구성은 다음과 같습니다.

Authorization: AWS4-HMAC-SHA256 Credential=<AccessKeyID>/<Scope>, SignedHeaders=<SignedHeaders>, Signature=<Signature>
AccessKeyID

API 인증키 중 Access Key ID를 입력합니다. 네이버 클라우드 플랫폼 홈페이지[마이페이지] > [계정관리] > [인증키관리]에서 확인할 수 있습니다.

Scope

Scope 문자열은 다음과 같이 구성됩니다. <Date>/<Region>/s3/aws4_request
- Date : YYYYMMDD 포맷으로 입력합니다.
- Region : 리전 이름을 입력합니다. 호출 도메인 확인에서 확인할 수 있습니다. 리전 이름은 필수 입력값이지만 네이버 클라우드 플랫폼 Object Storage에서는 도메인으로 리전을 구분하기 때문에 리전 이름의 정합성을 확인하지는 않습니다.

SignedHeaders

요청에 포함된 헤더 이름을 나열합니다. 헤더 이름은 모두 소문자로 변환하고 알파벳순으로 정렬합니다. 각 헤더 이름의 구분자로 세미콜론(“;”)을 입력합니다.

host;x-amz-content-sha256;x-amz-date



Signature

서명 생성 (Signature)에서 생성한 서명(Signature)을 입력합니다.

인증 헤더(Authorization) 생성 예 (Java)

public class ObjectStorageSample {
	private static byte[] sign(String stringData, byte[] key) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException {
        byte[] data = stringData.getBytes(CHARSET_NAME);
        Mac e = Mac.getInstance(HMAC_ALGORITHM);
        e.init(new SecretKeySpec(key, HMAC_ALGORITHM));
        return e.doFinal(data);
    }

    private static String hash(String text) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        MessageDigest e = MessageDigest.getInstance(HASH_ALGORITHM);
        e.update(text.getBytes(CHARSET_NAME));
        return Hex.encodeHexString(e.digest());
    }

    private static String getStandardizedQueryParameters(String queryString) throws UnsupportedEncodingException {
        TreeMap<String, String> sortedQueryParameters = new TreeMap<>();
        // sort by key name
        if (queryString != null && !queryString.isEmpty()) {
            String[] queryStringTokens = queryString.split("&");
            for (String field : queryStringTokens) {
                String[] fieldTokens = field.split("=");
                if (fieldTokens.length > 0) {
                    if (fieldTokens.length > 1) {
                        sortedQueryParameters.put(fieldTokens[0], fieldTokens[1]);
                    } else {
                        sortedQueryParameters.put(fieldTokens[0], "");
                    }
                }
            }
        }

        StringBuilder standardizedQueryParametersBuilder = new StringBuilder();
        int count = 0;
        for (String key : sortedQueryParameters.keySet()) {
            if (count > 0) {
                standardizedQueryParametersBuilder.append("&");
            }
            standardizedQueryParametersBuilder.append(key).append("=");

            if (sortedQueryParameters.get(key) != null && !sortedQueryParameters.get(key).isEmpty()) {
                standardizedQueryParametersBuilder.append(URLEncoder.encode(sortedQueryParameters.get(key), CHARSET_NAME));
            }

            count++;
        }
        return standardizedQueryParametersBuilder.toString();
    }

    private static TreeMap<String, String> getSortedHeaders(Header[] headers) {
        TreeMap<String, String> sortedHeaders = new TreeMap<>();
        // sort by header name
        for (Header header : headers) {
            sortedHeaders.put(header.getName(), header.getValue());
        }

        return sortedHeaders;
    }

    private static String getSignedHeaders(TreeMap<String, String> sortedHeaders) {
        StringBuilder signedHeadersBuilder = new StringBuilder();
        for (String headerName : sortedHeaders.keySet()) {
            signedHeadersBuilder.append(headerName.toLowerCase()).append(";");
        }
        return signedHeadersBuilder.toString();
    }

    private static String getStandardizedHeaders(TreeMap<String, String> sortedHeaders) {
        StringBuilder standardizedHeadersBuilder = new StringBuilder();
        for (String headerName : sortedHeaders.keySet()) {
            standardizedHeadersBuilder.append(headerName.toLowerCase()).append(":").append(sortedHeaders.get(headerName)).append("\n");
        }

        return standardizedHeadersBuilder.toString();
    }

    private static String getCanonicalRequest(HttpUriRequest request, String standardizedQueryParameters, String standardizedHeaders, String signedHeaders) {
        StringBuilder canonicalRequestBuilder = new StringBuilder().append(request.getMethod()).append("\n")
            .append(request.getURI().getPath()).append("\n")
            .append(standardizedQueryParameters).append("\n")
            .append(standardizedHeaders).append("\n")
            .append(signedHeaders).append("\n")
            .append(UNSIGNED_PAYLOAD);

        return canonicalRequestBuilder.toString();
    }

    private static String getScope(String datestamp, String regionName) {
        StringBuilder scopeBuilder = new StringBuilder().append(datestamp).append("/")
            .append(regionName).append("/")
            .append(SERVICE_NAME).append("/")
            .append(REQUEST_TYPE);
        return scopeBuilder.toString();
    }

    private static String getStringToSign(String timestamp, String scope, String canonicalRequest) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        StringBuilder stringToSignBuilder = new StringBuilder(AWS_ALGORITHM)
            .append("\n")
            .append(timestamp).append("\n")
            .append(scope).append("\n")
            .append(hash(canonicalRequest));

        return stringToSignBuilder.toString();
    }

    private static String getSignature(String secretKey, String datestamp, String regionName, String stringToSign) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException {
        byte[] kSecret = ("AWS4" + secretKey).getBytes(CHARSET_NAME);
        byte[] kDate = sign(datestamp, kSecret);
        byte[] kRegion = sign(regionName, kDate);
        byte[] kService = sign(SERVICE_NAME, kRegion);
        byte[] signingKey = sign(REQUEST_TYPE, kService);

        return Hex.encodeHexString(sign(stringToSign, signingKey));
    }

    private static String getAuthorization(String accessKey, String scope, String signedHeaders, String signature) {
        String signingCredentials = accessKey + "/" + scope;
        String credential = "Credential=" + signingCredentials;
        String signerHeaders = "SignedHeaders=" + signedHeaders;
        String signatureHeader = "Signature=" + signature;

        StringBuilder authHeaderBuilder = new StringBuilder().append(AWS_ALGORITHM).append(" ")
            .append(credential).append(", ")
            .append(signerHeaders).append(", ")
            .append(signatureHeader);

        return authHeaderBuilder.toString();
    }

    private static void authorization(HttpUriRequest request, String regionName, String accessKey, String secretKey) throws Exception {
        Date now = new Date();
        DATE_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
        TIME_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
        String datestamp = DATE_FORMATTER.format(now);
        String timestamp = TIME_FORMATTER.format(now);

        request.addHeader("X-Amz-Date", timestamp);

        request.addHeader("X-Amz-Content-Sha256", UNSIGNED_PAYLOAD);

        String standardizedQueryParameters = getStandardizedQueryParameters(request.getURI().getQuery());

        TreeMap<String, String> sortedHeaders = getSortedHeaders(request.getAllHeaders());
        String signedHeaders = getSignedHeaders(sortedHeaders);
        String standardizedHeaders = getStandardizedHeaders(sortedHeaders);

        String canonicalRequest = getCanonicalRequest(request, standardizedQueryParameters, standardizedHeaders, signedHeaders);
        System.out.println("> canonicalRequest :");
        System.out.println(canonicalRequest);

        String scope = getScope(datestamp, regionName);

        String stringToSign = getStringToSign(timestamp, scope, canonicalRequest);
        System.out.println("> stringToSign :");
        System.out.println(stringToSign);

        String signature = getSignature(secretKey, datestamp, regionName, stringToSign);

        String authorization = getAuthorization(accessKey, scope, signedHeaders, signature);
        request.addHeader("Authorization", authorization);
    }

    private static void putObject(String bucketName, String objectName, String localFilePath) throws Exception {
        HttpClient httpClient = HttpClientBuilder.create().build();

        HttpPut request = new HttpPut(ENDPOINT + "/" + bucketName + "/" + objectName);
        request.addHeader("Host", request.getURI().getHost());
        request.setEntity(new FileEntity(new File(localFilePath)));

        authorization(request, REGION_NAME, ACCESS_KEY, SECRET_KEY);

        HttpResponse response = httpClient.execute(request);
        System.out.println("Response : " + response.getStatusLine());
    }

    private static void getObject(String bucketName, String objectName, String localFilePath) throws Exception {
        HttpClient httpClient = HttpClientBuilder.create().build();
        HttpGet request = new HttpGet(ENDPOINT + "/" + bucketName + "/" + objectName);
        request.addHeader("Host", request.getURI().getHost());

        authorization(request, REGION_NAME, ACCESS_KEY, SECRET_KEY);

        HttpResponse response = httpClient.execute(request);
        System.out.println("Response : " + response.getStatusLine());

        InputStream is = response.getEntity().getContent();
        File targetFile = new File(localFilePath);
        OutputStream os = new FileOutputStream(targetFile);

        byte[] buffer = new byte[8 * 1024];
        int bytesRead;
        while ((bytesRead = is.read(buffer)) != -1) {
            os.write(buffer, 0, bytesRead);
        }

        is.close();
        os.close();
    }

    private static void listObjects(String bucketName, String queryString) throws Exception {
        HttpClient httpClient = HttpClientBuilder.create().build();
        URI uri = new URI(ENDPOINT + "/" + bucketName + "?" + queryString);
        HttpGet request = new HttpGet(uri);
        request.addHeader("Host", request.getURI().getHost());

        authorization(request, REGION_NAME, ACCESS_KEY, SECRET_KEY);

        HttpResponse response = httpClient.execute(request);
        System.out.println("> Response : " + response.getStatusLine());
        int i;
        InputStream is = response.getEntity().getContent();
        StringBuffer buffer = new StringBuffer();
        byte[] b = new byte[4096];
        while ((i = is.read(b)) != -1) {
            buffer.append(new String(b, 0, i));
        }
        System.out.println(buffer.toString());

    }

    private static final String CHARSET_NAME = "UTF-8";
    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private static final String HASH_ALGORITHM = "SHA-256";
    private static final String AWS_ALGORITHM = "AWS4-HMAC-SHA256";

    private static final String SERVICE_NAME = "s3";
    private static final String REQUEST_TYPE = "aws4_request";

    private static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";

    private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyyMMdd");
    private static final SimpleDateFormat TIME_FORMATTER = new SimpleDateFormat("yyyyMMdd\'T\'HHmmss\'Z\'");

    private static final String REGION_NAME = "kr-standard";
    private static final String ENDPOINT = "https://kr.object.ncloudstorage.com";
    private static final String ACCESS_KEY = "ACCESS_KEY_ID";
    private static final String SECRET_KEY = "SECRET_KEY";

    public static void main(String[] args) throws Exception {
        String bucketName = "sample-bucket";
        String objectName = "sample-object.txt";
        String sourceFilePath = "/tmp/source.txt";
        String targetFilePath = "/tmp/target.txt";

        putObject(bucketName, objectName, sourceFilePath);

        getObject(bucketName, objectName, targetFilePath);

        String queryString = "max-keys=10&delimiter=/";
        listObjects(bucketName, queryString);
    }
}

인증 헤더(Authorization) 생성 예 (Python2.7)

import hashlib
import hmac
import datetime
import requests
import urllib


def get_hash(key, msg):
    return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()


def create_signed_headers(headers):
    signed_headers = []

    for k in sorted(headers):
        signed_headers.append('%s;' % k)

    return ''.join(signed_headers)


def create_standardized_headers(headers):
    signed_headers = []

    for k in sorted(headers):
        signed_headers.append('%s:%s\n' % (k, headers[k]))

    return ''.join(signed_headers)


def create_standardized_query_parameters(request_parameters):
    standardized_query_parameters = []

    if request_parameters:
        for k in sorted(request_parameters):
            standardized_query_parameters.append('%s=%s' % (k, urllib.quote(request_parameters[k], safe='')))

        return '&'.join(standardized_query_parameters)
    else:
        return ''


class ObjectStorageSample:
    def __init__(self):
        self.region = 'kr-standard'
        self.endpoint = 'https://kr.object.ncloudstorage.com'
        self.host = 'kr.object.ncloudstorage.com'
        self.access_key = 'ACCESS_KEY_ID'
        self.secret_key = 'SECRET_KEY'

        self.payload_hash = 'UNSIGNED-PAYLOAD'
        self.hashing_algorithm = 'AWS4-HMAC-SHA256'
        self.service_name = 's3'
        self.request_type = 'aws4_request'

        self.time_format = '%Y%m%dT%H%M%SZ'
        self.date_format = '%Y%m%d'

    def _create_credential_scope(self, date_stamp):
        return date_stamp + '/' + self.region + '/' + self.service_name + '/' + self.request_type

    def _create_canonical_request(self, http_method, request_path, request_parameters, headers):
        standardized_query_parameters = create_standardized_query_parameters(request_parameters)
        standardized_headers = create_standardized_headers(headers)
        signed_headers = create_signed_headers(headers)

        canonical_request = (http_method + '\n' +
                             request_path + '\n' +
                             standardized_query_parameters + '\n' +
                             standardized_headers + '\n' +
                             signed_headers + '\n' +
                             self.payload_hash)

        print('canonical_request:\n%s\n' % canonical_request)
        return canonical_request

    def _create_string_to_sign(self, time_stamp, credential_scope, canonical_request):
        string_to_sign = (self.hashing_algorithm + '\n' +
                          time_stamp + '\n' +
                          credential_scope + '\n' +
                          hashlib.sha256(canonical_request.encode('utf-8')).hexdigest())

        print('string_to_sign:\n%s\n' % string_to_sign)
        return string_to_sign

    def _create_signature_key(self, date_stamp):
        key_date = get_hash(('AWS4' + self.secret_key).encode('utf-8'), date_stamp)
        key_string = get_hash(key_date, self.region)
        key_service = get_hash(key_string, self.service_name)
        key_signing = get_hash(key_service, self.request_type)
        return key_signing

    def _create_authorization_header(self, headers, signature_key, string_to_sign, credential_scope):
        signed_headers = create_signed_headers(headers)
        signature = hmac.new(signature_key, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()

        return (self.hashing_algorithm + ' ' +
                'Credential=' + self.access_key + '/' + credential_scope + ', ' +
                'SignedHeaders=' + signed_headers + ', ' +
                'Signature=' + signature)

    def _sign(self, http_method, request_path, headers, time, request_parameters=None):
        time_stamp = time.strftime(self.time_format)
        date_stamp = time.strftime(self.date_format)

        credential_scope = self._create_credential_scope(date_stamp)
        canonical_request = self._create_canonical_request(http_method, request_path, request_parameters, headers)
        string_to_sign = self._create_string_to_sign(time_stamp, credential_scope, canonical_request)
        signature_key = self._create_signature_key(date_stamp)

        headers['authorization'] = self._create_authorization_header(headers, signature_key, string_to_sign, credential_scope)

    def put_object(self, bucket_name, object_name, source_file_path, request_parameters=None):
        http_method = 'PUT'

        with open(source_file_path) as f:
            time = datetime.datetime.utcnow()
            time_stamp = time.strftime(self.time_format)

            headers = {'x-amz-date': time_stamp,
                       'x-amz-content-sha256': self.payload_hash,
                       'host': self.host}

            request_path = '/%s/%s' % (bucket_name, object_name)

            self._sign(http_method, request_path, headers, time, request_parameters)

            request_url = self.endpoint + request_path
            r = requests.put(request_url, headers=headers, params=request_parameters, data=f.read())

            print('Response code: %d' % r.status_code)

    def get_object(self, bucket_name, object_name, target_file_path, request_parameters=None):
        http_method = 'GET'

        time = datetime.datetime.utcnow()
        time_stamp = time.strftime(self.time_format)

        headers = {'x-amz-date': time_stamp,
                   'x-amz-content-sha256': self.payload_hash,
                   'host': self.host}

        request_path = '/%s/%s' % (bucket_name, object_name)

        self._sign(http_method, request_path, headers, time, request_parameters)

        request_url = self.endpoint + request_path
        r = requests.get(request_url, headers=headers, params=request_parameters, stream=True)

        print('Response code: %d' % r.status_code)

        if r.status_code == 200:
            with open(target_file_path, 'wb') as f:
                f.write(r.content)

    def list_objects(self, bucket_name, request_parameters=None):
        http_method = 'GET'

        time = datetime.datetime.utcnow()
        time_stamp = time.strftime(self.time_format)

        headers = {'x-amz-date': time_stamp,
                   'x-amz-content-sha256': self.payload_hash,
                   'host': self.host}

        request_path = '/%s' % bucket_name

        self._sign(http_method, request_path, headers, time, request_parameters)

        request_url = self.endpoint + request_path
        r = requests.get(request_url, headers=headers, params=request_parameters)

        print('Response code: %d' % r.status_code)
        print('Response content:\n%s' % r.content)


if __name__ == '__main__':
    sample = ObjectStorageSample()
    sample.put_object('sample-bucket', 'sample-object.txt', '/tmp/source.txt')
    sample.get_object('sample-bucket', 'sample-object.txt', '/tmp/target.txt')
    sample.list_objects('sample-bucket', request_parameters={'max-keys': '10', 'delimiter': '/'})