Skip to content

Commit e5a07f0

Browse files
committed
chore(backend): proxy attachments by default
- by default all attachments are proxied via s3 presigned urls - previous no proxy behavior can also be configured - reorganize routes so that not found route can function normally - add new api endpoint to reverse proxy presigned s3 attachments
1 parent eef604a commit e5a07f0

File tree

6 files changed

+155
-18
lines changed

6 files changed

+155
-18
lines changed

measure-backend/measure-go/event/attachment.go

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"io"
66
"mime"
7+
"net/url"
78
"path/filepath"
89
"slices"
910
"time"
@@ -94,12 +95,24 @@ func (a *Attachment) PreSignURL() (err error) {
9495
Credentials: credentials.NewStaticCredentials(config.AttachmentsAccessKey, config.AttachmentsSecretAccessKey, ""),
9596
}
9697

98+
shouldProxy := true
99+
100+
if config.AttachmentOrigin != "" {
101+
shouldProxy = false
102+
}
103+
97104
// if a custom endpoint was set, then most likely,
98-
// we are in local development mode and should force
99-
// path style instead of S3 virual path styles.
105+
// external object store is not native S3 like,
106+
// hence should force path style instead of S3 virtual
107+
// path styles.
100108
if config.AWSEndpoint != "" {
101109
awsConfig.S3ForcePathStyle = aws.Bool(true)
102-
awsConfig.Endpoint = aws.String(config.AttachmentOrigin)
110+
111+
if shouldProxy {
112+
awsConfig.Endpoint = aws.String(config.AWSEndpoint)
113+
} else {
114+
awsConfig.Endpoint = aws.String(config.AttachmentOrigin)
115+
}
103116
}
104117

105118
awsSession := session.Must(session.NewSession(awsConfig))
@@ -115,6 +128,37 @@ func (a *Attachment) PreSignURL() (err error) {
115128
return err
116129
}
117130

131+
if shouldProxy {
132+
endpoint, err := url.JoinPath(config.APIOrigin, "attachments")
133+
if err != nil {
134+
return err
135+
}
136+
137+
proxyUrl, err := url.Parse(endpoint)
138+
if err != nil {
139+
return err
140+
}
141+
142+
parsed, err := url.Parse(urlStr)
143+
if err != nil {
144+
return err
145+
}
146+
147+
// clear the scheme and host of
148+
// presigned URL, because we take interest
149+
// in capturing the presigned URL's path
150+
// and query string only.
151+
parsed.Scheme = ""
152+
parsed.Host = ""
153+
154+
query := proxyUrl.Query()
155+
156+
query.Set("payload", parsed.String())
157+
proxyUrl.RawQuery = query.Encode()
158+
159+
urlStr = proxyUrl.String()
160+
}
161+
118162
a.Location = urlStr
119163

120164
return

measure-backend/measure-go/main.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,18 @@ func main() {
6969
c.String(http.StatusOK, "pong")
7070
})
7171

