Merge pull request #362 from shurcooL/IsValidRemotePath-domain

gosrc: Accept domain.root in IsValidRemotePath.
diff --git a/gddo-server/assets/templates/common.html b/gddo-server/assets/templates/common.html
index e3376c0..c17b27f 100644
--- a/gddo-server/assets/templates/common.html
+++ b/gddo-server/assets/templates/common.html
@@ -88,7 +88,7 @@
 
 {{define "Bootstrap.css"}}<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet">{{end}}
 {{define "Bootstrap.js"}}<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>{{end}}
-{{define "jQuery"}}<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>{{end}}
+{{define "jQuery"}}<script src="//code.jquery.com/jquery-2.0.3.min.js"></script>{{end}}
 
 {{define "FlashMessages"}}{{range .}}
   {{if eq .ID "redir"}}{{if eq (len .Args) 1}}<div class="alert alert-warning">Redirected from {{index .Args 0}}.</div>{{end}}
diff --git a/gddo-server/client.go b/gddo-server/client.go
index 96ad2af..75b7c6f 100644
--- a/gddo-server/client.go
+++ b/gddo-server/client.go
@@ -11,10 +11,11 @@
 
 import (
 	"flag"
-	"log"
 	"net"
 	"net/http"
 	"time"
+
+	"github.com/golang/gddo/httputil"
 )
 
 var (
@@ -22,57 +23,19 @@
 	requestTimeout = flag.Duration("request_timeout", 20*time.Second, "Time out for roundtripping an HTTP request.")
 )
 
