215 lines
5.6 KiB
Go
215 lines
5.6 KiB
Go
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
|
|
)
|