Merge branch 'master' into lib-refactor

# Conflicts:
#	cmd/cloudflare-warp/main.go
#	cmd/cloudflare-warp/service_template.go
#	warp/login.go
This commit is contained in:
Matthew Holt 2018-03-09 19:01:44 -07:00
commit cbb4c7ed89
6 changed files with 170 additions and 51 deletions

View File

@ -5,6 +5,7 @@ package main
import (
"fmt"
"os"
"path/filepath"
cli "gopkg.in/urfave/cli.v2"
)
@ -180,16 +181,20 @@ func installLinuxService(c *cli.Context) error {
}
templateArgs := ServiceTemplateArgs{Path: etPath}
if err = copyCredentials(serviceConfigDir); err != nil {
fmt.Fprintf(os.Stderr, "Failed to copy user configuration: %v\n", err)
fmt.Fprintf(os.Stderr, "Before running the service, ensure that %s contains two files, %s and %s",
serviceConfigDir, credentialFile, configFile)
defaultConfigDir := filepath.Dir(c.String("config"))
defaultConfigFile := filepath.Base(c.String("config"))
if err = copyCredentials(serviceConfigDir, defaultConfigDir, defaultConfigFile); err != nil {
Log.WithError(err).Infof("Failed to copy user configuration. Before running the service, ensure that %s contains two files, %s and %s",
serviceConfigDir, credentialFile, defaultConfigFiles[0])
return err
}
switch {
case isSystemd():
Log.Infof("Using Systemd")
return installSystemd(&templateArgs)
default:
Log.Infof("Using Sysv")
return installSysv(&templateArgs)
}
}
@ -198,24 +203,30 @@ func installSystemd(templateArgs *ServiceTemplateArgs) error {
for _, serviceTemplate := range systemdTemplates {
err := serviceTemplate.Generate(templateArgs)
if err != nil {
Log.WithError(err).Infof("error generating service template")
return err
}
}
if err := runCommand("systemctl", "enable", "cloudflare-warp.service"); err != nil {
Log.WithError(err).Infof("systemctl enable cloudflare-warp.service error")
return err
}
if err := runCommand("systemctl", "start", "cloudflare-warp-update.timer"); err != nil {
Log.WithError(err).Infof("systemctl start cloudflare-warp-update.timer error")
return err
}
Log.Infof("systemctl daemon-reload")
return runCommand("systemctl", "daemon-reload")
}
func installSysv(templateArgs *ServiceTemplateArgs) error {
confPath, err := sysvTemplate.ResolvePath()
if err != nil {
Log.WithError(err).Infof("error resolving system path")
return err
}
if err := sysvTemplate.Generate(templateArgs); err != nil {
Log.WithError(err).Infof("error generating system template")
return err
}
for _, i := range [...]string{"2", "3", "4", "5"} {
@ -234,29 +245,36 @@ func installSysv(templateArgs *ServiceTemplateArgs) error {
func uninstallLinuxService(c *cli.Context) error {
switch {
case isSystemd():
Log.Infof("Using Systemd")
return uninstallSystemd()
default:
Log.Infof("Using Sysv")
return uninstallSysv()
}
}
func uninstallSystemd() error {
if err := runCommand("systemctl", "disable", "cloudflare-warp.service"); err != nil {
Log.WithError(err).Infof("systemctl disable cloudflare-warp.service error")
return err
}
if err := runCommand("systemctl", "stop", "cloudflare-warp-update.timer"); err != nil {
Log.WithError(err).Infof("systemctl stop cloudflare-warp-update.timer error")
return err
}
for _, serviceTemplate := range systemdTemplates {
if err := serviceTemplate.Remove(); err != nil {
Log.WithError(err).Infof("error removing service template")
return err
}
}
Log.Infof("Successfully uninstall cloudflare-warp service")
return nil
}
func uninstallSysv() error {
if err := sysvTemplate.Remove(); err != nil {
Log.WithError(err).Infof("error removing service template")
return err
}
for _, i := range [...]string{"2", "3", "4", "5"} {
@ -269,5 +287,6 @@ func uninstallSysv() error {
continue
}
}
Log.Infof("Successfully uninstall cloudflare-warp service")
return nil
}

View File

@ -9,6 +9,8 @@ import (
cli "gopkg.in/urfave/cli.v2"
)
const launchAgentIdentifier = "com.cloudflare.warp"
func runApp(app *cli.App) {
app.Commands = append(app.Commands, &cli.Command{
Name: "service",
@ -31,16 +33,20 @@ func runApp(app *cli.App) {
var launchdTemplate = ServiceTemplate{
Path: "~/Library/LaunchAgents/com.cloudflare.warp.plist",
Content: `<?xml version="1.0" encoding="UTF-8"?>
Content: fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.cloudflare.warp</string>
<string>%s</string>
<key>Program</key>
<string>{{ .Path }}</string>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/%s.out.log</string>
<key>StandardErrorPath</key>
<string>/tmp/%s.err.log</string>
<key>KeepAlive</key>
<dict>
<key>NetworkState</key>
@ -49,34 +55,43 @@ var launchdTemplate = ServiceTemplate{
<key>ThrottleInterval</key>
<integer>20</integer>
</dict>
</plist>`,
</plist>`, launchAgentIdentifier, launchAgentIdentifier, launchAgentIdentifier),
}
func installLaunchd(c *cli.Context) error {
Log.Infof("Installing Cloudflare Warp as an user launch agent")
etPath, err := os.Executable()
if err != nil {
Log.WithError(err).Infof("error determining executable path")
return fmt.Errorf("error determining executable path: %v", err)
}
templateArgs := ServiceTemplateArgs{Path: etPath}
err = launchdTemplate.Generate(&templateArgs)
if err != nil {
Log.WithError(err).Infof("error generating launchd template")
return err
}
plistPath, err := launchdTemplate.ResolvePath()
if err != nil {
Log.WithError(err).Infof("error resolving launchd template path")
return err
}
Log.Infof("Outputs are logged in %s and %s", fmt.Sprintf("/tmp/%s.out.log", launchAgentIdentifier), fmt.Sprintf("/tmp/%s.err.log", launchAgentIdentifier))
return runCommand("launchctl", "load", plistPath)
}
func uninstallLaunchd(c *cli.Context) error {
Log.Infof("Uninstalling Cloudflare Warp as an user launch agent")
plistPath, err := launchdTemplate.ResolvePath()
if err != nil {
Log.WithError(err).Infof("error resolving launchd template path")
return err
}
err = runCommand("launchctl", "unload", plistPath)
if err != nil {
Log.WithError(err).Infof("error unloading")
return err
}
Log.Infof("Outputs are logged in %s and %s", fmt.Sprintf("/tmp/%s.out.log", launchAgentIdentifier), fmt.Sprintf("/tmp/%s.err.log", launchAgentIdentifier))
return launchdTemplate.Remove()
}

View File

@ -28,13 +28,20 @@ import (
"github.com/pkg/errors"
)
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
const configFile = "config.yml"
const (
sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
configFile = "config.yml"
quickStartUrl = "https://developers.cloudflare.com/warp/quickstart/quickstart/"
)
var listeners = gracenet.Net{}
var Version = "DEV"
var BuildTime = "unknown"
var Log *logrus.Logger
var defaultConfigFiles = []string{"config.yml", "config.yaml"}
// Windows default config dir was ~/cloudflare-warp in documentation, let's keep it compatible
var defaultConfigDirs = []string{"~/.cloudflare-warp", "~/cloudflare-warp"}
// Shutdown channel used by the app. When closed, app must terminate.
// May be closed by the Windows service runner.
@ -77,6 +84,7 @@ WARNING:
&cli.StringFlag{
Name: "config",
Usage: "Specifies a config file in YAML format.",
Value: findDefaultConfigPath(),
},
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "autoupdate-freq",
@ -110,7 +118,7 @@ WARNING:
Name: "origincert",
Usage: "Path to the certificate generated for your origin when you run cloudflare-warp login.",
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
Value: filepath.Join(warp.DefaultConfigDir, warp.DefaultCredentialFilename),
Value: findDefaultOriginCertPath(),
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "url",
@ -123,6 +131,11 @@ WARNING:
Usage: "Set a hostname on a Cloudflare zone to route traffic through this tunnel.",
EnvVars: []string{"TUNNEL_HOSTNAME"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "origin-server-name",
Usage: "Hostname on the origin server certificate.",
EnvVars: []string{"TUNNEL_ORIGIN_SERVER_NAME"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "id",
Usage: "A unique identifier used to tie connections to this tunnel instance.",
@ -258,9 +271,15 @@ WARNING:
Log = logrus.New()
inputSource, err := findInputSourceContext(context)
if err != nil {
Log.WithError(err).Infof("Cannot load configuration from %s", context.String("config"))
return err
} else if inputSource != nil {
return altsrc.ApplyInputSourceValues(context, inputSource, app.Flags)
err := altsrc.ApplyInputSourceValues(context, inputSource, app.Flags)
if err != nil {
Log.WithError(err).Infof("Cannot apply configuration from %s", context.String("config"))
return err
}
Log.Infof("Applied configuration from %s", context.String("config"))
}
return nil
}
@ -314,6 +333,7 @@ func startServer(c *cli.Context) {
// c.NumFlags() == 0 && c.NArg() == 0. For warp to work, the user needs to at
// least provide a hostname.
if c.NumFlags() == 0 && c.NArg() == 0 && os.Getenv("TUNNEL_HOSTNAME") == "" {
Log.Infof("No arguments were provided. You need to at least specify the hostname for this tunnel. See %s", quickStartUrl)
cli.ShowAppHelp(c)
return
}
@ -389,6 +409,13 @@ func startServer(c *cli.Context) {
wg.Done()
}()
// ok, err := fileExists(originCertPath)
// if err != nil {
// Log.Fatalf("Cannot check if origin cert exists at path %s", c.String("origincert"))
// }
// if !ok {
// Log.Fatalf(`Cannot find a valid certificate for your origin at the path:
tlsConfig := tlsconfig.CLIFlags{RootCA: "cacert"}.GetConfig(c)
// Start the server
@ -525,25 +552,41 @@ func fileExists(path string) (bool, error) {
return true, nil
}
func findInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
if context.IsSet("config") {
return altsrc.NewYamlSourceFromFile(context.String("config"))
}
dirPath, err := homedir.Expand(warp.DefaultConfigDir)
if err != nil {
return nil, nil
}
for _, path := range []string{
filepath.Join(dirPath, "/config.yml"),
filepath.Join(dirPath, "/config.yaml"),
} {
ok, err := fileExists(path)
if ok {
return altsrc.NewYamlSourceFromFile(path)
} else if err != nil {
return nil, err
// returns the first path that contains a cert.pem file. If none of the defaultConfigDirs
// (differs by OS for legacy reasons) contains a cert.pem file, return empty string
func findDefaultOriginCertPath() string {
for _, defaultConfigDir := range defaultConfigDirs {
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, warp.DefaultCredentialFilename))
if ok, _ := fileExists(originCertPath); ok {
return originCertPath
}
}
return ""
}
// returns the firt path that contains a config file. If none of the combination of
// defaultConfigDirs (differs by OS for legacy reasons) 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 {
return ""
}
path := filepath.Join(dirPath, configFile)
if ok, _ := fileExists(path); ok {
return path
}
}
}
return ""
}
func findInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
if context.String("config") != "" {
return altsrc.NewYamlSourceFromFile(context.String("config"))
}
return nil, nil
}
@ -575,22 +618,26 @@ func validateUrl(c *cli.Context) (string, error) {
}
func initLogFile(c *cli.Context, protoLogger *logrus.Logger) error {
filePath, err := homedir.Expand(c.String("logfile"))
if err != nil {
return errors.Wrap(err, "Cannot resolve logfile path")
}
fileMode := os.O_WRONLY | os.O_APPEND | os.O_CREATE | os.O_TRUNC
// do not truncate log file if the client has been autoupdated
if c.Bool("is-autoupdated") {
fileMode = os.O_WRONLY | os.O_APPEND | os.O_CREATE
}
f, err := os.OpenFile(c.String("logfile"), fileMode, 0664)
f, err := os.OpenFile(filePath, fileMode, 0664)
if err != nil {
errors.Wrap(err, fmt.Sprintf("Cannot open file %s", c.String("logfile")))
errors.Wrap(err, fmt.Sprintf("Cannot open file %s", filePath))
}
defer f.Close()
pathMap := lfshook.PathMap{
logrus.InfoLevel: c.String("logfile"),
logrus.ErrorLevel: c.String("logfile"),
logrus.FatalLevel: c.String("logfile"),
logrus.PanicLevel: c.String("logfile"),
logrus.InfoLevel: filePath,
logrus.ErrorLevel: filePath,
logrus.FatalLevel: filePath,
logrus.PanicLevel: filePath,
}
Log.Hooks.Add(lfshook.NewHook(pathMap, &logrus.JSONFormatter{}))

View File

@ -74,18 +74,21 @@ func runCommand(command string, args ...string) error {
cmd := exec.Command(command, args...)
stderr, err := cmd.StderrPipe()
if err != nil {
Log.WithError(err).Infof("error getting stderr pipe")
return fmt.Errorf("error getting stderr pipe: %v", err)
}
err = cmd.Start()
if err != nil {
Log.WithError(err).Infof("error starting %s", command)
return fmt.Errorf("error starting %s: %v", command, err)
}
commandErr, _ := ioutil.ReadAll(stderr)
if len(commandErr) > 0 {
fmt.Fprintf(os.Stderr, "%s: %s", command, commandErr)
Log.Errorf("%s: %s", command, commandErr)
}
err = cmd.Wait()
if err != nil {
Log.WithError(err).Infof("%s returned error", command)
return fmt.Errorf("%s returned with error: %v", command, err)
}
return nil
@ -117,9 +120,8 @@ func openFile(path string, create bool) (file *os.File, exists bool, err error)
return file, false, err
}
func copyCertificate(configDir string) error {
// Copy certificate
destCredentialPath := filepath.Join(configDir, warp.DefaultCredentialFilename)
func copyCertificate(srcConfigDir, destConfigDir string) error {
destCredentialPath := filepath.Join(destConfigDir, warp.DefaultCredentialFilename)
destFile, exists, err := openFile(destCredentialPath, true)
if err != nil {
return err
@ -129,13 +131,14 @@ func copyCertificate(configDir string) error {
}
defer destFile.Close()
srcCredentialPath := filepath.Join(warp.DefaultConfigDir, warp.DefaultCredentialFilename)
srcCredentialPath := filepath.Join(srcConfigDir, warp.DefaultCredentialFilename)
srcFile, _, err := openFile(srcCredentialPath, false)
if err != nil {
return err
}
defer srcFile.Close()
// Copy certificate
_, err = io.Copy(destFile, srcFile)
if err != nil {
return fmt.Errorf("unable to copy %s to %s: %v", srcCredentialPath, destCredentialPath, err)
@ -144,19 +147,20 @@ func copyCertificate(configDir string) error {
return nil
}
func copyCredentials(configDir string) error {
if err := ensureConfigDirExists(configDir); err != nil {
func copyCredentials(serviceConfigDir, defaultConfigDir, defaultConfigFile string) error {
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
return err
}
if err := copyCertificate(configDir); err != nil {
if err := copyCertificate(defaultConfigDir, serviceConfigDir); err != nil {
return err
}
// Copy or create config
destConfigPath := filepath.Join(configDir, configFile)
destConfigPath := filepath.Join(serviceConfigDir, defaultConfigFile)
destFile, exists, err := openFile(destConfigPath, true)
if err != nil {
Log.WithError(err).Infof("cannot open %s", destConfigPath)
return err
} else if exists {
// config already exists, do nothing
@ -164,7 +168,7 @@ func copyCredentials(configDir string) error {
}
defer destFile.Close()
srcConfigPath := filepath.Join(warp.DefaultConfigDir, configFile)
srcConfigPath := filepath.Join(defaultConfigDir, defaultConfigFile)
srcFile, _, err := openFile(srcConfigPath, false)
if err != nil {
fmt.Println("Your service needs a config file that at least specifies the hostname option.")
@ -182,7 +186,7 @@ func copyCredentials(configDir string) error {
if err != nil {
return fmt.Errorf("unable to copy %s to %s: %v", srcConfigPath, destConfigPath, err)
}
fmt.Printf("Copied %s to %s", srcConfigPath, destConfigPath)
Log.Infof("Copied %s to %s", srcConfigPath, destConfigPath)
}
return nil

View File

@ -9,7 +9,6 @@ import (
"fmt"
"os"
log "github.com/sirupsen/logrus"
cli "gopkg.in/urfave/cli.v2"
"golang.org/x/sys/windows/svc"
@ -42,7 +41,7 @@ func runApp(app *cli.App) {
isIntSess, err := svc.IsAnInteractiveSession()
if err != nil {
log.Fatalf("failed to determine if we are running in an interactive session: %v", err)
Log.Fatalf("failed to determine if we are running in an interactive session: %v", err)
}
if isIntSess {
@ -52,11 +51,14 @@ func runApp(app *cli.App) {
elog, err := eventlog.Open(windowsServiceName)
if err != nil {
Log.WithError(err).Infof("Cannot open event log for %s", windowsServiceName)
return
}
defer elog.Close()
elog.Info(1, fmt.Sprintf("%s service starting", windowsServiceName))
// Run executes service name by calling windowsService which is a Handler
// interface that implements Execute method
err = svc.Run(windowsServiceName, &windowsService{app: app, elog: elog})
if err != nil {
elog.Error(1, fmt.Sprintf("%s service failed: %v", windowsServiceName, err))
@ -70,10 +72,12 @@ type windowsService struct {
elog *eventlog.Log
}
// called by the package code at the start of the service
func (s *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
changes <- svc.Status{State: svc.StartPending}
go s.app.Run(args)
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
loop:
for {
@ -81,8 +85,13 @@ loop:
case c := <-r:
switch c.Cmd {
case svc.Interrogate:
s.elog.Info(1, fmt.Sprintf("control request 1 #%d", c))
changes <- c.CurrentStatus
case svc.Stop, svc.Shutdown:
case svc.Stop:
s.elog.Info(1, "received stop control request")
break loop
case svc.Shutdown:
s.elog.Info(1, "received shutdown control request")
break loop
default:
s.elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
@ -95,50 +104,62 @@ loop:
}
func installWindowsService(c *cli.Context) error {
Log.Infof("Installing Cloudflare Warp Windows service")
exepath, err := os.Executable()
if err != nil {
Log.Infof("Cannot find path name that start the process")
return err
}
m, err := mgr.Connect()
if err != nil {
Log.WithError(err).Infof("Cannot establish a connection to the service control manager")
return err
}
defer m.Disconnect()
s, err := m.OpenService(windowsServiceName)
if err == nil {
s.Close()
Log.Errorf("service %s already exists", windowsServiceName)
return fmt.Errorf("service %s already exists", windowsServiceName)
}
s, err = m.CreateService(windowsServiceName, exepath, mgr.Config{DisplayName: windowsServiceDescription}, "is", "auto-started")
config := mgr.Config{StartType: mgr.StartAutomatic, DisplayName: windowsServiceDescription}
s, err = m.CreateService(windowsServiceName, exepath, config)
if err != nil {
Log.Infof("Cannot install service %s", windowsServiceName)
return err
}
defer s.Close()
err = eventlog.InstallAsEventCreate(windowsServiceName, eventlog.Error|eventlog.Warning|eventlog.Info)
if err != nil {
s.Delete()
Log.WithError(err).Infof("Cannot install event logger")
return fmt.Errorf("SetupEventLogSource() failed: %s", err)
}
return nil
}
func uninstallWindowsService(c *cli.Context) error {
Log.Infof("Uninstalling Cloudflare Warp Windows Service")
m, err := mgr.Connect()
if err != nil {
Log.Infof("Cannot establish a connection to the service control manager")
return err
}
defer m.Disconnect()
s, err := m.OpenService(windowsServiceName)
if err != nil {
Log.Infof("service %s is not installed", windowsServiceName)
return fmt.Errorf("service %s is not installed", windowsServiceName)
}
defer s.Close()
err = s.Delete()
if err != nil {
Log.Errorf("Cannot delete service %s", windowsServiceName)
return err
}
err = eventlog.Remove(windowsServiceName)
if err != nil {
Log.Infof("Cannot remove event logger")
return fmt.Errorf("RemoveEventLogSource() failed: %s", err)
}
return nil

View File

@ -20,7 +20,7 @@ func IsWebSocketUpgrade(req *http.Request) bool {
// ClientConnect creates a WebSocket client connection for provided request. Caller is responsible for closing.
func ClientConnect(req *http.Request, tlsClientConfig *tls.Config) (*websocket.Conn, *http.Response, error) {
req.URL.Scheme = "wss"
req.URL.Scheme = changeRequestScheme(req)
d := &websocket.Dialer{TLSClientConfig: tlsClientConfig}
conn, response, err := d.Dial(req.URL.String(), nil)
if err != nil {
@ -75,3 +75,16 @@ func sha1Base64(str string) string {
func generateAcceptKey(req *http.Request) string {
return sha1Base64(req.Header.Get("Sec-WebSocket-Key") + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
}
// changeRequestScheme is needed as the gorilla websocket library requires the ws scheme.
// (even though it changes it back to http/https, but ¯\_(ツ)_/¯.)
func changeRequestScheme(req *http.Request) string {
switch req.URL.Scheme {
case "https":
return "wss"
case "http":
return "ws"
default:
return req.URL.Scheme
}
}