393 lines
9.0 KiB
Go
393 lines
9.0 KiB
Go
package s3
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
HMAC_ALGORITHM = "HmacSHA256"
|
|
AWS_ALGORITHM = "AWS4-HMAC-SHA256"
|
|
SERVICE_NAME = "s3"
|
|
REQUEST_TYPE = "aws4_request"
|
|
UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD"
|
|
REGION_NAME = "kr-standard"
|
|
ENDPOINT = "https://kr.object.ncloudstorage.com"
|
|
)
|
|
|
|
type sortedHeader map[string]string
|
|
|
|
var errAccessKeyIsMissing = errors.New("NCLOUD_ACCESS_KEY is missing")
|
|
var errSecretKeyIsMissing = errors.New("NCLOUD_SECRET_KEY is missing")
|
|
var errRegionIsMissing = errors.New("NCLOUD_REGION is missing")
|
|
|
|
func sortVersions(versions []string) []string {
|
|
sort.Slice(versions, func(i, j int) bool {
|
|
leftnum := 0
|
|
for _, iv := range strings.Split(versions[i], ".") {
|
|
n, _ := strconv.Atoi(iv)
|
|
leftnum += leftnum<<8 + n
|
|
}
|
|
|
|
rightnum := 0
|
|
for _, iv := range strings.Split(versions[j], ".") {
|
|
n, _ := strconv.Atoi(iv)
|
|
rightnum += rightnum<<8 + n
|
|
}
|
|
|
|
return leftnum < rightnum
|
|
})
|
|
return versions
|
|
}
|
|
|
|
func sign(data string, key []byte) []byte {
|
|
mac := hmac.New(sha256.New, key)
|
|
mac.Write([]byte(data))
|
|
return mac.Sum(nil)
|
|
}
|
|
|
|
func hash(text string) string {
|
|
s256 := sha256.New()
|
|
s256.Write([]byte(text))
|
|
return hex.EncodeToString(s256.Sum(nil))
|
|
}
|
|
|
|
func getStandardizedQueryParameters(query url.Values) string {
|
|
return query.Encode()
|
|
}
|
|
|
|
func getSignedHeaders(header http.Header) string {
|
|
keys := make([]string, 0, len(header))
|
|
for k := range header {
|
|
keys = append(keys, strings.ToLower(k))
|
|
}
|
|
sort.Strings(keys)
|
|
return strings.Join(keys, ";") + ";"
|
|
}
|
|
|
|
func getStandardizedHeaders(header http.Header) string {
|
|
keys := make([]string, 0, len(header))
|
|
for k := range header {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
standardHeaders := make([]string, 0, len(header))
|
|
for _, k := range keys {
|
|
standardHeaders = append(standardHeaders, fmt.Sprintf("%s:%s", strings.ToLower(k), header.Get(k)))
|
|
}
|
|
return strings.Join(standardHeaders, "\n") + "\n"
|
|
}
|
|
|
|
func getCanonicalRequest(req *http.Request, standardizedQueryParam string, standardHeaders string, signedHeader string) string {
|
|
return strings.Join([]string{
|
|
req.Method,
|
|
req.URL.Path,
|
|
standardizedQueryParam,
|
|
standardHeaders,
|
|
signedHeader,
|
|
UNSIGNED_PAYLOAD,
|
|
}, "\n")
|
|
}
|
|
|
|
func getScope(datestamp string, regionName string) string {
|
|
return strings.Join([]string{
|
|
datestamp,
|
|
regionName, // "kr-standard"
|
|
SERVICE_NAME,
|
|
REQUEST_TYPE,
|
|
}, "/")
|
|
}
|
|
|
|
func getStringToSign(timestamp string, scope string, canonicalReq string) string {
|
|
return strings.Join([]string{
|
|
AWS_ALGORITHM, // AWS_ALGORITHM
|
|
timestamp,
|
|
scope,
|
|
hash(canonicalReq),
|
|
}, "\n")
|
|
}
|
|
|
|
func getSignature(secretKey string, datestamp string, regionName string, stringToSign string) string {
|
|
kSecret := []byte("AWS4" + secretKey)
|
|
kDate := sign(datestamp, kSecret)
|
|
kRegion := sign(regionName, kDate)
|
|
kService := sign(SERVICE_NAME, kRegion)
|
|
signingKey := sign(REQUEST_TYPE, kService)
|
|
|
|
return hex.EncodeToString(sign(stringToSign, signingKey))
|
|
}
|
|
|
|
func getAuthorization(accessKey string, scope string, signedHeader string, signature string) string {
|
|
signingCredentials := accessKey + "/" + scope
|
|
credential := "Credential=" + signingCredentials
|
|
signerHeaders := "SignedHeaders=" + signedHeader
|
|
signatureHeader := "Signature=" + signature
|
|
|
|
return fmt.Sprintf("%s %s, %s, %s", AWS_ALGORITHM, credential, signerHeaders, signatureHeader)
|
|
}
|
|
|
|
func (s S3) addAuthorizationHeader(req *http.Request) {
|
|
req.Header.Add("host", req.Host)
|
|
|
|
now := time.Now().UTC()
|
|
datestamp := now.Format("20060102")
|
|
timestamp := now.Format("20060102T150405Z")
|
|
req.Header.Add("x-amz-date", timestamp)
|
|
req.Header.Add("x-amz-content-sha256", UNSIGNED_PAYLOAD)
|
|
|
|
standardizedQueryParameters := getStandardizedQueryParameters(req.URL.Query())
|
|
signedHeaders := getSignedHeaders(req.Header)
|
|
standardizedHeaders := getStandardizedHeaders(req.Header)
|
|
canonicalRequest := getCanonicalRequest(req, standardizedQueryParameters, standardizedHeaders, signedHeaders)
|
|
scope := getScope(datestamp, s.regionName)
|
|
stringToSign := getStringToSign(timestamp, scope, canonicalRequest)
|
|
signature := getSignature(s.secretKey, datestamp, s.regionName, stringToSign)
|
|
authorization := getAuthorization(s.accessKey, scope, signedHeaders, signature)
|
|
req.Header.Add("Authorization", authorization)
|
|
}
|
|
|
|
type S3 struct {
|
|
accessKey string
|
|
secretKey string
|
|
regionName string
|
|
}
|
|
|
|
func NewNCloud() (S3, error) {
|
|
accessKey := os.Getenv("NCLOUD_ACCESS_KEY")
|
|
if len(accessKey) == 0 {
|
|
return S3{}, errAccessKeyIsMissing
|
|
}
|
|
secretKey := os.Getenv("NCLOUD_SECRET_KEY")
|
|
if len(secretKey) == 0 {
|
|
return S3{}, errSecretKeyIsMissing
|
|
}
|
|
region := os.Getenv("NCLOUD_REGION")
|
|
if len(region) == 0 {
|
|
return S3{}, errRegionIsMissing
|
|
}
|
|
return New(accessKey, secretKey, region), nil
|
|
}
|
|
|
|
func New(accessKey string, secretKey string, regionName string) S3 {
|
|
return S3{
|
|
accessKey: accessKey,
|
|
secretKey: secretKey,
|
|
regionName: regionName,
|
|
}
|
|
}
|
|
|
|
func (s S3) MakeGetObjectRequest(objectURL string) (*http.Request, error) {
|
|
req, err := http.NewRequest("GET", objectURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.addAuthorizationHeader(req)
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (s S3) makeGetItemsRequest(prefixURL string, delimiter string) (*http.Request, error) {
|
|
u, err := url.Parse(prefixURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
endpoint := u.Host
|
|
relpath := strings.TrimLeft(u.Path, "/")
|
|
ns := strings.SplitN(relpath, "/", 2)
|
|
bucket := ns[0]
|
|
prefix := ""
|
|
if len(ns) > 1 {
|
|
prefix = ns[1]
|
|
}
|
|
|
|
var completeurl string
|
|
if len(delimiter) > 0 {
|
|
completeurl = fmt.Sprintf("%s://%s/%s?prefix=%s&delimiter=%s", u.Scheme, endpoint, bucket, prefix, delimiter)
|
|
} else {
|
|
completeurl = fmt.Sprintf("%s://%s/%s?prefix=%s", u.Scheme, endpoint, bucket, prefix)
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", completeurl, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.addAuthorizationHeader(req)
|
|
|
|
return req, nil
|
|
}
|
|
|
|
type FileMeta struct {
|
|
Key string
|
|
LastModified time.Time
|
|
}
|
|
|
|
type listBucketResult struct {
|
|
Name string
|
|
Prefix string
|
|
Marker string
|
|
MaxKeys int
|
|
Delimiter string
|
|
IsTruncated bool
|
|
CommonPrefixes []struct {
|
|
Prefix string
|
|
}
|
|
Contents []FileMeta
|
|
}
|
|
|
|
func (s S3) ListFiles(prefixURL string) ([]FileMeta, error) {
|
|
req, err := s.makeGetItemsRequest(prefixURL, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result listBucketResult
|
|
err = xml.Unmarshal(body, &result)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var out []FileMeta
|
|
for _, c := range result.Contents {
|
|
if !strings.HasSuffix(c.Key, "/") {
|
|
c.Key = prefixURL + "/" + path.Base(c.Key)
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s S3) ListFolders(prefixURL string) ([]string, error) {
|
|
req, err := s.makeGetItemsRequest(prefixURL, "/")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result listBucketResult
|
|
err = xml.Unmarshal(body, &result)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
output := make([]string, 0, len(result.CommonPrefixes))
|
|
for _, prefix := range result.CommonPrefixes {
|
|
output = append(output, strings.TrimRight(prefix.Prefix, "/"))
|
|
}
|
|
|
|
return sortVersions(output), nil
|
|
}
|
|
|
|
func (s S3) ReadFile(url string) (io.ReadCloser, error) {
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.addAuthorizationHeader(req)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Body, nil
|
|
}
|
|
|
|
func (s S3) DownloadFile(url string, outputFile string) error {
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.addAuthorizationHeader(req)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Create the file
|
|
out, err := os.Create(outputFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
// Write the body to file
|
|
_, err = io.Copy(out, resp.Body)
|
|
return err
|
|
}
|
|
|
|
func (s S3) UploadFile(url string, content []byte, publicRead bool) error {
|
|
req, err := http.NewRequest("PUT", url, bytes.NewReader(content))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.addAuthorizationHeader(req)
|
|
if publicRead {
|
|
req.Header.Add("x-amz-acl", "public-read")
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode == http.StatusOK {
|
|
return nil
|
|
}
|
|
return io.EOF
|
|
}
|
|
|
|
// https://api.ncloud-docs.com/docs/storage-objectstorage-putobjectacl
|
|
func (s S3) SetObjectACL(url string, acl string) error {
|
|
url += "?acl=" + acl
|
|
req, err := http.NewRequest("PUT", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.addAuthorizationHeader(req)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode == http.StatusOK {
|
|
return nil
|
|
}
|
|
return io.EOF
|
|
}
|