diff --git a/cmd/cloudflare-warp/linux_service.go b/cmd/cloudflare-warp/linux_service.go index 755fd97c..93f871c0 100644 --- a/cmd/cloudflare-warp/linux_service.go +++ b/cmd/cloudflare-warp/linux_service.go @@ -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 } diff --git a/cmd/cloudflare-warp/macos_service.go b/cmd/cloudflare-warp/macos_service.go index 9b50a5b3..5acd1e29 100644 --- a/cmd/cloudflare-warp/macos_service.go +++ b/cmd/cloudflare-warp/macos_service.go @@ -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: ` + Content: fmt.Sprintf(` Label - com.cloudflare.warp + %s Program {{ .Path }} RunAtLoad + StandardOutPath + /tmp/%s.out.log + StandardErrorPath + /tmp/%s.err.log KeepAlive NetworkState @@ -49,34 +55,43 @@ var launchdTemplate = ServiceTemplate{ ThrottleInterval 20 -`, +`, 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() } diff --git a/cmd/cloudflare-warp/main.go b/cmd/cloudflare-warp/main.go index 48a7c065..5b24cd60 100644 --- a/cmd/cloudflare-warp/main.go +++ b/cmd/cloudflare-warp/main.go @@ -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{})) diff --git a/cmd/cloudflare-warp/service_template.go b/cmd/cloudflare-warp/service_template.go index 72e6e2f6..8585a003 100644 --- a/cmd/cloudflare-warp/service_template.go +++ b/cmd/cloudflare-warp/service_template.go @@ -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 diff --git a/cmd/cloudflare-warp/windows_service.go b/cmd/cloudflare-warp/windows_service.go index 2dea8120..26524c37 100644 --- a/cmd/cloudflare-warp/windows_service.go +++ b/cmd/cloudflare-warp/windows_service.go @@ -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 diff --git a/websocket/websocket.go b/websocket/websocket.go index d821c706..c70b2a7d 100644 --- a/websocket/websocket.go +++ b/websocket/websocket.go @@ -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 + } +}