Skip to content

Commit

Permalink
chore(backend): proxy attachments by default
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
detj committed Aug 14, 2024
1 parent eef604a commit e5a07f0
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 18 deletions.
50 changes: 47 additions & 3 deletions measure-backend/measure-go/event/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"io"
"mime"
"net/url"
"path/filepath"
"slices"
"time"
Expand Down Expand Up @@ -94,12 +95,24 @@ func (a *Attachment) PreSignURL() (err error) {
Credentials: credentials.NewStaticCredentials(config.AttachmentsAccessKey, config.AttachmentsSecretAccessKey, ""),
}

shouldProxy := true

if config.AttachmentOrigin != "" {
shouldProxy = false
}

// if a custom endpoint was set, then most likely,
// we are in local development mode and should force
// path style instead of S3 virual path styles.
// external object store is not native S3 like,
// hence should force path style instead of S3 virtual
// path styles.
if config.AWSEndpoint != "" {
awsConfig.S3ForcePathStyle = aws.Bool(true)
awsConfig.Endpoint = aws.String(config.AttachmentOrigin)

if shouldProxy {
awsConfig.Endpoint = aws.String(config.AWSEndpoint)
} else {
awsConfig.Endpoint = aws.String(config.AttachmentOrigin)
}
}

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

if shouldProxy {
endpoint, err := url.JoinPath(config.APIOrigin, "attachments")
if err != nil {
return err
}

proxyUrl, err := url.Parse(endpoint)
if err != nil {
return err
}

parsed, err := url.Parse(urlStr)
if err != nil {
return err
}

// clear the scheme and host of
// presigned URL, because we take interest
// in capturing the presigned URL's path
// and query string only.
parsed.Scheme = ""
parsed.Host = ""

query := proxyUrl.Query()

query.Set("payload", parsed.String())
proxyUrl.RawQuery = query.Encode()

urlStr = proxyUrl.String()
}

a.Location = urlStr

return
Expand Down
22 changes: 13 additions & 9 deletions measure-backend/measure-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,18 @@ func main() {
c.String(http.StatusOK, "pong")
})

// Auth routes
// SDK routes
r.PUT("/events", measure.ValidateAPIKey(), measure.PutEvents)
r.PUT("/builds", measure.ValidateAPIKey(), measure.PutBuild)

// Dashboard routes
// Any route below this point will use CORS
r.Use(cors)

// Proxy route
r.GET("/attachments", measure.ProxyAttachment)

