Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GCS SignedURL発行ロジックの追加 #89

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ toolchain go1.22.0

require (
cloud.google.com/go/firestore v1.14.0
cloud.google.com/go/storage v1.38.0
firebase.google.com/go/v4 v4.13.0
github.com/gin-gonic/gin v1.9.1
github.com/go-resty/resty/v2 v2.11.0
Expand All @@ -26,7 +27,6 @@ require (
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.6 // indirect
cloud.google.com/go/longrunning v0.5.5 // indirect
cloud.google.com/go/storage v1.38.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/bytedance/sonic v1.11.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
Expand Down
10 changes: 10 additions & 0 deletions pkg/gcs/folder.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ type FolderRef struct {
bucket *storage.BucketHandle
}

func NewFolderRef(bucket *storage.BucketHandle, basePath string) FolderRef {
if basePath != "" && basePath[len(basePath)-1] != '/' {
basePath += "/"
}
return FolderRef{
basePath: basePath,
bucket: bucket,
}
}

func (f FolderRef) Object(name string) ObjectRef {
return ObjectRef{
objName: f.basePath + name,
Expand Down
55 changes: 49 additions & 6 deletions pkg/gcs/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"cloud.google.com/go/storage"
"context"
"io"
"net/url"
"time"
)

Expand Down Expand Up @@ -33,14 +34,56 @@ func (r ObjectRef) Download(ctx context.Context) ([]byte, error) {
return data, nil
}

func (r ObjectRef) IssueLink() (SignedObjectLink, error) {
url, err := r.bucket.SignedURL(r.objName, &storage.SignedURLOptions{
Method: "GET",
Expires: time.Now().Add(time.Minute * 1),
Scheme: storage.SigningSchemeV4,
// IssueLinkOptions
// ExpiresIn is the duration the link is valid for.
// AuthHeaders are the headers to sign.
// AuthMetaHeaders are the metadata headers to sign. The key will be prefixed with "x-goog-meta-".
// headers starting with "x-goog-meta-" are considered metadata headers, and it will be set as metadata in the object.
// Both AuthHeaders and AuthMetaHeaders are required when using the URL.
// AuthQueries are the query parameters to sign.
type IssueLinkOptions struct {
ExpiresIn time.Duration
AuthHeaders map[string]string
AuthMetaHeaders map[string]string
AuthQueries map[string]string
}

// IssueDownloadSignedURL
// removeAuthQueries will remove the query parameters from the signed URL.
// It will be useful for preventing CSRF attacks.
func (r ObjectRef) IssueDownloadSignedURL(ops IssueLinkOptions, removeAuthQueries bool) (SignedObjectLink, error) {
headers := make([]string, 0, len(ops.AuthHeaders))
for k, v := range ops.AuthHeaders {
headers = append(headers, k+":"+v)
}
for k, v := range ops.AuthMetaHeaders {
headers = append(headers, "x-goog-meta-"+k+":"+v)
}
queries := url.Values{}
for k, v := range ops.AuthQueries {
queries.Set(k, v)
}
signedUrl, err := r.bucket.SignedURL(r.objName, &storage.SignedURLOptions{
Method: "GET",
Expires: time.Now().Add(ops.ExpiresIn),
Headers: headers,
QueryParameters: queries,
Scheme: storage.SigningSchemeV4,
})
if err != nil {
return "", err
}
return SignedObjectLink(url), nil
if removeAuthQueries {
u, err := url.Parse(signedUrl)
if err != nil {
return "", err
}
q := u.Query()
for k := range ops.AuthQueries {
q.Del(k)
}
u.RawQuery = q.Encode()
signedUrl = u.String()
}
return SignedObjectLink(signedUrl), nil
}
102 changes: 102 additions & 0 deletions pkg/gcs/object_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package gcs

import (
"cloud.google.com/go/storage"
"context"
"fmt"
"google.golang.org/api/option"
"io"
"net/http"
"net/url"
"testing"
"time"
"ynufes-mypage-backend/pkg/setting"
)

func TestObjectRef_IssueDownloadSignedURL(t *testing.T) {
ctx := context.Background()
conf := setting.Get()
certPath := conf.Infrastructure.Firebase.JsonCredentialFile
c, err := storage.NewClient(ctx, option.WithCredentialsFile(certPath))
if err != nil {
t.Fatal(err)
}
b := c.Bucket("ynufes-mypage-staging-bucket")
ref := NewFolderRef(b, "some-folder")
uploadRef, err := ref.Upload(ctx, "some-file", []byte("some-content"))
if err != nil {
t.Fatal(err)
return
}
issueOpt := IssueLinkOptions{
ExpiresIn: 5 * time.Second,
AuthHeaders: map[string]string{
"user": "shion-test",
},
AuthMetaHeaders: map[string]string{
"user": "shion-meta",
},
AuthQueries: map[string]string{
"user-query": "shion-query",
},
}
targetUrl, err := uploadRef.IssueDownloadSignedURL(issueOpt, true)
if err != nil {
t.Fatal(err)
}
if err := verifySignedURL(targetUrl, issueOpt, true); err != nil {
t.Fatal(err)
}
}

//func uploadTestFile(
// ctx context.Context,
// bucket *storage.BucketHandle,
// fileName string,
// content []byte,
//) (ObjectRef, error) {
// ref := NewFolderRef(bucket, "some-folder")
// return ref.Upload(ctx, fileName, content)
//}

func verifySignedURL(
target SignedObjectLink,
opt IssueLinkOptions,
addQuery bool,
) error {
targetURL, err := url.Parse(string(target))
if err != nil {
return fmt.Errorf("failed to parse signed URL: %w", err)
}
if addQuery {
q := targetURL.Query()
for k, v := range opt.AuthQueries {
q.Set(k, v)
}
targetURL.RawQuery = q.Encode()
}
fmt.Println("targetURL: ", targetURL.String())
req, err := http.NewRequest("GET", targetURL.String(), nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
for k, v := range opt.AuthHeaders {
req.Header.Set(k, v)
}
for k, v := range opt.AuthMetaHeaders {
req.Header.Set("x-goog-meta-"+k, v)
}
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
payload, _ := io.ReadAll(resp.Body)
if string(payload) != "some-content" {
return fmt.Errorf("unexpected payload: %s", string(payload))
}
return nil
}
Loading