Support HTTP proxies and cookie authentication.
* Add --cookies flag, taking a cURL/Mozilla style cookie file.
* Support in-memory cookie Jar in the Gitiles client.
* Support redirects in the Gitiles client.
* Add --agent flag to set the User-Agent string.
* Add slothfs-gitiles-test to debug the whole thing.
Change-Id: Ie2aec36372706f71083a323a7aa92864f2210463
diff --git a/cmd/slothfs-expand-manifest/main.go b/cmd/slothfs-expand-manifest/main.go
index 2b73407..294c43c 100644
--- a/cmd/slothfs-expand-manifest/main.go
+++ b/cmd/slothfs-expand-manifest/main.go
@@ -20,6 +20,7 @@
"log"
"os"
+ "github.com/google/slothfs/cookie"
"github.com/google/slothfs/gitiles"
"github.com/google/slothfs/manifest"
@@ -30,33 +31,46 @@
gitilesURL := flag.String("gitiles", "", "URL for gitiles")
branch := flag.String("branch", "master", "branch to use for manifest")
repo := flag.String("repo", "platform/manifest", "manifest repository")
+ cookieJarPath := flag.String("cookies", "", "path to cURL-style cookie jar file.")
+ agent := flag.String("agent", "slothfs-expand", "gitiles User-Agent string to use.")
flag.Parse()
if *gitilesURL == "" {
log.Fatal("must set --gitiles")
}
+ opts := gitiles.Options{
+ UserAgent: *agent,
+ }
+ if *cookieJarPath != "" {
+ var err error
+ opts.CookieJar, err = cookie.NewJar(*cookieJarPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+
// SustainedQPS is a little high, but since this is a one-shot
// program let's try to get away with it.
- service, err := gitiles.NewService(*gitilesURL, gitiles.Options{})
+ service, err := gitiles.NewService(*gitilesURL, opts)
if err != nil {
- log.Fatal(err)
+ log.Fatalf("NewService: %v", err)
}
mf, err := fetchManifest(service, *repo, *branch)
if err != nil {
- log.Fatal(err)
+ log.Fatalf("fetchManifest: %v", err)
}
mf.Filter()
if err := derefManifest(service, *repo, mf); err != nil {
- log.Fatal(err)
+ log.Fatalf("derefManifest: %v", err)
}
xml, err := mf.MarshalXML()
if err != nil {
- log.Fatal(err)
+ log.Fatalf("MarshalXML: %v", err)
}
os.Stdout.Write(xml)
@@ -79,7 +93,6 @@
}
func derefManifest(service *gitiles.Service, manifestRepo string, mf *manifest.Manifest) error {
-
branchSet := map[string]struct{}{}
var todoProjects []int
diff --git a/cmd/slothfs-gitiles-test/main.go b/cmd/slothfs-gitiles-test/main.go
new file mode 100644
index 0000000..3e30cdc
--- /dev/null
+++ b/cmd/slothfs-gitiles-test/main.go
@@ -0,0 +1,129 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// slothfs-gitiles-test is a program executing a single gitiles HTTP
+// request, to be used for troubleshooting proxy/auth problems.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "os"
+ "sync"
+
+ "github.com/google/slothfs/cookie"
+ "github.com/google/slothfs/gitiles"
+)
+
+func main() {
+ gitilesURL := flag.String("gitiles", "", "URL for gitiles")
+ cookieJarPath := flag.String("cookies", "", "path to cURL-style cookie jar file.")
+ agent := flag.String("agent", "slothfs-expand", "gitiles User-Agent string to use.")
+ tap := flag.Bool("tap", false, "if set, tap traffic exchanged with $http_proxy")
+ flag.Parse()
+
+ if *tap {
+ tapTraffic()
+ }
+ if *gitilesURL == "" {
+ log.Fatal("must set --gitiles")
+ }
+
+ opts := gitiles.Options{
+ Redirect: true,
+ UserAgent: *agent,
+ }
+ if *cookieJarPath != "" {
+ var err error
+ opts.CookieJar, err = cookie.NewJar(*cookieJarPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ service, err := gitiles.NewService(*gitilesURL, opts)
+ if err != nil {
+ log.Fatalf("NewService: %v", err)
+ }
+
+ projs, err := service.List()
+ if err != nil {
+ log.Fatalf("List: %v", err)
+ }
+
+ for p := range projs {
+ fmt.Printf("project: %s\n", p)
+ }
+}
+
+func logCopy(w io.Writer, r io.Reader, who string) {
+ var buf [320000]byte
+
+ for {
+ n, e1 := r.Read(buf[:])
+ log.Println(who, string(buf[:n]))
+ _, e2 := w.Write(buf[:n])
+ if e1 != nil || e2 != nil {
+ break
+ }
+ }
+}
+
+func forward(conn net.Conn, addr string) {
+ f, err := net.Dial("tcp", addr)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ var wg sync.WaitGroup
+ wg.Add(2)
+ go func() {
+ logCopy(f, conn, "A")
+ wg.Done()
+ }()
+ go func() {
+ logCopy(conn, f, "B")
+ wg.Done()
+ }()
+ wg.Wait()
+ f.Close()
+ conn.Close()
+}
+
+func tapTraffic() {
+ proxy := os.Getenv("http_proxy")
+ if proxy == "" {
+ log.Println("no http_proxy, not tapping")
+ return
+ }
+
+ l, err := net.Listen("tcp", ":0")
+ if err != nil {
+ log.Fatal(err)
+ }
+ os.Setenv("http_proxy", l.Addr().String())
+
+ go func() {
+ for {
+ c, err := l.Accept()
+ if err != nil {
+ break
+ }
+ go forward(c, proxy)
+ }
+ }()
+}
diff --git a/cmd/slothfs-multifs/main.go b/cmd/slothfs-multifs/main.go
index f9f9a93..1c25fcb 100644
--- a/cmd/slothfs-multifs/main.go
+++ b/cmd/slothfs-multifs/main.go
@@ -23,6 +23,7 @@
"time"
"github.com/google/slothfs/cache"
+ "github.com/google/slothfs/cookie"
"github.com/google/slothfs/fs"
"github.com/google/slothfs/gitiles"
"github.com/hanwen/go-fuse/fuse/nodefs"
@@ -33,6 +34,8 @@
cacheDir := flag.String("cache", filepath.Join(os.Getenv("HOME"), ".cache", "slothfs"), "cache dir")
debug := flag.Bool("debug", false, "print debug info")
config := flag.String("config", "", "JSON file configuring what repositories should be cloned.")
+ cookieJarPath := flag.String("cookies", "", "path to cURL-style cookie jar file.")
+ agent := flag.String("agent", "slothfs-multifs", "gitiles User-Agent string to use.")
flag.Parse()
if *cacheDir == "" {
@@ -53,7 +56,18 @@
log.Printf("NewCache: %v", err)
}
- service, err := gitiles.NewService(*gitilesURL, gitiles.Options{})
+ gitilesOpts := gitiles.Options{
+ UserAgent: *agent,
+ }
+ if *cookieJarPath != "" {
+ var err error
+ gitilesOpts.CookieJar, err = cookie.NewJar(*cookieJarPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ service, err := gitiles.NewService(*gitilesURL, gitilesOpts)
if err != nil {
log.Printf("NewService: %v", err)
}
diff --git a/cookie/cookie.go b/cookie/cookie.go
new file mode 100644
index 0000000..d5c8e5c
--- /dev/null
+++ b/cookie/cookie.go
@@ -0,0 +1,102 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// cookie parses curl cookie jar files.
+package cookie
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/cookiejar"
+ "net/url"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// ParseCookieJar parses a cURL/Mozilla/Netscape cookie jar text file.
+func ParseCookieJar(r io.Reader) ([]*http.Cookie, error) {
+ var result []*http.Cookie
+ scanner := bufio.NewScanner(r)
+ for scanner.Scan() {
+ line := scanner.Text()
+ httpOnly := false
+ const httpOnlyPrefix = "#HttpOnly_"
+ if strings.HasPrefix(line, httpOnlyPrefix) {
+ line = line[len(httpOnlyPrefix):]
+ httpOnly = true
+ }
+ if strings.HasPrefix(line, "#") {
+ continue
+ }
+ line = strings.TrimSpace(line)
+
+ if line == "" {
+ continue
+ }
+ fields := strings.Split(line, "\t")
+ if len(fields) != 7 {
+ return nil, fmt.Errorf("got %d fields in line %q, want 8", len(fields), line)
+ }
+
+ exp, err := strconv.ParseInt(fields[4], 10, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ c := http.Cookie{
+ Domain: fields[0],
+ Name: fields[5],
+ Value: fields[6],
+ Path: fields[2],
+ Expires: time.Unix(exp, 0),
+ Secure: fields[3] == "TRUE",
+ HttpOnly: httpOnly,
+ }
+
+ result = append(result, &c)
+ }
+
+ return result, nil
+}
+
+func NewJar(path string) (http.CookieJar, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ cs, err := ParseCookieJar(f)
+ if err != nil {
+ return nil, err
+ }
+
+ jar, err := cookiejar.New(nil)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, c := range cs {
+ jar.SetCookies(&url.URL{
+ Scheme: "http",
+ Host: c.Domain,
+ }, []*http.Cookie{c})
+ }
+
+ return jar, nil
+}
diff --git a/cookie/cookie_test.go b/cookie/cookie_test.go
new file mode 100644
index 0000000..b63260e
--- /dev/null
+++ b/cookie/cookie_test.go
@@ -0,0 +1,52 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cookie
+
+import (
+ "bytes"
+ "net/http"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestParseCookieJar(t *testing.T) {
+ in := `# Netscape HTTP Cookie File
+# http://www.netscape.com/newsref/std/cookie_spec.html
+# This is a generated file! Do not edit.
+#HttpOnly_login.netscape.com FALSE / FALSE 1467968199 XYZ abc|pqr`
+
+ buf := bytes.NewBufferString(in)
+ got, err := ParseCookieJar(buf)
+ if err != nil {
+ t.Fatalf("ParseCookieJar: %v", err)
+ }
+
+ want := []*http.Cookie{
+ {
+ Domain: "login.netscape.com",
+ Path: "/",
+ Secure: false,
+ Expires: time.Unix(1467968199, 0),
+ Name: "XYZ",
+ Value: "abc|pqr",
+ HttpOnly: true,
+ },
+ }
+
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+}
diff --git a/gitiles/client.go b/gitiles/client.go
index 86845e3..2cf859f 100644
--- a/gitiles/client.go
+++ b/gitiles/client.go
@@ -34,6 +34,8 @@
type Service struct {
limiter *rate.Limiter
addr url.URL
+ client http.Client
+ agent string
}
// Addr returns the address of the gitiles service.
@@ -45,6 +47,12 @@
type Options struct {
BurstQPS int
SustainedQPS float64
+
+ // A writable cookie jar for storing (among others) authentication cookies.
+ CookieJar http.CookieJar
+
+ // UserAgent defines how we present ourself to the server.
+ UserAgent string
}
// NewService returns a new Gitiles JSON client.
@@ -60,10 +68,18 @@
if err != nil {
return nil, err
}
- return &Service{
+ s := &Service{
limiter: rate.NewLimiter(rate.Limit(opts.SustainedQPS), opts.BurstQPS),
addr: *url,
- }, nil
+ agent: opts.UserAgent,
+ }
+
+ s.client.Jar = opts.CookieJar
+ s.client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+ req.Header.Set("User-Agent", s.agent)
+ return nil
+ }
+ return s, nil
}
func (s *Service) get(u *url.URL) ([]byte, error) {
@@ -72,7 +88,12 @@
if err := s.limiter.Wait(ctx); err != nil {
return nil, err
}
- resp, err := http.Get(u.String())
+ req, err := http.NewRequest("GET", u.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Add("User-Agent", s.agent)
+ resp, err := s.client.Do(req)
if err != nil {
return nil, err
@@ -81,6 +102,12 @@
if resp.StatusCode != 200 {
return nil, fmt.Errorf("%s: %s", u.String(), resp.Status)
}
+ if got := resp.Request.URL.String(); got != u.String() {
+ // We accept redirects, but only for authentication.
+ // If we get a 200 from a different page than we
+ // requested, it's probably some sort of login page.
+ return nil, fmt.Errorf("got URL %s, want %s", got, u.String())
+ }
c, err := ioutil.ReadAll(resp.Body)
if err != nil {