1// Copyright 2012 Google Inc. All Rights Reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package main 16 17import ( 18 "bufio" 19 "errors" 20 "fmt" 21 "net" 22 "net/url" 23 "os" 24 "os/exec" 25 "strings" 26 "syscall" 27 "time" 28) 29 30type Client struct { 31 ProxyBin string 32 Args []string 33 34 insecure bool 35} 36 37func (c *Client) Run() error { 38 if err := c.resolveArgs(); err != nil { 39 return fmt.Errorf("resolveArgs() got error: %v", err) 40 } 41 42 // Connect to the proxy. 43 uconn, hconn, addr, err := c.connect() 44 if err != nil { 45 return fmt.Errorf("connect() got error: %v", err) 46 } 47 // Keep the unix socket connection open for the duration of the request. 48 defer uconn.Close() 49 // Keep a connection to the HTTP server open, so no other user can 50 // bind on the same address so long as the process is running. 51 defer hconn.Close() 52 53 // Start the git-remote-http subprocess. 54 cargs := []string{"-c", fmt.Sprintf("http.proxy=%v", addr), "remote-http"} 55 cargs = append(cargs, c.Args...) 56 cmd := exec.Command("git", cargs...) 57 58 for _, v := range os.Environ() { 59 if !strings.HasPrefix(v, "GIT_PERSISTENT_HTTPS_SECURE=") { 60 cmd.Env = append(cmd.Env, v) 61 } 62 } 63 // Set the GIT_PERSISTENT_HTTPS_SECURE environment variable when 64 // the proxy is using a SSL connection. This allows credential helpers 65 // to identify secure proxy connections, despite being passed an HTTP 66 // scheme. 67 if !c.insecure { 68 cmd.Env = append(cmd.Env, "GIT_PERSISTENT_HTTPS_SECURE=1") 69 } 70 71 cmd.Stdin = os.Stdin 72 cmd.Stdout = os.Stdout 73 cmd.Stderr = os.Stderr 74 if err := cmd.Run(); err != nil { 75 if eerr, ok := err.(*exec.ExitError); ok { 76 if stat, ok := eerr.ProcessState.Sys().(syscall.WaitStatus); ok && stat.ExitStatus() != 0 { 77 os.Exit(stat.ExitStatus()) 78 } 79 } 80 return fmt.Errorf("git-remote-http subprocess got error: %v", err) 81 } 82 return nil 83} 84 85func (c *Client) connect() (uconn net.Conn, hconn net.Conn, addr string, err error) { 86 uconn, err = DefaultSocket.Dial() 87 if err != nil { 88 if e, ok := err.(*net.OpError); ok && (os.IsNotExist(e.Err) || e.Err == syscall.ECONNREFUSED) { 89 if err = c.startProxy(); err == nil { 90 uconn, err = DefaultSocket.Dial() 91 } 92 } 93 if err != nil { 94 return 95 } 96 } 97 98 if addr, err = c.readAddr(uconn); err != nil { 99 return 100 } 101 102 // Open a tcp connection to the proxy. 103 if hconn, err = net.Dial("tcp", addr); err != nil { 104 return 105 } 106 107 // Verify the address hasn't changed ownership. 108 var addr2 string 109 if addr2, err = c.readAddr(uconn); err != nil { 110 return 111 } else if addr != addr2 { 112 err = fmt.Errorf("address changed after connect. got %q, want %q", addr2, addr) 113 return 114 } 115 return 116} 117 118func (c *Client) readAddr(conn net.Conn) (string, error) { 119 conn.SetDeadline(time.Now().Add(5 * time.Second)) 120 data := make([]byte, 100) 121 n, err := conn.Read(data) 122 if err != nil { 123 return "", fmt.Errorf("error reading unix socket: %v", err) 124 } else if n == 0 { 125 return "", errors.New("empty data response") 126 } 127 conn.Write([]byte{1}) // Ack 128 129 var addr string 130 if addrs := strings.Split(string(data[:n]), "\n"); len(addrs) != 2 { 131 return "", fmt.Errorf("got %q, wanted 2 addresses", data[:n]) 132 } else if c.insecure { 133 addr = addrs[1] 134 } else { 135 addr = addrs[0] 136 } 137 return addr, nil 138} 139 140func (c *Client) startProxy() error { 141 cmd := exec.Command(c.ProxyBin) 142 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 143 stdout, err := cmd.StdoutPipe() 144 if err != nil { 145 return err 146 } 147 defer stdout.Close() 148 if err := cmd.Start(); err != nil { 149 return err 150 } 151 result := make(chan error) 152 go func() { 153 bytes, _, err := bufio.NewReader(stdout).ReadLine() 154 if line := string(bytes); err == nil && line != "OK" { 155 err = fmt.Errorf("proxy returned %q, want \"OK\"", line) 156 } 157 result <- err 158 }() 159 select { 160 case err := <-result: 161 return err 162 case <-time.After(5 * time.Second): 163 return errors.New("timeout waiting for proxy to start") 164 } 165 panic("not reachable") 166} 167 168func (c *Client) resolveArgs() error { 169 if nargs := len(c.Args); nargs == 0 { 170 return errors.New("remote needed") 171 } else if nargs > 2 { 172 return fmt.Errorf("want at most 2 args, got %v", c.Args) 173 } 174 175 // Rewrite the url scheme to be http. 176 idx := len(c.Args) - 1 177 rawurl := c.Args[idx] 178 rurl, err := url.Parse(rawurl) 179 if err != nil { 180 return fmt.Errorf("invalid remote: %v", err) 181 } 182 c.insecure = rurl.Scheme == "persistent-http" 183 rurl.Scheme = "http" 184 c.Args[idx] = rurl.String() 185 if idx != 0 && c.Args[0] == rawurl { 186 c.Args[0] = c.Args[idx] 187 } 188 return nil 189} 190