aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/asset/backend_gcs.go
blob: 15fd1f492f6aee07750c46d1897455d8b5e5f529 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// Copyright 2022 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package asset

import (
	"context"
	"fmt"
	"io"
	"net/url"
	"regexp"
	"strings"

	"github.com/google/syzkaller/pkg/debugtracer"
	"github.com/google/syzkaller/pkg/gcs"
)

type cloudStorageBackend struct {
	client gcs.Client
	bucket string
	tracer debugtracer.DebugTracer
}

func makeCloudStorageBackend(bucket string, tracer debugtracer.DebugTracer) (*cloudStorageBackend, error) {
	tracer.Log("created gcs backend for bucket '%s'", bucket)
	client, err := gcs.NewClient(context.Background())
	if err != nil {
		return nil, fmt.Errorf("the call to NewClient failed: %w", err)
	}
	return &cloudStorageBackend{
		client: client,
		bucket: bucket,
		tracer: tracer,
	}, nil
}

// Actual write errors might be hidden, so we wrap the writer here
// to ensure that they get logged.
type writeErrorLogger struct {
	writeCloser io.WriteCloser
	tracer      debugtracer.DebugTracer
}

func (wel *writeErrorLogger) Write(p []byte) (n int, err error) {
	n, err = wel.writeCloser.Write(p)
	if err != nil {
		wel.tracer.Log("cloud storage write error: %s", err)
	}
	return
}

func (wel *writeErrorLogger) Close() error {
	err := wel.writeCloser.Close()
	if err != nil {
		wel.tracer.Log("cloud storage writer close error: %s", err)
	}
	return err
}

func (csb *cloudStorageBackend) upload(req *uploadRequest) (*uploadResponse, error) {
	path := fmt.Sprintf("%s/%s", csb.bucket, req.savePath)
	// Best-effort check only. In the worst case we'll just overwite the file.
	// The alternative would be to add an If-precondition, but it'd require
	// complicated error-during-write handling.
	exists, err := csb.client.FileExists(path)
	if err != nil {
		return nil, err
	}
	if exists {
		return nil, &FileExistsError{req.savePath}
	}
	w, err := csb.client.FileWriter(path, req.contentType, req.contentEncoding)
	csb.tracer.Log("gcs upload: obtained a writer for %s, error %s", path, err)
	if err != nil {
		return nil, err
	}
	return &uploadResponse{
		writer: &writeErrorLogger{
			writeCloser: w,
			tracer:      csb.tracer,
		},
		path: req.savePath,
	}, nil
}

func (csb *cloudStorageBackend) downloadURL(path string, publicURL bool) (string, error) {
	return gcs.GetDownloadURL(fmt.Sprintf("%s/%s", csb.bucket, path), publicURL), nil
}

var allowedDomainsRe = regexp.MustCompile(`^storage\.googleapis\.com|storage\.cloud\.google\.com$`)

func (csb *cloudStorageBackend) getPath(downloadURL string) (string, error) {
	u, err := url.Parse(downloadURL)
	if err != nil {
		return "", fmt.Errorf("failed to parse the URL: %w", err)
	}
	if !allowedDomainsRe.MatchString(u.Host) {
		return "", fmt.Errorf("not allowed host: %s", u.Host)
	}
	prefix := "/" + csb.bucket + "/"
	if !strings.HasPrefix(u.Path, prefix) {
		return "", ErrUnknownBucket
	}
	return u.Path[len(prefix):], nil
}

func (csb *cloudStorageBackend) list() ([]*gcs.Object, error) {
	return csb.client.ListObjects(csb.bucket)
}

func (csb *cloudStorageBackend) remove(path string) error {
	path = fmt.Sprintf("%s/%s", csb.bucket, path)
	err := csb.client.DeleteFile(path)
	if err == gcs.ErrFileNotFound {
		return ErrAssetDoesNotExist
	}
	return err
}