parent
2fc2f3c927
commit
faeba02e57
@ -1,199 +0,0 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
raven "github.com/getsentry/raven-go"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
|
||||
|
||||
// Flags return the global flags for Access related commands (hopefully none)
|
||||
func Flags() []cli.Flag {
|
||||
return []cli.Flag{} // no flags yet.
|
||||
}
|
||||
|
||||
// Commands returns all the Access related subcommands
|
||||
func Commands() []*cli.Command {
|
||||
return []*cli.Command{
|
||||
{
|
||||
Name: "access",
|
||||
Category: "Access (BETA)",
|
||||
Usage: "access <subcommand>",
|
||||
Description: `(BETA) Cloudflare Access protects internal resources by securing, authenticating and monitoring access
|
||||
per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
|
||||
able to reach sensitive resources. The commands provided here allow you to interact with Access protected
|
||||
applications from the command line. This feature is considered beta. Your feedback is greatly appreciated!
|
||||
https://cfl.re/CLIAuthBeta`,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "login",
|
||||
Action: login,
|
||||
Usage: "login <url of access application>",
|
||||
Description: `The login subcommand initiates an authentication flow with your identity provider.
|
||||
The subcommand will launch a browser. For headless systems, a url is provided.
|
||||
Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT)
|
||||
scoped to your identity, the application you intend to reach, and valid for a session duration set by your
|
||||
administrator. cloudflared stores the token in local storage.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "url",
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "curl",
|
||||
Action: curl,
|
||||
Usage: "curl <args>",
|
||||
Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-access-token
|
||||
header when using curl to reach an application behind Access.`,
|
||||
ArgsUsage: "allow-request will allow the curl request to continue even if the jwt is not present.",
|
||||
SkipFlagParsing: true,
|
||||
},
|
||||
{
|
||||
Name: "token",
|
||||
Action: token,
|
||||
Usage: "token -app=<url of access application>",
|
||||
ArgsUsage: "url of Access application",
|
||||
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "app",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// login pops up the browser window to do the actual login and JWT generation
|
||||
func login(c *cli.Context) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
logger := log.CreateLogger()
|
||||
args := c.Args()
|
||||
appURL, err := url.Parse(args.First())
|
||||
if args.Len() < 1 || err != nil {
|
||||
logger.Errorf("Please provide the url of the Access application\n")
|
||||
return err
|
||||
}
|
||||
if _, err := fetchToken(c, appURL); err != nil {
|
||||
logger.Errorf("Failed to fetch token: %s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// curl provides a wrapper around curl, passing Access JWT along in request
|
||||
func curl(c *cli.Context) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
logger := log.CreateLogger()
|
||||
args := c.Args()
|
||||
if args.Len() < 1 {
|
||||
logger.Error("Please provide the access app and command you wish to run.")
|
||||
return errors.New("incorrect args")
|
||||
}
|
||||
|
||||
cmdArgs, appURL, allowRequest, err := buildCurlCmdArgs(args.Slice())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := getTokenIfExists(appURL)
|
||||
if err != nil || token == "" {
|
||||
if allowRequest {
|
||||
logger.Warn("You don't have an Access token set. Please run access token <access application> to fetch one.")
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
}
|
||||
token, err = fetchToken(c, appURL)
|
||||
if err != nil {
|
||||
logger.Error("Failed to refresh token: ", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmdArgs = append(cmdArgs, "-H")
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("cf-access-token: %s", token))
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
}
|
||||
|
||||
// token dumps provided token to stdout
|
||||
func token(c *cli.Context) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
appURL, err := url.Parse(c.String("app"))
|
||||
if err != nil || c.NumFlags() < 1 {
|
||||
fmt.Fprintln(os.Stderr, "Please provide a url.")
|
||||
return err
|
||||
}
|
||||
token, err := getTokenIfExists(appURL)
|
||||
if err != nil || token == "" {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run token command to generate token.")
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprint(os.Stdout, token); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Failed to write token to stdout.")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processURL will preprocess the string (parse to a url, convert to punycode, etc).
|
||||
func processURL(s string) (*url.URL, error) {
|
||||
u, err := url.ParseRequestURI(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host, err := idna.ToASCII(u.Hostname())
|
||||
if err != nil { // we fail to convert to punycode, just return the url we parsed.
|
||||
return u, nil
|
||||
}
|
||||
if u.Port() != "" {
|
||||
u.Host = fmt.Sprintf("%s:%s", host, u.Port())
|
||||
} else {
|
||||
u.Host = host
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// buildCurlCmdArgs will build the curl cmd args
|
||||
func buildCurlCmdArgs(cmdArgs []string) ([]string, *url.URL, bool, error) {
|
||||
allowRequest, iAllowRequest := false, 0
|
||||
var appURL *url.URL
|
||||
for i, arg := range cmdArgs {
|
||||
if arg == "-allow-request" || arg == "-ar" {
|
||||
iAllowRequest = i
|
||||
allowRequest = true
|
||||
}
|
||||
|
||||
u, err := processURL(arg)
|
||||
if err == nil {
|
||||
appURL = u
|
||||
cmdArgs[i] = appURL.String()
|
||||
}
|
||||
}
|
||||
|
||||
if appURL == nil {
|
||||
logger.Error("Please provide a valid URL.")
|
||||
return cmdArgs, appURL, allowRequest, errors.New("invalid url")
|
||||
}
|
||||
|
||||
if allowRequest {
|
||||
// remove from cmdArgs
|
||||
cmdArgs[iAllowRequest] = cmdArgs[len(cmdArgs)-1]
|
||||
cmdArgs = cmdArgs[:len(cmdArgs)-1]
|
||||
}
|
||||
|
||||
return cmdArgs, appURL, allowRequest, nil
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/oidc"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
// fetchToken will either load a stored token or generate a new one
|
||||
func fetchToken(c *cli.Context, appURL *url.URL) (string, error) {
|
||||
if token, err := getTokenIfExists(appURL); token != "" && err == nil {
|
||||
fmt.Fprintf(os.Stdout, "You have an existing token:\n\n%s\n\n", token)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
path, err := generateFilePathForTokenURL(appURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// this weird parameter is the resource name (token) and the key/value
|
||||
// we want to send to the transfer service. the key is token and the value
|
||||
// is blank (basically just the id generated in the transfer service)
|
||||
const resourceName, key, value = "token", "token", ""
|
||||
token, err := transfer.Run(c, appURL, resourceName, key, value, path, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", string(token))
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
// getTokenIfExists will return the token from local storage if it exists
|
||||
func getTokenIfExists(url *url.URL) (string, error) {
|
||||
path, err := generateFilePathForTokenURL(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token, err := jose.ParseJWT(string(content))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims, err := token.Claims()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ident, err := oidc.IdentityFromClaims(claims)
|
||||
if err == nil && ident.ExpiresAt.After(time.Now()) {
|
||||
return token.Encode(), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// generateFilePathForTokenURL will return a filepath for given access application url
|
||||
func generateFilePathForTokenURL(url *url.URL) (string, error) {
|
||||
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ok, err := config.FileExists(configPath)
|
||||
if !ok && err == nil {
|
||||
// create config directory if doesn't already exist
|
||||
err = os.Mkdir(configPath, 0700)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := strings.Replace(fmt.Sprintf("%s%s-token", url.Hostname(), url.EscapedPath()), "/", "-", -1)
|
||||
return filepath.Join(configPath, name), nil
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
)
|
||||
|
||||
var (
|
||||
// File names from which we attempt to read configuration.
|
||||
DefaultConfigFiles = []string{"config.yml", "config.yaml"}
|
||||
|
||||
// Launchd doesn't set root env variables, so there is default
|
||||
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
|
||||
DefaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/usr/local/etc/cloudflared", "/etc/cloudflared"}
|
||||
)
|
||||
|
||||
const DefaultCredentialFile = "cert.pem"
|
||||
|
||||
// FileExists checks to see if a file exist at the provided path.
|
||||
func FileExists(path string) (bool, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// ignore missing files
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
f.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FindInputSourceContext pulls the input source from the config flag.
|
||||
func FindInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
|
||||
if context.String("config") != "" {
|
||||
return altsrc.NewYamlSourceFromFile(context.String("config"))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindDefaultConfigPath returns the first path that contains a config file.
|
||||
// If none of the combination of DefaultConfigDirs and DefaultConfigFiles
|
||||
// contains a config file, return empty string.
|
||||
func FindDefaultConfigPath() string {
|
||||
for _, configDir := range DefaultConfigDirs {
|
||||
for _, configFile := range DefaultConfigFiles {
|
||||
dirPath, err := homedir.Expand(configDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dirPath, configFile)
|
||||
if ok, _ := FileExists(path); ok {
|
||||
return path
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
// Package encrypter is suitable for encrypting messages you would like to securely share between two points.
|
||||
// Useful for providing end to end encryption (E2EE). It uses Box (NaCl) for encrypting the messages.
|
||||
// tldr is it uses Elliptic Curves (Curve25519) for the keys, XSalsa20 and Poly1305 for encryption.
|
||||
// You can read more here https://godoc.org/golang.org/x/crypto/nacl/box.
|
||||
//
|
||||
// msg := []byte("super safe message.")
|
||||
// alice, err := New("alice_priv_key.pem", "alice_pub_key.pem")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// bob, err := New("bob_priv_key.pem", "bob_pub_key.pem")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// encrypted, err := alice.Encrypt(msg, bob.PublicKey())
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// data, err := bob.Decrypt(encrypted, alice.PublicKey())
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Println(string(data))
|
||||
package encrypter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
// Encrypter represents a keypair value with auxiliary functions to make
|
||||
// doing encryption and decryption easier
|
||||
type Encrypter struct {
|
||||
privateKey *[32]byte
|
||||
publicKey *[32]byte
|
||||
}
|
||||
|
||||
// New returns a new encrypter with initialized keypair
|
||||
func New(privateKey, publicKey string) (*Encrypter, error) {
|
||||
e := &Encrypter{}
|
||||
pubKey, key, err := e.fetchOrGenerateKeys(privateKey, publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.privateKey, e.publicKey = key, pubKey
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// PublicKey returns a base64 encoded public key. Useful for transport (like in HTTP requests)
|
||||
func (e *Encrypter) PublicKey() string {
|
||||
return base64.URLEncoding.EncodeToString(e.publicKey[:])
|
||||
}
|
||||
|
||||
// Decrypt data that was encrypted using our publicKey. It will use our privateKey and the sender's publicKey to decrypt
|
||||
// data is an encrypted buffer of data, mostly like from the Encrypt function. Messages contain the nonce data on the front
|
||||
// of the message.
|
||||
// senderPublicKey is a base64 encoded version of the sender's public key (most likely from the PublicKey function).
|
||||
// The return value is the decrypted buffer or an error.
|
||||
func (e *Encrypter) Decrypt(data []byte, senderPublicKey string) ([]byte, error) {
|
||||
var decryptNonce [24]byte
|
||||
copy(decryptNonce[:], data[:24]) // we pull the nonce from the front of the actual message.
|
||||
pubKey, err := e.decodePublicKey(senderPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decrypted, ok := box.Open(nil, data[24:], &decryptNonce, pubKey, e.privateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to decrypt message")
|
||||
}
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
// Encrypt data using our privateKey and the recipient publicKey
|
||||
// data is a buffer of data that we would like to encrypt. Messages will have the nonce added to front
|
||||
// as they have to unique for each message shared.
|
||||
// recipientPublicKey is a base64 encoded version of the sender's public key (most likely from the PublicKey function).
|
||||
// The return value is the encrypted buffer or an error.
|
||||
func (e *Encrypter) Encrypt(data []byte, recipientPublicKey string) ([]byte, error) {
|
||||
var nonce [24]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubKey, err := e.decodePublicKey(recipientPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// This encrypts msg and adds the nonce to the front of the message, since the nonce has to be
|
||||
// the same for encrypting and decrypting
|
||||
return box.Seal(nonce[:], data, &nonce, pubKey, e.privateKey), nil
|
||||
}
|
||||
|
||||
// WriteKeys keys will take the currently initialized keypair and write them to provided filenames
|
||||
func (e *Encrypter) WriteKeys(privateKey, publicKey string) error {
|
||||
if err := e.writeKey(e.privateKey[:], "BOX PRIVATE KEY", privateKey); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.writeKey(e.publicKey[:], "PUBLIC KEY", publicKey)
|
||||
}
|
||||
|
||||
// fetchOrGenerateKeys will either load or create a keypair if it doesn't exist
|
||||
func (e *Encrypter) fetchOrGenerateKeys(privateKey, publicKey string) (*[32]byte, *[32]byte, error) {
|
||||
key, err := e.fetchKey(privateKey)
|
||||
if os.IsNotExist(err) {
|
||||
return box.GenerateKey(rand.Reader)
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pub, err := e.fetchKey(publicKey)
|
||||
if os.IsNotExist(err) {
|
||||
return box.GenerateKey(rand.Reader)
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return pub, key, nil
|
||||
}
|
||||
|
||||
// writeKey will write a key to disk in DER format (it's a standard pem key)
|
||||
func (e *Encrypter) writeKey(key []byte, pemType, filename string) error {
|
||||
data := pem.EncodeToMemory(&pem.Block{
|
||||
Type: pemType,
|
||||
Bytes: key,
|
||||
})
|
||||
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchKey will load a a DER formatted key from disk
|
||||
func (e *Encrypter) fetchKey(filename string) (*[32]byte, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
io.Copy(buf, f)
|
||||
|
||||
p, _ := pem.Decode(buf.Bytes())
|
||||
if p == nil {
|
||||
return nil, errors.New("Failed to decode key")
|
||||
}
|
||||
var newKey [32]byte
|
||||
copy(newKey[:], p.Bytes)
|
||||
|
||||
return &newKey, nil
|
||||
}
|
||||
|
||||
// decodePublicKey will base64 decode the provided key to the box representation
|
||||
func (e *Encrypter) decodePublicKey(key string) (*[32]byte, error) {
|
||||
pub, err := base64.URLEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var newKey [32]byte
|
||||
copy(newKey[:], pub)
|
||||
return &newKey, nil
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
package tunnel
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
|
||||
"github.com/rifflock/lfshook"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/urfave/cli.v2"
|
@ -0,0 +1,194 @@
|
||||
package main
|
||||
|
||||
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"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const baseLoginURL = "https://dash.cloudflare.com/warp"
|
||||
const baseCertStoreURL = "https://login.cloudflarewarp.com"
|
||||
const clientTimeout = time.Minute * 20
|
||||
|
||||
func login(c *cli.Context) error {
|
||||
configPath, err := homedir.Expand(defaultConfigDirs[0])
|
||||
if err != nil {
|
||||
return 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 err
|
||||
}
|
||||
path := filepath.Join(configPath, defaultCredentialFile)
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err == nil && fileInfo.Size() > 0 {
|
||||
fmt.Fprintf(os.Stderr, `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.
|
||||
`, path)
|
||||
return nil
|
||||
}
|
||||
if err != nil && err.(*os.PathError).Err != syscall.ENOENT {
|
||||
return err
|
||||
}
|
||||
|
||||
// for local debugging
|
||||
baseURL := baseCertStoreURL
|
||||
if c.IsSet("url") {
|
||||
baseURL = c.String("url")
|
||||
}
|
||||
// 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 cloudflared 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 download(certURL, path) {
|
||||
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
|
||||
`, path)
|
||||
} 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, path)
|
||||
}
|
||||
return 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()
|
||||
}
|
||||
|
||||
func download(certURL, filePath string) bool {
|
||||
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 {
|
||||
putSuccess(client, certURL)
|
||||
return true
|
||||
}
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error fetching certificate")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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) {
|
||||
// indicate success to the relay server
|
||||
req, err := http.NewRequest("PUT", certURL+"/ok", nil)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("HTTP request error")
|
||||
return
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("HTTP error")
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
logger.Errorf("Unexpected HTTP error code %d", resp.StatusCode)
|
||||
}
|
||||
}
|