diff --git a/cmd/cloudflared/access/carrier.go b/cmd/cloudflared/access/carrier.go index 52aacc5655f..202e6b16e2d 100644 --- a/cmd/cloudflared/access/carrier.go +++ b/cmd/cloudflared/access/carrier.go @@ -12,6 +12,8 @@ import ( "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/carrier" + "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" + cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/stream" @@ -125,6 +127,9 @@ func ssh(c *cli.Context) error { return errors.Wrap(err, "error validating origin URL") } log.Info().Str(LogFieldHost, forwarder.Host).Msg("Start Websocket listener") + if pidfile := c.String(cfdflags.Pidfile); pidfile != "" { + _ = cliutil.WritePidFile(pidfile, log) + } err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options) if err != nil { log.Err(err).Msg("Error on Websocket listener") @@ -143,5 +148,8 @@ func ssh(c *cli.Context) error { logger := log.With().Str("host", url.Host).Logger() s = stream.NewDebugStream(s, &logger, maxMessages) } + if pidfile := c.String(cfdflags.Pidfile); pidfile != "" { + _ = cliutil.WritePidFile(pidfile, log) + } return carrier.StartClient(wsConn, s, options) } diff --git a/cmd/cloudflared/access/cmd.go b/cmd/cloudflared/access/cmd.go index 636b9288e27..f5440ad49cf 100644 --- a/cmd/cloudflared/access/cmd.go +++ b/cmd/cloudflared/access/cmd.go @@ -80,10 +80,17 @@ func Commands() []*cli.Command { Aliases: []string{"forward"}, Category: "Access", Usage: "access ", - Flags: []cli.Flag{&cli.BoolFlag{ - Name: fedrampFlag, - Usage: "use when performing operations in fedramp account", - }}, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: fedrampFlag, + Usage: "use when performing operations in fedramp account", + }, + &cli.StringFlag{ + Name: cfdflags.Pidfile, + Usage: "Write the application's PID to this file after first successful connection.", + EnvVars: []string{"TUNNEL_PIDFILE"}, + }, + }, Description: `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 @@ -204,6 +211,11 @@ func Commands() []*cli.Command { Hidden: true, Usage: "Writes up-to the max provided stream payloads to the logger as debug statements.", }, + &cli.StringFlag{ + Name: cfdflags.Pidfile, + Usage: "Write the application's PID to this file after first successful connection.", + EnvVars: []string{"TUNNEL_PIDFILE"}, + }, }, }, { diff --git a/cmd/cloudflared/access/pidfile_test.go b/cmd/cloudflared/access/pidfile_test.go new file mode 100644 index 00000000000..63fff4e46f7 --- /dev/null +++ b/cmd/cloudflared/access/pidfile_test.go @@ -0,0 +1,56 @@ +package access + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v2" + + cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" +) + +func TestAccessCommandHasPidfileFlag(t *testing.T) { + t.Parallel() + + cmds := Commands() + require.NotEmpty(t, cmds) + + var accessCmd *cli.Command + for _, cmd := range cmds { + if cmd.Name == "access" { + accessCmd = cmd + break + } + } + require.NotNil(t, accessCmd) + + var accessPidFlag *cli.StringFlag + for _, f := range accessCmd.Flags { + if sf, ok := f.(*cli.StringFlag); ok && sf.Name == cfdflags.Pidfile { + accessPidFlag = sf + break + } + } + require.NotNil(t, accessPidFlag, "access command should have --pidfile flag") + assert.Contains(t, accessPidFlag.EnvVars, "TUNNEL_PIDFILE") + + var tcpCmd *cli.Command + for _, sub := range accessCmd.Subcommands { + if sub.Name == "tcp" { + tcpCmd = sub + break + } + } + require.NotNil(t, tcpCmd) + + var tcpPidFlag *cli.StringFlag + for _, f := range tcpCmd.Flags { + if sf, ok := f.(*cli.StringFlag); ok && sf.Name == cfdflags.Pidfile { + tcpPidFlag = sf + break + } + } + require.NotNil(t, tcpPidFlag, "access tcp subcommand should have --pidfile flag") + assert.Contains(t, tcpPidFlag.EnvVars, "TUNNEL_PIDFILE") +} diff --git a/cmd/cloudflared/cliutil/pidfile.go b/cmd/cloudflared/cliutil/pidfile.go new file mode 100644 index 00000000000..37e513cd340 --- /dev/null +++ b/cmd/cloudflared/cliutil/pidfile.go @@ -0,0 +1,33 @@ +package cliutil + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/mitchellh/go-homedir" + "github.com/rs/zerolog" +) + +const ( + LogFieldPIDPathname = "pidPathname" + LogFieldExpandedPath = "expandedPath" +) + +// WritePidFile writes the current process ID into the specified path. +func WritePidFile(pidPathname string, log *zerolog.Logger) error { + expandedPath, err := homedir.Expand(pidPathname) + if err != nil { + log.Err(err).Str(LogFieldPIDPathname, pidPathname).Msg("Unable to expand the path, try to use absolute path in --pidfile") + return err + } + cleanPath := filepath.Clean(expandedPath) + file, err := os.Create(cleanPath) + if err != nil { + log.Err(err).Str(LogFieldExpandedPath, expandedPath).Msg("Unable to write pid") + return err + } + defer func() { _ = file.Close() }() + _, err = fmt.Fprintf(file, "%d", os.Getpid()) + return err +} diff --git a/cmd/cloudflared/cliutil/pidfile_test.go b/cmd/cloudflared/cliutil/pidfile_test.go new file mode 100644 index 00000000000..ab686afa28c --- /dev/null +++ b/cmd/cloudflared/cliutil/pidfile_test.go @@ -0,0 +1,61 @@ +package cliutil + +import ( + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWritePidFile_Success(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + pidPath := filepath.Join(tempDir, "test.pid") + logger := zerolog.Nop() + + err := WritePidFile(pidPath, &logger) + require.NoError(t, err) + + content, err := os.ReadFile(pidPath) + require.NoError(t, err) + + pid, err := strconv.Atoi(strings.TrimSpace(string(content))) + require.NoError(t, err) + assert.Equal(t, os.Getpid(), pid) +} + +func TestWritePidFile_InvalidPath(t *testing.T) { + t.Parallel() + + // An impossible path where a directory cannot be written into + pidPath := filepath.Join("/nonexistent_dir_12345", "sub", "test.pid") + logger := zerolog.Nop() + + err := WritePidFile(pidPath, &logger) + require.Error(t, err) +} + +func TestWritePidFile_HomedirExpansion(t *testing.T) { + tempHome := t.TempDir() + t.Setenv("HOME", tempHome) + + pidPath := "~/test_homedir.pid" + logger := zerolog.Nop() + + err := WritePidFile(pidPath, &logger) + require.NoError(t, err) + + expectedPath := filepath.Join(tempHome, "test_homedir.pid") + content, err := os.ReadFile(expectedPath) + require.NoError(t, err) + + pid, err := strconv.Atoi(strings.TrimSpace(string(content))) + require.NoError(t, err) + assert.Equal(t, os.Getpid(), pid) +} diff --git a/cmd/cloudflared/flags/flags.go b/cmd/cloudflared/flags/flags.go index a792bc808ce..39e76f892ee 100644 --- a/cmd/cloudflared/flags/flags.go +++ b/cmd/cloudflared/flags/flags.go @@ -175,4 +175,7 @@ const ( // Automatically close the login interstitial browser window after the user makes a decision. AutoCloseInterstitial = "auto-close" + + // Pidfile writes the application's process ID to a file. + Pidfile = "pidfile" ) diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 30a4862d39a..54625807847 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -14,7 +14,6 @@ import ( "github.com/coreos/go-systemd/v22/daemon" "github.com/facebookgo/grace/gracenet" "github.com/getsentry/sentry-go" - "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/urfave/cli/v2" @@ -615,19 +614,7 @@ func notifySystemd(waitForSignal *signal.Signal) { func writePidFile(waitForSignal *signal.Signal, pidPathname string, log *zerolog.Logger) { <-waitForSignal.Wait() - expandedPath, err := homedir.Expand(pidPathname) - if err != nil { - log.Err(err).Str(LogFieldPIDPathname, pidPathname).Msg("Unable to expand the path, try to use absolute path in --pidfile") - return - } - cleanPath := filepath.Clean(expandedPath) - file, err := os.Create(cleanPath) - if err != nil { - log.Err(err).Str(LogFieldExpandedPath, expandedPath).Msg("Unable to write pid") - return - } - defer func() { _ = file.Close() }() - _, _ = fmt.Fprintf(file, "%d", os.Getpid()) + _ = cliutil.WritePidFile(pidPathname, log) } func hostnameFromURI(uri string) string {