golang.org/x/build@v0.0.0-20240506185731-218518f32b70/perfdata/appengine/app.go (about) 1 // Copyright 2016 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // This binary contains an App Engine app for perfdata.golang.org 6 package main 7 8 import ( 9 "context" 10 "errors" 11 "fmt" 12 "log" 13 "net/http" 14 "os" 15 "strings" 16 "time" 17 18 _ "github.com/go-sql-driver/mysql" 19 "golang.org/x/build/perfdata/app" 20 "golang.org/x/build/perfdata/db" 21 "golang.org/x/build/perfdata/fs/gcs" 22 oauth2 "google.golang.org/api/oauth2/v2" 23 "google.golang.org/appengine" 24 aelog "google.golang.org/appengine/log" 25 "google.golang.org/appengine/user" 26 ) 27 28 // connectDB returns a DB initialized from the environment variables set in app.yaml. CLOUDSQL_CONNECTION_NAME, CLOUDSQL_USER, and CLOUDSQL_DATABASE must be set to point to the Cloud SQL instance. CLOUDSQL_PASSWORD can be set if needed. 29 func connectDB() (*db.DB, error) { 30 var ( 31 connectionName = mustGetenv("CLOUDSQL_CONNECTION_NAME") 32 user = mustGetenv("CLOUDSQL_USER") 33 password = os.Getenv("CLOUDSQL_PASSWORD") // NOTE: password may be empty 34 dbName = mustGetenv("CLOUDSQL_DATABASE") 35 socket = os.Getenv("CLOUDSQL_SOCKET_PREFIX") 36 ) 37 38 // /cloudsql is used on App Engine. 39 if socket == "" { 40 socket = "/cloudsql" 41 } 42 43 return db.OpenSQL("mysql", fmt.Sprintf("%s:%s@unix(%s/%s)/%s", user, password, socket, connectionName, dbName)) 44 } 45 46 func mustGetenv(k string) string { 47 v := os.Getenv(k) 48 if v == "" { 49 log.Panicf("%s environment variable not set.", k) 50 } 51 return v 52 } 53 54 func auth(w http.ResponseWriter, r *http.Request) (string, error) { 55 ctx := appengine.NewContext(r) 56 u, err := reqUser(ctx, r) 57 if err != nil { 58 return "", err 59 } 60 if u == "" { 61 url, err := user.LoginURL(ctx, r.URL.String()) 62 if err != nil { 63 return "", err 64 } 65 http.Redirect(w, r, url, http.StatusFound) 66 return "", app.ErrResponseWritten 67 } 68 return u, nil 69 } 70 71 // reqUser gets the username from the request, trying AE user authentication, AE OAuth authentication, and Google OAuth authentication, in that order. 72 // If the request contains no authentication, "", nil is returned. 73 // If the request contains bogus authentication, an error is returned. 74 func reqUser(ctx context.Context, r *http.Request) (string, error) { 75 u := user.Current(ctx) 76 if u != nil { 77 return u.Email, nil 78 } 79 if r.Header.Get("Authorization") == "" { 80 return "", nil 81 } 82 u, err := user.CurrentOAuth(ctx, "https://www.googleapis.com/auth/userinfo.email") 83 if err == nil { 84 return u.Email, nil 85 } 86 return oauthServiceUser(ctx, r) 87 } 88 89 // oauthServiceUser authenticates the OAuth token from r's headers. 90 // This is necessary because user.CurrentOAuth does not work if the token is for a service account. 91 func oauthServiceUser(ctx context.Context, r *http.Request) (string, error) { 92 tok := r.Header.Get("Authorization") 93 if !strings.HasPrefix(tok, "Bearer ") { 94 return "", errors.New("unknown Authorization header") 95 } 96 tok = tok[len("Bearer "):] 97 98 service, err := oauth2.New(http.DefaultClient) 99 if err != nil { 100 return "", err 101 } 102 info, err := service.Tokeninfo().AccessToken(tok).Do() 103 if err != nil { 104 return "", err 105 } 106 107 if !info.VerifiedEmail || info.Email == "" { 108 return "", errors.New("token does not contain verified e-mail address") 109 } 110 return info.Email, nil 111 } 112 113 // appHandler is the default handler, registered to serve "/". 114 // It creates a new App instance using the appengine Context and then 115 // dispatches the request to the App. The environment variable 116 // GCS_BUCKET must be set in app.yaml with the name of the bucket to 117 // write to. PERFDATA_VIEW_URL_BASE may be set to the URL that should 118 // be supplied in /upload responses. 119 func appHandler(w http.ResponseWriter, r *http.Request) { 120 ctx := appengine.NewContext(r) 121 // App Engine does not return a context with a deadline set, 122 // even though the request does have a deadline. urlfetch uses 123 // a 5s default timeout if the context does not have a 124 // deadline, so explicitly set a deadline to match the App 125 // Engine timeout. 126 ctx, cancel := context.WithTimeout(ctx, 60*time.Second) 127 defer cancel() 128 // GCS clients need to be constructed with an AppEngine 129 // context, so we can't actually make the App until the 130 // request comes in. 131 // TODO(quentin): Figure out if there's a way to construct the 132 // app and clients once, in init(), instead of on every request. 133 db, err := connectDB() 134 if err != nil { 135 aelog.Errorf(ctx, "connectDB: %v", err) 136 http.Error(w, err.Error(), 500) 137 return 138 } 139 defer db.Close() 140 141 fs, err := gcs.NewFS(ctx, mustGetenv("GCS_BUCKET")) 142 if err != nil { 143 aelog.Errorf(ctx, "gcs.NewFS: %v", err) 144 http.Error(w, err.Error(), 500) 145 return 146 } 147 mux := http.NewServeMux() 148 app := &app.App{DB: db, FS: fs, Auth: auth, ViewURLBase: os.Getenv("PERFDATA_VIEW_URL_BASE")} 149 app.RegisterOnMux(mux) 150 mux.ServeHTTP(w, r) 151 } 152 153 func main() { 154 http.HandleFunc("/", appHandler) 155 appengine.Main() 156 }