72-
// Auth routes
72+
// SDK routes
73+
r.PUT("/events", measure.ValidateAPIKey(), measure.PutEvents)
74+
r.PUT("/builds", measure.ValidateAPIKey(), measure.PutBuild)
75+
76+
// Dashboard routes
77+
// Any route below this point will use CORS
7378
r.Use(cors)
79+
80+
// Proxy route
81+
r.GET("/attachments", measure.ProxyAttachment)
82+
83+
// Auth routes
7484
auth := r.Group("/auth")
7585
{
7686
auth.POST("github", measure.SigninGitHub)
@@ -79,13 +89,7 @@ func main() {
7989
auth.DELETE("signout", measure.ValidateRefreshToken(), measure.Signout)
8090
}
8191

82-
// SDK routes
83-
r.PUT("/events", measure.ValidateAPIKey(), measure.PutEvents)
84-
r.PUT("/builds", measure.ValidateAPIKey(), measure.PutBuild)
85-
86-
// Dashboard routes
87-
r.Use(cors).Use(measure.ValidateAccessToken())
88-
apps := r.Group("/apps")
92+
apps := r.Group("/apps", measure.ValidateAccessToken())
8993
{
9094
apps.GET(":id/journey", measure.GetAppJourney)
9195
apps.GET(":id/metrics", measure.GetAppMetrics)
@@ -107,7 +111,7 @@ func main() {
107111
apps.PATCH(":id/settings", measure.UpdateAppSettings)
108112
}
109113

110-
teams := r.Group("/teams")
114+
teams := r.Group("/teams", measure.ValidateAccessToken())
111115
{
112116
teams.POST("", measure.CreateTeam)
113117
teams.GET("", measure.GetTeams)

measure-backend/measure-go/measure/app.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2551,8 +2551,7 @@ func GetCrashDetailCrashes(c *gin.Context) {
25512551
return
25522552
}
25532553

2554-
// generate pre-sign URLs for
2555-
// attachments
2554+
// set appropriate attachment URLs
25562555
for i := range eventExceptions {
25572556
if len(eventExceptions[i].Attachments) > 0 {
25582557
for j := range eventExceptions[i].Attachments {
@@ -3301,8 +3300,7 @@ func GetANRDetailANRs(c *gin.Context) {
33013300
return
33023301
}
33033302

3304-
// generate pre-sign URLs for
3305-
// attachments
3303+
// set appropriate attachment URLs
33063304
for i := range eventANRs {
33073305
if len(eventANRs[i].Attachments) > 0 {
33083306
for j := range eventANRs[i].Attachments {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package measure
2+
3+
import (
4+
"fmt"
5+
"measure-backend/measure-go/server"
6+
"net/http"
7+
"net/http/httputil"
8+
"net/url"
9+
10+
"github.com/gin-gonic/gin"
11+
)
12+
13+
// ProxyAttachment proxies presigned S3 URLs to an S3-like
14+
// server.
15+
//
16+
// We parse the payload from the incoming request's query
17+
// string, then construct a new URL by replacing the S3 origin.
18+
// Next, we create a reverse proxy and configure it to pipe
19+
// response back to the original caller.
20+
//
21+
// The original S3 origin used when constructing the presigned
22+
// URL must match the proxied presigned URL.
23+
func ProxyAttachment(c *gin.Context) {
24+
payload := c.Query("payload")
25+
if payload == "" {
26+
msg := `need payload for proxying to object store`
27+
c.JSON(http.StatusBadRequest, gin.H{
28+
"error": msg,
29+
})
30+
return
31+
}
32+
33+
config := server.Server.Config
34+
presignedUrl := config.AWSEndpoint + payload
35+
36+
parsed, err := url.Parse(presignedUrl)
37+
if err != nil {
38+
msg := "failed to parse reconstructed presigned url"
39+
fmt.Println(msg, err)
40+
c.JSON(http.StatusInternalServerError, gin.H{
41+
"error": msg,
42+
})
43+
return
44+
}
45+
46+
proxy := httputil.NewSingleHostReverseProxy(parsed)
47+
proxy.Director = func(req *http.Request) {
48+
req.URL.Scheme = parsed.Scheme
49+
req.URL.Host = parsed.Host
50+
req.URL.Path = parsed.Path
51+
req.URL.RawQuery = parsed.RawQuery
52+
req.Host = parsed.Host
53+
54+
req.Header = c.Request.Header
55+
56+
fmt.Printf("Attachment proxy target url: %s\n", req.URL.String())
57+
}
58+
59+
proxy.ModifyResponse = func(resp *http.Response) error {
60+
fmt.Printf("Attachment proxy http status: %s\n", resp.Status)
61+
return nil
62+
}
63+
64+
proxy.ServeHTTP(c.Writer, c.Request)
65+
}

measure-backend/measure-go/server/server.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type ServerConfig struct {
4545
AWSEndpoint string
4646
AttachmentOrigin string
4747
SiteOrigin string
48+
APIOrigin string
4849
OAuthGitHubKey string
4950
OAuthGitHubSecret string
5051
OAuthGoogleKey string
@@ -102,14 +103,19 @@ func NewConfig() *ServerConfig {
102103

103104
attachmentOrigin := os.Getenv("ATTACHMENTS_S3_ORIGIN")
104105
if attachmentOrigin == "" {
105-
log.Println("ATTACHMENTS_S3_ORIGIN env var not set, event attachment downloads won't work")
106+
log.Println("ATTACHMENTS_S3_ORIGIN env var not set, event attachment downloads will be proxied")
106107
}
107108

108109
siteOrigin := os.Getenv("SITE_ORIGIN")
109110
if siteOrigin == "" {
110111
log.Fatal("SITE_ORIGIN env var not set. Need for Cross Origin Resource Sharing (CORS) to work.")
111112
}
112113

114+
apiOrigin := os.Getenv("API_ORIGIN")
115+
if apiOrigin == "" {
116+
log.Fatal("API_ORIGIN env var not set. Need for proxying session attachments.")
117+
}
118+
113119
oauthGitHubKey := os.Getenv("OAUTH_GITHUB_KEY")
114120
if oauthGitHubKey == "" {
115121
log.Println("OAUTH_GITHUB_KEY env var is not set, dashboard authn won't work")
@@ -171,6 +177,7 @@ func NewConfig() *ServerConfig {
171177
AWSEndpoint: endpoint,
172178
AttachmentOrigin: attachmentOrigin,
173179
SiteOrigin: siteOrigin,
180+
APIOrigin: apiOrigin,
174181
OAuthGitHubKey: oauthGitHubKey,
175182
OAuthGitHubSecret: oauthGitHubSecret,
176183
OAuthGoogleKey: oauthGoogleKey,

self-host/compose.yml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
# Measure Docker Compose
22
#
33
# Guidelines
4+
#
45
# Follow these guidelines for a consistently maintained
56
# compose file and a pleasurable maintainer experience.
67
#
78
# - Order first-party services at the top followed by
89
# third-party services. Databases and other external
910
# services should appear after measure services.
11+
# - All environment variables MUST be passed using the
12+
# `environment` directive of each service. That's the
13+
# ONLY source of truth.
14+
#
15+
# Tips
16+
#
17+
# - For setting default values for any environment variable
18+
# Specify the default value after the hyphen (-). To set
19+
# empty default value do not specify anything after the
20+
# hyphen. See example below.
21+
#
22+
# ```yaml
23+
# environment:
24+
# - MY_VAR=${MY_VAR:-default_value}
25+
# - EMPTY_MY_VAR=${EMPTY_MY_VAR:-}
26+
# ```
27+
1028

1129
name: measure
1230
services:
@@ -46,12 +64,13 @@ services:
4664
- SYMBOLS_ACCESS_KEY=${SYMBOLS_ACCESS_KEY}
4765
- SYMBOLS_SECRET_ACCESS_KEY=${SYMBOLS_SECRET_ACCESS_KEY}
4866
- SYMBOLICATOR_ORIGIN=${SYMBOLICATOR_ORIGIN}
49-
- ATTACHMENTS_S3_ORIGIN=${ATTACHMENTS_S3_ORIGIN}
67+
- ATTACHMENTS_S3_ORIGIN=${ATTACHMENTS_S3_ORIGIN:-}
5068
- ATTACHMENTS_S3_BUCKET=${ATTACHMENTS_S3_BUCKET}
5169
- ATTACHMENTS_S3_BUCKET_REGION=${ATTACHMENTS_S3_BUCKET_REGION}
5270
- ATTACHMENTS_ACCESS_KEY=${ATTACHMENTS_ACCESS_KEY}
5371
- ATTACHMENTS_SECRET_ACCESS_KEY=${ATTACHMENTS_SECRET_ACCESS_KEY}
5472
- SITE_ORIGIN=${NEXT_PUBLIC_SITE_URL}
73+
- API_ORIGIN=${NEXT_PUBLIC_API_BASE_URL}
5574
- OAUTH_GOOGLE_KEY=${OAUTH_GOOGLE_KEY}
5675
- OAUTH_GITHUB_KEY=${OAUTH_GITHUB_KEY}
5776
- OAUTH_GITHUB_SECRET=${OAUTH_GITHUB_SECRET}

0 commit comments

Comments
 (0)