package warp import ( "crypto/rand" "encoding/base32" "fmt" "io" "net/http" "net/url" "os" "os/exec" "path/filepath" "runtime" "syscall" "time" homedir "github.com/mitchellh/go-homedir" ) // Login obtains credentials from Cloudflare to enable // the creation of tunnels with the Warp service. // baseURL is the base URL from which to login to warp; // leave empty to use default. func Login(configDir, credentialFile, baseURL string) error { credPath := filepath.Join(configDir, credentialFile) if ok, err := HasExistingCertificate(configDir, credentialFile); ok && err == nil { return fmt.Errorf(`You have an existing certificate at %s which login would overwrite. If this is intentional, please move or delete that file then run this command again. `, credPath) } else if err != nil && err.(*os.PathError).Err != syscall.ENOENT { return err } // for local debugging if baseURL == "" { baseURL = baseCertStoreURL } // generate a random post URL certURL := baseURL + generateRandomPath() loginURL, err := url.Parse(baseLoginURL) if err != nil { // shouldn't happen, URL is hardcoded return err } loginURL.RawQuery = "callback=" + url.QueryEscape(certURL) err = open(loginURL.String()) if err != nil { fmt.Fprintf(os.Stderr, `Please open the following URL and log in with your Cloudflare account: %s Leave the program running to install the certificate automatically. `, loginURL.String()) } else { fmt.Fprintf(os.Stderr, `A browser window should have opened at the following URL: %s If the browser failed to open, open it yourself and visit the URL above. `, loginURL.String()) } if ok, err := download(certURL, credPath); ok && err == nil { fmt.Fprintf(os.Stderr, `You have successfully logged in. If you wish to copy your credentials to a server, they have been saved to: %s `, credPath) } else { fmt.Fprintf(os.Stderr, `Failed to write the certificate due to the following error: %v Your browser will download the certificate instead. You will have to manually copy it to the following path: %s `, err, credPath) } return nil } // HasExistingCertificate returns true if a certificate in configDir // exists with name credentialFile. func HasExistingCertificate(configDir, credentialFile string) (bool, error) { configPath, err := homedir.Expand(configDir) if err != nil { return false, err } ok, err := fileExists(configPath) if !ok && err == nil { // create config directory if doesn't already exist err = os.Mkdir(configPath, 0700) } if err != nil { return false, err } path := filepath.Join(configPath, credentialFile) fileInfo, err := os.Stat(path) return err == nil && fileInfo.Size() > 0, nil } // generateRandomPath generates a random URL to associate with the certificate. func generateRandomPath() string { randomBytes := make([]byte, 40) _, err := rand.Read(randomBytes) if err != nil { panic(err) } return "/" + base32.StdEncoding.EncodeToString(randomBytes) } // open opens the specified URL in the default browser of the user. func open(url string) error { var cmd string var args []string switch runtime.GOOS { case "windows": cmd = "cmd" args = []string{"/c", "start"} case "darwin": cmd = "open" default: // "linux", "freebsd", "openbsd", "netbsd" cmd = "xdg-open" } args = append(args, url) return exec.Command(cmd, args...).Start() } // download downloads a certificate at certURL to filePath. // It returns true if the certificate was successfully // downloaded; false otherwise, with any applicable error. // An error may be returned even if the certificate was // downloaded successfully. func download(certURL, filePath string) (bool, error) { client := &http.Client{Timeout: clientTimeout} // attempt a (long-running) certificate get for i := 0; i < 20; i++ { ok, err := tryDownload(client, certURL, filePath) if ok { return true, putSuccess(client, certURL) } if err != nil { return false, fmt.Errorf("fetching certificate: %v", err) } } return false, nil } func tryDownload(client *http.Client, certURL, filePath string) (ok bool, err error) { resp, err := client.Get(certURL) if err != nil { return false, err } defer resp.Body.Close() if resp.StatusCode == 404 { return false, nil } if resp.StatusCode != 200 { return false, fmt.Errorf("Unexpected HTTP error code %d", resp.StatusCode) } if resp.Header.Get("Content-Type") != "application/x-pem-file" { return false, fmt.Errorf("Unexpected content type %s", resp.Header.Get("Content-Type")) } // write response file, err := os.Create(filePath) if err != nil { return false, err } defer file.Close() written, err := io.Copy(file, resp.Body) switch { case err != nil: return false, err case resp.ContentLength != written && resp.ContentLength != -1: return false, fmt.Errorf("Short read (%d bytes) from server while writing certificate", written) default: return true, nil } } func putSuccess(client *http.Client, certURL string) error { // indicate success to the relay server req, err := http.NewRequest("PUT", certURL+"/ok", nil) if err != nil { return fmt.Errorf("HTTP request error: %v", err) } resp, err := client.Do(req) if err != nil { return fmt.Errorf("HTTP error: %v", err) } resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("unexpected HTTP status code %d", resp.StatusCode) } return nil } const ( // The default directory in which to store configuration/credentials. DefaultConfigDir = "~/.cloudflare-warp" // The default credential filename. DefaultCredentialFilename = "cert.pem" ) const ( baseLoginURL = "https://www.cloudflare.com/a/warp" baseCertStoreURL = "https://login.cloudflarewarp.com" clientTimeout = 20 * time.Minute )