github.com/jgbaldwinbrown/perf@v0.1.1/storage/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  	"errors"
    10  	"fmt"
    11  	"log"
    12  	"net/http"
    13  	"os"
    14  	"strings"
    15  	"time"
    16  
    17  	_ "github.com/go-sql-driver/mysql"
    18  	"golang.org/x/net/context"
    19  	"golang.org/x/perf/storage/app"
    20  	"golang.org/x/perf/storage/db"
    21  	"golang.org/x/perf/storage/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  }