| // Copyright 2016 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| // This binary contains an App Engine app for perfdata.golang.org | |
| package main | |
| import ( | |
| "context" | |
| "errors" | |
| "fmt" | |
| "log" | |
| "net/http" | |
| "os" | |
| "strings" | |
| "time" | |
| _ "github.com/go-sql-driver/mysql" | |
| "golang.org/x/build/perfdata/app" | |
| "golang.org/x/build/perfdata/db" | |
| "golang.org/x/build/perfdata/fs/gcs" | |
| oauth2 "google.golang.org/api/oauth2/v2" | |
| "google.golang.org/appengine/v2" | |
| aelog "google.golang.org/appengine/v2/log" | |
| "google.golang.org/appengine/v2/user" | |
| ) | |
| // 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. | |
| func connectDB() (*db.DB, error) { | |
| var ( | |
| connectionName = mustGetenv("CLOUDSQL_CONNECTION_NAME") | |
| user = mustGetenv("CLOUDSQL_USER") | |
| password = os.Getenv("CLOUDSQL_PASSWORD") // NOTE: password may be empty | |
| dbName = mustGetenv("CLOUDSQL_DATABASE") | |
| socket = os.Getenv("CLOUDSQL_SOCKET_PREFIX") | |
| ) | |
| // /cloudsql is used on App Engine. | |
| if socket == "" { | |
| socket = "/cloudsql" | |
| } | |
| return db.OpenSQL("mysql", fmt.Sprintf("%s:%s@unix(%s/%s)/%s", user, password, socket, connectionName, dbName)) | |
| } | |
| func mustGetenv(k string) string { | |
| v := os.Getenv(k) | |
| if v == "" { | |
| log.Panicf("%s environment variable not set.", k) | |
| } | |
| return v | |
| } | |
| func auth(w http.ResponseWriter, r *http.Request) (string, error) { | |
| ctx := appengine.NewContext(r) | |
| u, err := reqUser(ctx, r) | |
| if err != nil { | |
| return "", err | |
| } | |
| if u == "" { | |
| url, err := user.LoginURL(ctx, r.URL.String()) | |
| if err != nil { | |
| return "", err | |
| } | |
| http.Redirect(w, r, url, http.StatusFound) | |
| return "", app.ErrResponseWritten | |
| } | |
| return u, nil | |
| } | |
| // reqUser gets the username from the request, trying AE user authentication, AE OAuth authentication, and Google OAuth authentication, in that order. | |
| // If the request contains no authentication, "", nil is returned. | |
| // If the request contains bogus authentication, an error is returned. | |
| func reqUser(ctx context.Context, r *http.Request) (string, error) { | |
| u := user.Current(ctx) | |
| if u != nil { | |
| return u.Email, nil | |
| } | |
| if r.Header.Get("Authorization") == "" { | |
| return "", nil | |
| } | |
| u, err := user.CurrentOAuth(ctx, "https://www.googleapis.com/auth/userinfo.email") | |
| if err == nil { | |
| return u.Email, nil | |
| } | |
| return oauthServiceUser(ctx, r) | |
| } | |
| // oauthServiceUser authenticates the OAuth token from r's headers. | |
| // This is necessary because user.CurrentOAuth does not work if the token is for a service account. | |
| func oauthServiceUser(ctx context.Context, r *http.Request) (string, error) { | |
| tok := r.Header.Get("Authorization") | |
| if !strings.HasPrefix(tok, "Bearer ") { | |
| return "", errors.New("unknown Authorization header") | |
| } | |
| tok = tok[len("Bearer "):] | |
| service, err := oauth2.New(http.DefaultClient) | |
| if err != nil { | |
| return "", err | |
| } | |
| info, err := service.Tokeninfo().AccessToken(tok).Do() | |
| if err != nil { | |
| return "", err | |
| } | |
| if !info.VerifiedEmail || info.Email == "" { | |
| return "", errors.New("token does not contain verified e-mail address") | |
| } | |
| return info.Email, nil | |
| } | |
| // appHandler is the default handler, registered to serve "/". | |
| // It creates a new App instance using the appengine Context and then | |
| // dispatches the request to the App. The environment variable | |
| // GCS_BUCKET must be set in app.yaml with the name of the bucket to | |
| // write to. PERFDATA_VIEW_URL_BASE may be set to the URL that should | |
| // be supplied in /upload responses. | |
| func appHandler(w http.ResponseWriter, r *http.Request) { | |
| ctx := appengine.NewContext(r) | |
| // App Engine does not return a context with a deadline set, | |
| // even though the request does have a deadline. urlfetch uses | |
| // a 5s default timeout if the context does not have a | |
| // deadline, so explicitly set a deadline to match the App | |
| // Engine timeout. | |
| ctx, cancel := context.WithTimeout(ctx, 60*time.Second) | |
| defer cancel() | |
| // GCS clients need to be constructed with an AppEngine | |
| // context, so we can't actually make the App until the | |
| // request comes in. | |
| // TODO(quentin): Figure out if there's a way to construct the | |
| // app and clients once, in init(), instead of on every request. | |
| db, err := connectDB() | |
| if err != nil { | |
| aelog.Errorf(ctx, "connectDB: %v", err) | |
| http.Error(w, err.Error(), 500) | |
| return | |
| } | |
| defer db.Close() | |
| fs, err := gcs.NewFS(ctx, mustGetenv("GCS_BUCKET")) | |
| if err != nil { | |
| aelog.Errorf(ctx, "gcs.NewFS: %v", err) | |
| http.Error(w, err.Error(), 500) | |
| return | |
| } | |
| mux := http.NewServeMux() | |
| app := &app.App{DB: db, FS: fs, Auth: auth, ViewURLBase: os.Getenv("PERFDATA_VIEW_URL_BASE")} | |
| app.RegisterOnMux(mux) | |
| mux.ServeHTTP(w, r) | |
| } | |
| func main() { | |
| http.HandleFunc("/", appHandler) | |
| appengine.Main() | |
| } |