// Auth routes
auth := r.Group("/auth")
{
auth.POST("github", measure.SigninGitHub)
Expand All @@ -79,13 +89,7 @@ func main() {
auth.DELETE("signout", measure.ValidateRefreshToken(), measure.Signout)
}

// SDK routes
r.PUT("/events", measure.ValidateAPIKey(), measure.PutEvents)
r.PUT("/builds", measure.ValidateAPIKey(), measure.PutBuild)

// Dashboard routes
r.Use(cors).Use(measure.ValidateAccessToken())
apps := r.Group("/apps")
apps := r.Group("/apps", measure.ValidateAccessToken())
{
apps.GET(":id/journey", measure.GetAppJourney)
apps.GET(":id/metrics", measure.GetAppMetrics)
Expand All @@ -107,7 +111,7 @@ func main() {
apps.PATCH(":id/settings", measure.UpdateAppSettings)
}

teams := r.Group("/teams")
teams := r.Group("/teams", measure.ValidateAccessToken())
{
teams.POST("", measure.CreateTeam)
teams.GET("", measure.GetTeams)
Expand Down
6 changes: 2 additions & 4 deletions measure-backend/measure-go/measure/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2551,8 +2551,7 @@ func GetCrashDetailCrashes(c *gin.Context) {
return
}

// generate pre-sign URLs for
// attachments
// set appropriate attachment URLs
for i := range eventExceptions {
if len(eventExceptions[i].Attachments) > 0 {
for j := range eventExceptions[i].Attachments {
Expand Down Expand Up @@ -3301,8 +3300,7 @@ func GetANRDetailANRs(c *gin.Context) {
return
}

// generate pre-sign URLs for
// attachments
// set appropriate attachment URLs
for i := range eventANRs {
if len(eventANRs[i].Attachments) > 0 {
for j := range eventANRs[i].Attachments {
Expand Down
65 changes: 65 additions & 0 deletions measure-backend/measure-go/measure/attachment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package measure

import (
"fmt"
"measure-backend/measure-go/server"
"net/http"
"net/http/httputil"
"net/url"

"github.com/gin-gonic/gin"
)

// ProxyAttachment proxies presigned S3 URLs to an S3-like
// server.
//
// We parse the payload from the incoming request's query
// string, then construct a new URL by replacing the S3 origin.
// Next, we create a reverse proxy and configure it to pipe
// response back to the original caller.
//
// The original S3 origin used when constructing the presigned
// URL must match the proxied presigned URL.
func ProxyAttachment(c *gin.Context) {
payload := c.Query("payload")
if payload == "" {
msg := `need payload for proxying to object store`
c.JSON(http.StatusBadRequest, gin.H{
"error": msg,
})
return
}

config := server.Server.Config
presignedUrl := config.AWSEndpoint + payload

parsed, err := url.Parse(presignedUrl)
if err != nil {
msg := "failed to parse reconstructed presigned url"
fmt.Println(msg, err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": msg,
})
return
}

proxy := httputil.NewSingleHostReverseProxy(parsed)
proxy.Director = func(req *http.Request) {
req.URL.Scheme = parsed.Scheme
req.URL.Host = parsed.Host
req.URL.Path = parsed.Path
req.URL.RawQuery = parsed.RawQuery
req.Host = parsed.Host

req.Header = c.Request.Header

fmt.Printf("Attachment proxy target url: %s\n", req.URL.String())
}

proxy.ModifyResponse = func(resp *http.Response) error {
fmt.Printf("Attachment proxy http status: %s\n", resp.Status)
return nil
}

proxy.ServeHTTP(c.Writer, c.Request)
}
9 changes: 8 additions & 1 deletion measure-backend/measure-go/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type ServerConfig struct {
AWSEndpoint string
AttachmentOrigin string
SiteOrigin string
APIOrigin string
OAuthGitHubKey string
OAuthGitHubSecret string
OAuthGoogleKey string
Expand Down Expand Up @@ -102,14 +103,19 @@ func NewConfig() *ServerConfig {

attachmentOrigin := os.Getenv("ATTACHMENTS_S3_ORIGIN")
if attachmentOrigin == "" {
log.Println("ATTACHMENTS_S3_ORIGIN env var not set, event attachment downloads won't work")
log.Println("ATTACHMENTS_S3_ORIGIN env var not set, event attachment downloads will be proxied")
}

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

apiOrigin := os.Getenv("API_ORIGIN")
if apiOrigin == "" {
log.Fatal("API_ORIGIN env var not set. Need for proxying session attachments.")
}

oauthGitHubKey := os.Getenv("OAUTH_GITHUB_KEY")
if oauthGitHubKey == "" {
log.Println("OAUTH_GITHUB_KEY env var is not set, dashboard authn won't work")
Expand Down Expand Up @@ -171,6 +177,7 @@ func NewConfig() *ServerConfig {
AWSEndpoint: endpoint,
AttachmentOrigin: attachmentOrigin,
SiteOrigin: siteOrigin,
APIOrigin: apiOrigin,
OAuthGitHubKey: oauthGitHubKey,
OAuthGitHubSecret: oauthGitHubSecret,
OAuthGoogleKey: oauthGoogleKey,
Expand Down
21 changes: 20 additions & 1 deletion self-host/compose.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
# Measure Docker Compose
#
# Guidelines
#
# Follow these guidelines for a consistently maintained
# compose file and a pleasurable maintainer experience.
#
# - Order first-party services at the top followed by
# third-party services. Databases and other external
# services should appear after measure services.
# - All environment variables MUST be passed using the
# `environment` directive of each service. That's the
# ONLY source of truth.
#
# Tips
#
# - For setting default values for any environment variable
# Specify the default value after the hyphen (-). To set
# empty default value do not specify anything after the
# hyphen. See example below.
#
# ```yaml
# environment:
# - MY_VAR=${MY_VAR:-default_value}
# - EMPTY_MY_VAR=${EMPTY_MY_VAR:-}
# ```


name: measure
services:
Expand Down Expand Up @@ -46,12 +64,13 @@ services:
- SYMBOLS_ACCESS_KEY=${SYMBOLS_ACCESS_KEY}
- SYMBOLS_SECRET_ACCESS_KEY=${SYMBOLS_SECRET_ACCESS_KEY}
- SYMBOLICATOR_ORIGIN=${SYMBOLICATOR_ORIGIN}
- ATTACHMENTS_S3_ORIGIN=${ATTACHMENTS_S3_ORIGIN}
- ATTACHMENTS_S3_ORIGIN=${ATTACHMENTS_S3_ORIGIN:-}
- ATTACHMENTS_S3_BUCKET=${ATTACHMENTS_S3_BUCKET}
- ATTACHMENTS_S3_BUCKET_REGION=${ATTACHMENTS_S3_BUCKET_REGION}
- ATTACHMENTS_ACCESS_KEY=${ATTACHMENTS_ACCESS_KEY}
- ATTACHMENTS_SECRET_ACCESS_KEY=${ATTACHMENTS_SECRET_ACCESS_KEY}
- SITE_ORIGIN=${NEXT_PUBLIC_SITE_URL}
- API_ORIGIN=${NEXT_PUBLIC_API_BASE_URL}
- OAUTH_GOOGLE_KEY=${OAUTH_GOOGLE_KEY}
- OAUTH_GITHUB_KEY=${OAUTH_GITHUB_KEY}
- OAUTH_GITHUB_SECRET=${OAUTH_GITHUB_SECRET}
Expand Down

0 comments on commit e5a07f0

Please sign in to comment.