-type timeoutConn struct {
-	net.Conn
-}
-
-func (c timeoutConn) Read(p []byte) (int, error) {
-	n, err := c.Conn.Read(p)
-	c.Conn.SetReadDeadline(time.Time{})
-	return n, err
-}
-
-func timeoutDial(network, addr string) (net.Conn, error) {
-	c, err := net.DialTimeout(network, addr, *dialTimeout)
-	if err != nil {
-		return c, err
+func newHTTPClient() *http.Client {
+	return &http.Client{
+		Transport: httputil.NewAuthTransport(
+			&http.Transport{
+				Proxy: http.ProxyFromEnvironment,
+				Dial: (&net.Dialer{
+					Timeout:   *dialTimeout,
+					KeepAlive: *requestTimeout / 2,
+				}).Dial,
+				ResponseHeaderTimeout: *requestTimeout / 2,
+				TLSHandshakeTimeout:   *requestTimeout / 2,
+			},
+		),
+		Timeout: *requestTimeout,
 	}
-	// The net/http transport CancelRequest feature does not work until after
-	// the TLS handshake is complete. To help catch hangs during the TLS
-	// handshake, we set a deadline on the connection here and clear the
-	// deadline when the first read on the connection completes. This is not
-	// perfect, but it does catch the case where the server accepts and ignores
-	// a connection.
-	c.SetDeadline(time.Now().Add(*requestTimeout))
-	return timeoutConn{c}, nil
 }
-
-type transport struct {
-	t http.Transport
-}
-
-func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
-	timer := time.AfterFunc(*requestTimeout, func() {
-		t.t.CancelRequest(req)
-		log.Printf("Canceled request for %s", req.URL)
-	})
-	defer timer.Stop()
-	if req.URL.Host == "api.github.com" && gitHubCredentials != "" {
-		if req.URL.RawQuery == "" {
-			req.URL.RawQuery = gitHubCredentials
-		} else {
-			req.URL.RawQuery += "&" + gitHubCredentials
-		}
-	}
-	if userAgent != "" {
-		req.Header.Set("User-Agent", userAgent)
-	}
-	return t.t.RoundTrip(req)
-}
-
-var httpClient = &http.Client{Transport: &transport{
-	t: http.Transport{
-		Proxy: http.ProxyFromEnvironment,
-		Dial:  timeoutDial,
-		ResponseHeaderTimeout: *requestTimeout / 2,
-	}}}
diff --git a/gddo-server/config.go.template b/gddo-server/config.go.template
deleted file mode 100644
index efea18b..0000000
--- a/gddo-server/config.go.template
+++ /dev/null
@@ -1,7 +0,0 @@
-package main
-
-func init() {
-	// Register an application at https://github.com/settings/applications/new
-	// and enter the client ID and client secret here.
-	gitHubCredentials = "client_id=<id>&client_secret=<secret>"
-}
diff --git a/gddo-server/main.go b/gddo-server/main.go
index 816971a..ead09cf 100644
--- a/gddo-server/main.go
+++ b/gddo-server/main.go
@@ -811,28 +811,29 @@
 
 var (
 	db                    *database.Database
+	httpClient            *http.Client
 	statusImageHandlerPNG http.Handler
 	statusImageHandlerSVG http.Handler
 )
 
 var (
-	robot             = flag.Float64("robot", 100, "Request counter threshold for robots.")
-	assetsDir         = flag.String("assets", filepath.Join(defaultBase("github.com/golang/gddo/gddo-server"), "assets"), "Base directory for templates and static files.")
-	getTimeout        = flag.Duration("get_timeout", 8*time.Second, "Time to wait for package update from the VCS.")
-	firstGetTimeout   = flag.Duration("first_get_timeout", 5*time.Second, "Time to wait for first fetch of package from the VCS.")
-	maxAge            = flag.Duration("max_age", 24*time.Hour, "Update package documents older than this age.")
-	httpAddr          = flag.String("http", ":8080", "Listen for HTTP connections on this address.")
-	sidebarEnabled    = flag.Bool("sidebar", false, "Enable package page sidebar.")
-	defaultGOOS       = flag.String("default_goos", "", "Default GOOS to use when building package documents.")
-	gitHubCredentials = ""
-	userAgent         = ""
+	robot           = flag.Float64("robot", 100, "Request counter threshold for robots.")
+	assetsDir       = flag.String("assets", filepath.Join(defaultBase("github.com/golang/gddo/gddo-server"), "assets"), "Base directory for templates and static files.")
+	getTimeout      = flag.Duration("get_timeout", 8*time.Second, "Time to wait for package update from the VCS.")
+	firstGetTimeout = flag.Duration("first_get_timeout", 5*time.Second, "Time to wait for first fetch of package from the VCS.")
+	maxAge          = flag.Duration("max_age", 24*time.Hour, "Update package documents older than this age.")
+	httpAddr        = flag.String("http", ":8080", "Listen for HTTP connections on this address.")
+	sidebarEnabled  = flag.Bool("sidebar", false, "Enable package page sidebar.")
+	defaultGOOS     = flag.String("default_goos", "", "Default GOOS to use when building package documents.")
 )
 
 func main() {
 	flag.Parse()
-	doc.SetDefaultGOOS(*defaultGOOS)
 	log.Printf("Starting server, os.Args=%s", strings.Join(os.Args, " "))
 
+	doc.SetDefaultGOOS(*defaultGOOS)
+	httpClient = newHTTPClient()
+
 	if err := parseHTMLTemplates([][]string{
 		{"about.html", "common.html", "layout.html"},
 		{"bot.html", "common.html", "layout.html"},
@@ -918,7 +919,7 @@
 	mux.Handle("/robots.txt", staticServer.FileHandler("robots.txt"))
 	mux.Handle("/BingSiteAuth.xml", staticServer.FileHandler("BingSiteAuth.xml"))
 	mux.Handle("/C", http.RedirectHandler("http://golang.org/doc/articles/c_go_cgo.html", http.StatusMovedPermanently))
-	mux.Handle("/ajax.googleapis.com/", http.NotFoundHandler())
+	mux.Handle("/code.jquery.com/", http.NotFoundHandler())
 	mux.Handle("/", handler(serveHome))
 
 	cacheBusters.Handler = mux
diff --git a/gosrc/gosrc.go b/gosrc/gosrc.go
index 9ae9fca..41b41b3 100644
--- a/gosrc/gosrc.go
+++ b/gosrc/gosrc.go
@@ -324,13 +324,17 @@
 		}
 	}
 
-	repo := strings.TrimSuffix(im.repo, "."+im.vcs)
-	i := strings.Index(repo, "://")
+	// clonePath is the repo URL from import meta tag, with the "scheme://" prefix removed.
+	// It should be used for cloning repositories.
+	// repo is the repo URL from import meta tag, with the "scheme://" prefix removed, and
+	// a possible ".vcs" suffix trimmed.
+	i := strings.Index(im.repo, "://")
 	if i < 0 {
 		return nil, NotFoundError{Message: "bad repo URL: " + im.repo}
 	}
-	proto := repo[:i]
-	repo = repo[i+len("://"):]
+	proto := im.repo[:i]
+	clonePath := im.repo[i+len("://"):]
+	repo := strings.TrimSuffix(clonePath, "."+im.vcs)
 	dirName := importPath[len(im.projectRoot):]
 
 	resolvedPath := repo + dirName
@@ -340,6 +344,7 @@
 		match := map[string]string{
 			"dir":        dirName,
 			"importPath": importPath,
+			"clonePath":  clonePath,
 			"repo":       repo,
 			"scheme":     proto,
 			"vcs":        im.vcs,
diff --git a/gosrc/vcs.go b/gosrc/vcs.go
index ce872f9..db0c083 100644
--- a/gosrc/vcs.go
+++ b/gosrc/vcs.go
@@ -101,7 +101,7 @@
 
 type vcsCmd struct {
 	schemes  []string
-	download func([]string, string, string) (string, string, error)
+	download func(schemes []string, clonePath, repo, savedEtag string) (tag, etag string, err error)
 }
 
 var vcsCmds = map[string]*vcsCmd{
@@ -117,11 +117,11 @@
 
 var lsremoteRe = regexp.MustCompile(`(?m)^([0-9a-f]{40})\s+refs/(?:tags|heads)/(.+)$`)
 
-func downloadGit(schemes []string, repo, savedEtag string) (string, string, error) {
+func downloadGit(schemes []string, clonePath, repo, savedEtag string) (string, string, error) {
 	var p []byte
 	var scheme string
 	for i := range schemes {
-		cmd := exec.Command("git", "ls-remote", "--heads", "--tags", schemes[i]+"://"+repo)
+		cmd := exec.Command("git", "ls-remote", "--heads", "--tags", schemes[i]+"://"+clonePath)
 		log.Println(strings.Join(cmd.Args, " "))
 		var err error
 		p, err = outputWithTimeout(cmd, lsRemoteTimeout)
@@ -158,7 +158,7 @@
 		if err := os.MkdirAll(dir, 0777); err != nil {
 			return "", "", err
 		}
-		cmd := exec.Command("git", "clone", scheme+"://"+repo, dir)
+		cmd := exec.Command("git", "clone", scheme+"://"+clonePath, dir)
 		log.Println(strings.Join(cmd.Args, " "))
 		if err := runWithTimeout(cmd, cloneTimeout); err != nil {
 			return "", "", err
@@ -183,12 +183,12 @@
 	return tag, etag, nil
 }
 
-func downloadSVN(schemes []string, repo, savedEtag string) (string, string, error) {
+func downloadSVN(schemes []string, clonePath, repo, savedEtag string) (string, string, error) {
 	var scheme string
 	var revno string
 	for i := range schemes {
 		var err error
-		revno, err = getSVNRevision(schemes[i] + "://" + repo)
+		revno, err = getSVNRevision(schemes[i] + "://" + clonePath)
 		if err == nil {
 			scheme = schemes[i]
 			break
@@ -212,7 +212,7 @@
 		if err := os.MkdirAll(dir, 0777); err != nil {
 			return "", "", err
 		}
-		cmd := exec.Command("svn", "checkout", scheme+"://"+repo, "-r", revno, dir)
+		cmd := exec.Command("svn", "checkout", scheme+"://"+clonePath, "-r", revno, dir)
 		log.Println(strings.Join(cmd.Args, " "))
 		if err := runWithTimeout(cmd, cloneTimeout); err != nil {
 			return "", "", err
@@ -271,7 +271,7 @@
 
 	// Download and checkout.
 
-	tag, etag, err := cmd.download(schemes, match["repo"], etagSaved)
+	tag, etag, err := cmd.download(schemes, match["clonePath"], match["repo"], etagSaved)
 	if err != nil {
 		return nil, err
 	}
diff --git a/httputil/transport.go b/httputil/transport.go
new file mode 100644
index 0000000..4823a87
--- /dev/null
+++ b/httputil/transport.go
@@ -0,0 +1,134 @@
+// Copyright 2015 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 or at
+// https://developers.google.com/open-source/licenses/bsd.
+
+// This file implements a http.RoundTripper that authenticates
+// requests issued against api.github.com endpoint.
+
+package httputil
+
+import (
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+
+	"google.golang.org/cloud/compute/metadata"
+)
+
+// AuthTransport is an implementation of http.RoundTripper that authenticates
+// with the GitHub API.
+//
+// When both a token and client credentials are set, the latter is preferred.
+type AuthTransport struct {
+	UserAgent    string
+	Token        string
+	ClientID     string
+	ClientSecret string
+	Base         http.RoundTripper
+}
+
+// NewAuthTransport gives new AuthTransport created with GitHub credentials
+// read from GCE metadata when the metadata server is accessible (we're on GCE)
+// or read from environment varialbes otherwise.
+func NewAuthTransport(base http.RoundTripper) *AuthTransport {
+	if metadata.OnGCE() {
+		return NewAuthTransportFromMetadata(base)
+	}
+	return NewAuthTransportFromEnvironment(base)
+}
+
+// NewAuthTransportFromEnvironment gives new AuthTransport created with GitHub
+// credentials read from environment variables.
+func NewAuthTransportFromEnvironment(base http.RoundTripper) *AuthTransport {
+	return &AuthTransport{
+		UserAgent:    os.Getenv("USER_AGENT"),
+		Token:        os.Getenv("GITHUB_TOKEN"),
+		ClientID:     os.Getenv("GITHUB_CLIENT_ID"),
+		ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
+		Base:         base,
+	}
+}
+
+// NewAuthTransportFromMetadata gives new AuthTransport created with GitHub
+// credentials read from GCE metadata.
+func NewAuthTransportFromMetadata(base http.RoundTripper) *AuthTransport {
+	return &AuthTransport{
+		UserAgent:    gceAttr("user-agent"),
+		Token:        gceAttr("github-token"),
+		ClientID:     gceAttr("github-client-id"),
+		ClientSecret: gceAttr("github-client-secret"),
+		Base:         base,
+	}
+}
+
+// RoundTrip implements the http.RoundTripper interface.
+func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+	var reqCopy *http.Request
+	if t.UserAgent != "" {
+		reqCopy = copyRequest(req)
+		reqCopy.Header.Set("User-Agent", t.UserAgent)
+	}
+	if req.URL.Host == "api.github.com" {
+		switch {
+		case t.ClientID != "" && t.ClientSecret != "":
+			if reqCopy == nil {
+				reqCopy = copyRequest(req)
+			}
+			if reqCopy.URL.RawQuery == "" {
+				reqCopy.URL.RawQuery = "client_id=" + t.ClientID + "&client_secret=" + t.ClientSecret
+			} else {
+				reqCopy.URL.RawQuery += "&client_id=" + t.ClientID + "&client_secret=" + t.ClientSecret
+			}
+		case t.Token != "":
+			if reqCopy == nil {
+				reqCopy = copyRequest(req)
+			}
+			reqCopy.Header.Set("Authorization", "token "+t.Token)
+		}
+	}
+	if reqCopy != nil {
+		return t.base().RoundTrip(reqCopy)
+	}
+	return t.base().RoundTrip(req)
+}
+
+// CancelRequest cancels an in-flight request by closing its connection.
+func (c *AuthTransport) CancelRequest(req *http.Request) {
+	type canceler interface {
+		CancelRequest(req *http.Request)
+	}
+	if cr, ok := c.base().(canceler); ok {
+		cr.CancelRequest(req)
+	}
+}
+
+func (t *AuthTransport) base() http.RoundTripper {
+	if t.Base != nil {
+		return t.Base
+	}
+	return http.DefaultTransport
+}
+
+func gceAttr(name string) string {
+	s, err := metadata.ProjectAttributeValue(name)
+	if err != nil {
+		log.Printf("error querying metadata for %q: %s", name, err)
+		return ""
+	}
+	return s
+}
+
+func copyRequest(req *http.Request) *http.Request {
+	req2 := new(http.Request)
+	*req2 = *req
+	req2.URL = new(url.URL)
+	*req2.URL = *req.URL
+	req2.Header = make(http.Header, len(req.Header))
+	for k, s := range req.Header {
+		req2.Header[k] = append([]string(nil), s...)
+	}
+	return req2
+}
diff --git a/lintapp/README.md b/lintapp/README.md
index 0edff47..93577df 100644
--- a/lintapp/README.md
+++ b/lintapp/README.md
@@ -6,6 +6,6 @@
 Development Environment Setup
 -----------------------------
 
-- Copy config.go.template to config.go and edit the file as described in the comments.
-- Install Go App Engine SDK 
-- Run the server using the dev_appserver command.
+- Copy `app.yaml` to `prod.yaml` and put in the authentication data.
+- Install Go App Engine SDK.
+- Run the server using the `goapp serve prod.yaml` command.
diff --git a/lintapp/app.yaml b/lintapp/app.yaml
index 9894f1b..878da17 100644
--- a/lintapp/app.yaml
+++ b/lintapp/app.yaml
@@ -14,3 +14,9 @@
 
 - url: /.*
   script: _go_app
+
+env_variables:
+  CONTACT_EMAIL: ''        # set contact email for /-/bot.html
+  GITHUB_CLIENT_ID: ''     # used to increase rate-limits; see https://github.com/settings/applications/new
+  GITHUB_CLIENT_SECRET: '' # used to increase rate-limits; see https://github.com/settings/applications/new
+  GITHUB_TOKEN: ''         # personal token used for authentication; see https://github.com/settings/tokens/new
diff --git a/lintapp/config.go.template b/lintapp/config.go.template
deleted file mode 100644
index e3579fa..0000000
--- a/lintapp/config.go.template
+++ /dev/null
@@ -1,10 +0,0 @@
-package lintapp
-
-func init() {
-	// Register an application at https://github.com/settings/applications/new
-	// and enter the client ID and client secret here.
-	gitHubCredentials = "client_id=<id>&client_secret=<secret>"
-
-	// Set contact email for /-/bot.html
-	contactEmail = "example@example.com"
-}
diff --git a/lintapp/main.go b/lintapp/main.go
index 9a76640..14fc690 100644
--- a/lintapp/main.go
+++ b/lintapp/main.go
@@ -13,6 +13,7 @@
 	"fmt"
 	"html/template"
 	"net/http"
+	"os"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -23,6 +24,8 @@
 	"appengine/urlfetch"
 
 	"github.com/golang/gddo/gosrc"
+	"github.com/golang/gddo/httputil"
+
 	"github.com/golang/lint"
 )
 
@@ -30,6 +33,9 @@
 	http.Handle("/", handlerFunc(serveRoot))
 	http.Handle("/-/bot", handlerFunc(serveBot))
 	http.Handle("/-/refresh", handlerFunc(serveRefresh))
+	if s := os.Getenv("CONTACT_EMAIL"); s != "" {
+		contactEmail = s
+	}
 }
 
 var (
@@ -41,7 +47,7 @@
 		"timeago":      timeagoFn,
 		"contactEmail": contactEmailFn,
 	}
-	gitHubCredentials = ""
+	github = httputil.NewAuthTransportFromEnvironment(nil)
 )
 
 func parseTemplate(fnames ...string) *template.Template {
@@ -102,29 +108,15 @@
 	return writeResponse(w, status, errorTemplate, http.StatusText(status))
 }
 
-type transport struct {
-	rt http.RoundTripper
-	ua string
-}
-
-func (t transport) RoundTrip(r *http.Request) (*http.Response, error) {
-	r.Header.Set("User-Agent", t.ua)
-	if r.URL.Host == "api.github.com" && gitHubCredentials != "" {
-		if r.URL.RawQuery == "" {
-			r.URL.RawQuery = gitHubCredentials
-		} else {
-			r.URL.RawQuery += "&" + gitHubCredentials
-		}
-	}
-	return t.rt.RoundTrip(r)
-}
-
 func httpClient(r *http.Request) *http.Client {
 	c := appengine.NewContext(r)
 	return &http.Client{
-		Transport: &transport{
-			rt: &urlfetch.Transport{Context: c, Deadline: 10 * time.Second},
-			ua: fmt.Sprintf("%s (+http://%s/-/bot)", appengine.AppID(c), r.Host),
+		Transport: &httputil.Transport{
+			Token:        github.Token,
+			ClientID:     github.ClientID,
+			ClientSecret: github.ClientSecret,
+			Base:         &urlfetch.Transport{Context: c, Deadline: 10 * time.Second},
+			UserAgent:    fmt.Sprintf("%s (+http://%s/-/bot)", appengine.AppID(c), r.Host),
 		},
 	}
 }
diff --git a/talksapp/README.md b/talksapp/README.md
index 5e77c6f..69227d5 100644
--- a/talksapp/README.md
+++ b/talksapp/README.md
@@ -6,7 +6,7 @@
 Development Environment Setup
 -----------------------------
 
-- Copy config.go.template to config.go and edit the file as described in the comments.
-- Install Go App Engine SDK 
+- Copy `app.yaml` to `prod.yaml` and put in the authentication data.
+- Install Go App Engine SDK.
 - $ sh setup.sh 
-- Run the server using the dev_appserver command.
+- Run the server using the `goapp serve prod.yaml` command.
diff --git a/talksapp/app.yaml b/talksapp/app.yaml
index 2e9b1d6..58491b7 100644
--- a/talksapp/app.yaml
+++ b/talksapp/app.yaml
@@ -17,3 +17,9 @@
   upload: present/play.js
 - url: /.*
   script: _go_app
+
+env_variables:
+  CONTACT_EMAIL: ''        # set contact email for /-/bot.html
+  GITHUB_CLIENT_ID: ''     # used to increase rate-limits; see https://github.com/settings/applications/new
+  GITHUB_CLIENT_SECRET: '' # used to increase rate-limits; see https://github.com/settings/applications/new
+  GITHUB_TOKEN: ''         # personal token used for authentication; see https://github.com/settings/tokens/new
diff --git a/talksapp/main.go b/talksapp/main.go
index 117bf5e..4a6c400 100644
--- a/talksapp/main.go
+++ b/talksapp/main.go
@@ -9,14 +9,11 @@
 
 import (
 	"bytes"
-	"encoding/json"
 	"errors"
 	"fmt"
 	"html/template"
 	"io"
-	"log"
 	"net/http"
-	"net/url"
 	"os"
 	"path"
 	"time"
@@ -25,8 +22,10 @@
 	"appengine/memcache"
 	"appengine/urlfetch"
 
-	"code.google.com/p/go.tools/present"
 	"github.com/golang/gddo/gosrc"
+	"github.com/golang/gddo/httputil"
+
+	"golang.org/x/tools/present"
 )
 
 var (
@@ -34,9 +33,9 @@
 		".article": parsePresentTemplate("article.tmpl"),
 		".slide":   parsePresentTemplate("slides.tmpl"),
 	}
-	homeArticle       = loadHomeArticle()
-	contactEmail      = "golang-dev@googlegroups.com"
-	githubCredentials = parseGithubCredentials()
+	homeArticle  = loadHomeArticle()
+	contactEmail = "golang-dev@googlegroups.com"
+	github       = httputil.NewAuthTransportFromEnvironment(nil)
 )
 
 func init() {
@@ -44,25 +43,9 @@
 	http.Handle("/compile", handlerFunc(serveCompile))
 	http.Handle("/bot.html", handlerFunc(serveBot))
 	present.PlayEnabled = true
-}
-
-func parseGithubCredentials() string {
-	f, err := os.Open("secret.json")
-	if err != nil {
-		log.Fatalf("open github credentials file secret.json: %v", err)
+	if s := os.Getenv("CONTACT_EMAIL"); s != "" {
+		contactEmail = s
 	}
-	defer f.Close()
-	var cred struct{ ClientID, ClientSecret string }
-	if err := json.NewDecoder(f).Decode(&cred); err != nil {
-		log.Fatalf("parse github credentials: %v", err)
-	}
-	if cred.ClientID == "" || cred.ClientSecret == "" {
-		log.Fatalf("secret.json needs to define ClientID and ClientSecret")
-	}
-	return url.Values{
-		"client_id":     {cred.ClientID},
-		"client_secret": {cred.ClientSecret},
-	}.Encode()
 }
 
 func playable(c present.Code) bool {
@@ -131,29 +114,15 @@
 	w.WriteHeader(status)
 }
 
-type transport struct {
-	rt http.RoundTripper
-	ua string
-}
-
-func (t transport) RoundTrip(r *http.Request) (*http.Response, error) {
-	r.Header.Set("User-Agent", t.ua)
-	if r.URL.Host == "api.github.com" {
-		if r.URL.RawQuery == "" {
-			r.URL.RawQuery = githubCredentials
-		} else {
-			r.URL.RawQuery += "&" + githubCredentials
-		}
-	}
-	return t.rt.RoundTrip(r)
-}
-
 func httpClient(r *http.Request) *http.Client {
 	c := appengine.NewContext(r)
 	return &http.Client{
-		Transport: &transport{
-			rt: &urlfetch.Transport{Context: c, Deadline: 10 * time.Second},
-			ua: fmt.Sprintf("%s (+http://%s/bot.html)", appengine.AppID(c), r.Host),
+		Transport: &httputil.AuthTransport{
+			Token:        github.Token,
+			ClientID:     github.ClientID,
+			ClientSecret: github.ClientSecret,
+			Base:         &urlfetch.Transport{Context: c, Deadline: 10 * time.Second},
+			UserAgent:    fmt.Sprintf("%s (+http://%s/-/bot)", appengine.AppID(c), r.Host),
 		},
 	}
 }
diff --git a/talksapp/secret.json b/talksapp/secret.json
deleted file mode 100644
index 979fe98..0000000
--- a/talksapp/secret.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-	"ClientID": "",
-	"ClientSecret": "" 
-}