github.com/ericwq/aprilsh@v0.0.0-20240517091432-958bc568daa0/frontend/server/server.go (about)

     1  // Copyright 2022~2024 wangqi. All rights reserved.
     2  // Use of this source code is governed by a MIT-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"context"
    11  	"errors"
    12  	"flag"
    13  	"fmt"
    14  	"io"
    15  	"math"
    16  	"net"
    17  	"os"
    18  	"os/signal"
    19  	"os/user"
    20  	"path/filepath"
    21  	"reflect"
    22  	"sort"
    23  	"strconv"
    24  	"strings"
    25  	"sync"
    26  	"syscall"
    27  	"time"
    28  
    29  	"log/slog"
    30  	"log/syslog"
    31  
    32  	"github.com/creack/pty"
    33  	"github.com/ericwq/aprilsh/encrypt"
    34  	"github.com/ericwq/aprilsh/frontend"
    35  	"github.com/ericwq/aprilsh/network"
    36  	"github.com/ericwq/aprilsh/statesync"
    37  	"github.com/ericwq/aprilsh/terminal"
    38  	"github.com/ericwq/aprilsh/util"
    39  	utmps "github.com/ericwq/goutmp"
    40  	"golang.org/x/sync/errgroup"
    41  	"golang.org/x/sys/unix"
    42  )
    43  
    44  const (
    45  	_PATH_BSHELL = "/bin/sh"
    46  
    47  	_FC_OPEN_PTS_FAIL    = 100 // open pts failed.
    48  	_FC_SKIP_START_SHELL = 101 // skip startShell() entirely.
    49  	_FC_SKIP_PIPE_LOCK   = 102 // skip pipe lock for start shell.
    50  	_FC_DEF_BASH_SHELL   = 103 // use default bash shell
    51  	_FC_NON_UTF8_LOCALE  = 104 // non utf8 locale
    52  
    53  	_ServeHeader = "serve"
    54  	_RunHeader   = "run"
    55  	_KeyHeader   = "key"
    56  	_ShellHeader = "shell"
    57  
    58  	envArgs   = "APRILSH_ARGS"
    59  	envUDS    = "APRILSH_UDS"
    60  	apshPath  = "APRILSH_APSH_PATH"  // executable client file path for testing
    61  	apshdPath = "APRILSH_APSHD_PATH" // executable server file path for testing
    62  
    63  	earlyShutdown = "early-shutdown"
    64  )
    65  
    66  var usage = `Usage:
    67    ` + frontend.CommandServerName + ` [-version] [-h] [--auto N]
    68    ` + frontend.CommandServerName + ` [-b] [-t TERM] [-destination user@server.domain]
    69    ` + frontend.CommandServerName + ` [-s] [-v[v]] [-i LOCALADDR] [-p PORT[:PORT2]] [-l NAME=VALUE] [-- command...]
    70  Options:
    71  ---------------------------------------------------------------------------------------------------
    72    -h,  --help        print this message
    73    -a,  --auto        auto stop the server after N seconds
    74         --version     print version information
    75  ---------------------------------------------------------------------------------------------------
    76    -b,  --begin       begin a client connection
    77    -t,  --term        client TERM (such as xterm-256color, or alacritty or xterm-kitty)
    78    -d,  --destination in the form of user@host[:port], here the port is ssh server port (default 22)
    79  ---------------------------------------------------------------------------------------------------
    80    -s,  --server      listen with SSH ip
    81    -i,  --ip          listen with this ip/host
    82    -p,  --port        listen base port (default 8100)
    83    -l,  --locale      key-value pairs (such as LANG=UTF-8, you can have multiple -l options)
    84    -v,  --verbose     verbose log output (debug level, default no verbose)
    85    -vv                verbose log output (trace level)
    86         -- command    shell command and options (note the space before command)
    87  ---------------------------------------------------------------------------------------------------
    88  `
    89  
    90  var failToStartShell = errors.New("fail to start shell")
    91  
    92  var (
    93  	syslogSupport bool
    94  	syslogWriter  *syslog.Writer
    95  
    96  	signals frontend.Signals
    97  
    98  	maxPortLimit = 100 // assume 10 concurrent users, each owns 10 connections
    99  
   100  	funcGetRecord func() *utmps.Utmpx // easy for testing
   101  	utmpSupport   bool
   102  )
   103  
   104  func init() {
   105  	utmpSupport = utmps.HasUtmpSupport()
   106  	funcGetRecord = utmps.GetRecord
   107  }
   108  
   109  // https://www.antoniojgutierrez.com/posts/2021-05-14-short-and-long-options-in-go-flags-pkg/
   110  type localeFlag map[string]string
   111  
   112  func (lv *localeFlag) String() string {
   113  	return fmt.Sprint(*lv)
   114  }
   115  
   116  func (lv *localeFlag) Set(value string) error {
   117  	kv := strings.Split(value, "=")
   118  	if len(kv) != 2 {
   119  		return errors.New("malform locale parameter: " + value)
   120  	}
   121  
   122  	(*lv)[kv[0]] = kv[1]
   123  	return nil
   124  }
   125  
   126  func (lv *localeFlag) IsBoolFlag() bool {
   127  	return false
   128  }
   129  
   130  type Config struct {
   131  	version     bool       // print version information
   132  	server      bool       // use SSH ip
   133  	verbose     int        // verbose output
   134  	desiredIP   string     // server ip/host
   135  	desiredPort string     // server port
   136  	locales     localeFlag // localse environment variables
   137  	term        string     // client TERM
   138  	autoStop    int        // auto stop after N seconds
   139  	begin       bool       // begin a client connection
   140  	child       bool       // begin a child process
   141  	destination string     // [user@]hostname, destination string
   142  	host        string     // target host/server
   143  	user        string     // target user
   144  	addSource   bool       // add source file to log
   145  	flowControl int        // control flow for testing
   146  
   147  	commandPath string   // shell command path (absolute path)
   148  	commandArgv []string // the positional (non-flag) command-line arguments.
   149  	withMotd    bool
   150  
   151  	// the serve func
   152  	serve func(*os.File, *os.File, *io.PipeWriter, *statesync.Complete, // chan bool,
   153  		*network.Transport[*statesync.Complete, *statesync.UserStream], int64, int64, string) error
   154  }
   155  
   156  // generate shell for specified user or current user if user is nil.
   157  func (conf *Config) prepareShell(user *user.User) {
   158  	// Get shell
   159  	if len(conf.commandArgv) == 0 {
   160  		var shell string
   161  		var err error
   162  		if user == nil {
   163  			shell = os.Getenv("SHELL")
   164  			if len(shell) == 0 {
   165  				shell, _ = util.GetShell() // another way to get shell path
   166  			}
   167  		} else {
   168  			shell, err = util.GetShell4(user)
   169  			if err != nil {
   170  				util.Logger.Warn("prepareShell failed", "user", user.Username, "error", err)
   171  			}
   172  		}
   173  
   174  		shellPath := shell
   175  		if len(shellPath) == 0 || conf.flowControl == _FC_DEF_BASH_SHELL { // empty shell means Bourne shell
   176  			shellPath = _PATH_BSHELL
   177  		}
   178  
   179  		conf.commandPath = shellPath
   180  
   181  		shellName := getShellNameFrom(shellPath)
   182  
   183  		conf.commandArgv = []string{shellName}
   184  
   185  		conf.withMotd = true
   186  	}
   187  
   188  	if len(conf.commandPath) == 0 {
   189  		conf.commandPath = conf.commandArgv[0]
   190  
   191  		if len(conf.commandArgv) == 1 {
   192  			shellName := getShellNameFrom(conf.commandPath)
   193  			conf.commandArgv = []string{shellName}
   194  		} else {
   195  			conf.commandArgv = conf.commandArgv[1:]
   196  		}
   197  	}
   198  }
   199  
   200  // build the config instance and check the utf-8 locale. return error if the terminal
   201  // can't support utf-8 locale.
   202  func (conf *Config) buildConfig() (string, bool) {
   203  	// just need version info
   204  	if conf.version {
   205  		return "", true
   206  	}
   207  
   208  	if conf.server {
   209  		if sshIP, ok := getSSHip(); ok {
   210  			conf.desiredIP = sshIP
   211  		} else {
   212  			msg := sshIP
   213  			return msg, false
   214  		}
   215  	}
   216  
   217  	if len(conf.desiredPort) > 0 {
   218  		// Sanity-check arguments
   219  
   220  		// fmt.Printf("#main desiredPort=%s\n", conf.desiredPort)
   221  		_, _, ok := network.ParsePortRange(conf.desiredPort)
   222  		if !ok {
   223  			return fmt.Sprintf("Bad UDP port (%s)", conf.desiredPort), false
   224  		}
   225  	}
   226  
   227  	conf.commandPath = ""
   228  	conf.withMotd = false
   229  	conf.serve = serve
   230  
   231  	conf.prepareShell(nil)
   232  
   233  	// Adopt implementation locale
   234  	util.SetNativeLocale()
   235  	if !util.IsUtf8Locale() || conf.flowControl == _FC_NON_UTF8_LOCALE {
   236  		nativeType := util.GetCtype()
   237  		nativeCharset := util.LocaleCharset()
   238  
   239  		// apply locale-related environment variables from client
   240  		util.ClearLocaleVariables()
   241  		for k, v := range conf.locales {
   242  			// fmt.Printf("#buildConfig setenv %s=%s\n", k, v)
   243  			os.Setenv(k, v)
   244  		}
   245  
   246  		// check again
   247  		util.SetNativeLocale()
   248  		if !util.IsUtf8Locale() || conf.flowControl == _FC_NON_UTF8_LOCALE {
   249  			clientType := util.GetCtype()
   250  			clientCharset := util.LocaleCharset()
   251  			fmt.Printf("%s needs a UTF-8 native locale to run.\n", frontend.CommandServerName)
   252  			fmt.Printf("Unfortunately, the local environment %s specifies "+
   253  				"the character set \"%s\",\n", nativeType, nativeCharset)
   254  			fmt.Printf("The client-supplied environment %s specifies "+
   255  				"the character set \"%s\".\n", clientType, clientCharset)
   256  
   257  			return "UTF-8 locale fail.", false
   258  		}
   259  	}
   260  	return "", true
   261  }
   262  
   263  // parseFlags parses the command-line arguments provided to the program.
   264  // Typically os.Args[0] is provided as 'progname' and os.args[1:] as 'args'.
   265  // Returns the Config in case parsing succeeded, or an error. In any case, the
   266  // output of the flag.Parse is returned in output.
   267  // A special case is usage requests with -h or -help: then the error
   268  // flag.ErrHelp is returned and output will contain the usage message.
   269  func parseFlags(progname string, args []string) (config *Config, output string, err error) {
   270  	// https://eli.thegreenplace.net/2020/testing-flag-parsing-in-go-programs/
   271  	flagSet := flag.NewFlagSet(progname, flag.ContinueOnError)
   272  	var buf bytes.Buffer
   273  	flagSet.SetOutput(&buf)
   274  
   275  	var conf Config
   276  	conf.locales = make(localeFlag)
   277  	conf.commandArgv = []string{}
   278  
   279  	// flagSet.IntVar(&conf.verbose, "verbose", 0, "verbose output")
   280  	var v1, v2 bool
   281  	flagSet.BoolVar(&v1, "v", false, "verbose log output debug level")
   282  	flagSet.BoolVar(&v1, "verbose", false, "verbose log output debug levle")
   283  	flagSet.BoolVar(&v2, "vv", false, "verbose log output trace level")
   284  
   285  	flagSet.BoolVar(&conf.addSource, "source", false, "add source info to log")
   286  
   287  	flagSet.IntVar(&conf.autoStop, "auto", 0, "auto stop after N seconds")
   288  	flagSet.IntVar(&conf.autoStop, "a", 0, "auto stop after N seconds")
   289  
   290  	flagSet.BoolVar(&conf.version, "version", false, "print version information")
   291  	// flagSet.BoolVar(&conf.version, "v", false, "print version information")
   292  
   293  	flagSet.BoolVar(&conf.begin, "begin", false, "begin a client connection")
   294  	flagSet.BoolVar(&conf.begin, "b", false, "begin a client connection")
   295  
   296  	flagSet.BoolVar(&conf.child, "child", false, "begin child process")
   297  	flagSet.BoolVar(&conf.child, "c", false, "begin child process")
   298  
   299  	flagSet.BoolVar(&conf.server, "server", false, "listen with SSH ip")
   300  	flagSet.BoolVar(&conf.server, "s", false, "listen with SSH ip")
   301  
   302  	flagSet.StringVar(&conf.desiredIP, "ip", "", "listen ip")
   303  	flagSet.StringVar(&conf.desiredIP, "i", "", "listen ip")
   304  
   305  	flagSet.StringVar(&conf.desiredPort, "port", strconv.Itoa(frontend.DefaultPort), "listen port range")
   306  	flagSet.StringVar(&conf.desiredPort, "p", strconv.Itoa(frontend.DefaultPort), "listen port range")
   307  
   308  	flagSet.StringVar(&conf.term, "term", "", "client TERM")
   309  	flagSet.StringVar(&conf.term, "t", "", "client TERM")
   310  
   311  	flagSet.StringVar(&conf.destination, "destination", "", "destination string")
   312  
   313  	flagSet.Var(&conf.locales, "locale", "locale list, key=value pair")
   314  	flagSet.Var(&conf.locales, "l", "locale list, key=value pair")
   315  
   316  	err = flagSet.Parse(args)
   317  	if err != nil {
   318  		return nil, buf.String(), err
   319  	}
   320  
   321  	// check the format of desiredPort
   322  	// _, err = strconv.Atoi(conf.desiredPort)
   323  	// if err != nil {
   324  	// 	return nil, buf.String(), err
   325  	// }
   326  
   327  	// get the non-flag command-line arguments.
   328  	conf.commandArgv = flagSet.Args()
   329  
   330  	// detremine verbose level
   331  	if v1 {
   332  		conf.verbose = util.DebugLevel
   333  	} else if v2 {
   334  		conf.verbose = util.TraceLevel
   335  	}
   336  
   337  	return &conf, buf.String(), nil
   338  }
   339  
   340  func printVersion() {
   341  	fmt.Printf("%s package : %s server, %s\n",
   342  		frontend.AprilshPackageName, frontend.AprilshPackageName, frontend.CommandServerName)
   343  	frontend.PrintVersion()
   344  }
   345  
   346  // func printUsage(hint string, usage ...string) {
   347  // 	if hint != "" {
   348  // 		fmt.Printf("Hints: %s\n%s", hint, usage)
   349  // 	} else {
   350  // 		fmt.Printf("%s", usage)
   351  // 	}
   352  // }
   353  
   354  func beginChild(conf *Config) { //(port string, term string) {
   355  	// Unlike Dial, ListenPacket creates a connection without any
   356  	// association with peers.
   357  	conn, _ := net.ListenPacket("udp", ":0")
   358  	defer conn.Close()
   359  	// conn, err := net.ListenPacket("udp", ":0")
   360  	// if err != nil {
   361  	// 	fmt.Println(err)
   362  	// 	return
   363  	// }
   364  
   365  	dest, _ := net.ResolveUDPAddr("udp", "localhost:"+conf.desiredPort)
   366  	// dest, err := net.ResolveUDPAddr("udp", "localhost:"+conf.desiredPort)
   367  	// if err != nil {
   368  	// 	fmt.Println(err)
   369  	// 	return
   370  	// }
   371  
   372  	// request from server
   373  	// open aprilsh:TERM,user@server.domain
   374  	request := fmt.Sprintf("%s%s,%s", frontend.AprilshMsgOpen, conf.term, conf.destination)
   375  	conn.SetDeadline(time.Now().Add(time.Millisecond * 20))
   376  	conn.WriteTo([]byte(request), dest)
   377  	// n, err := conn.WriteTo([]byte(request), dest)
   378  	// if err != nil {
   379  	// 	fmt.Println("write to udp: ", err)
   380  	// 	return
   381  	// } else if n != len(request) {
   382  	// 	fmt.Println("can't send correct query.")
   383  	// 	return
   384  	// }
   385  
   386  	// read the response
   387  	response := make([]byte, 128)
   388  	conn.SetDeadline(time.Now().Add(time.Millisecond * 200))
   389  	m, _, err := conn.ReadFrom(response)
   390  	if err != nil {
   391  		fmt.Println(err)
   392  		return
   393  	}
   394  
   395  	fmt.Printf("%s", string(response[:m]))
   396  }
   397  
   398  const (
   399  	unixsockNetwork = "unixgram"
   400  )
   401  
   402  // "/tmp/aprilsh.sock"
   403  var unixsockAddr string = filepath.Join(os.TempDir(), "aprilsh-{}.sock")
   404  
   405  type uxClient struct {
   406  	connection net.Conn
   407  }
   408  
   409  func newUxClient() (client *uxClient, err error) {
   410  	client = &uxClient{}
   411  	client.connection, err = net.Dial(unixsockNetwork, unixsockAddr)
   412  	return
   413  }
   414  
   415  func (uc *uxClient) send(msg string) (err error) {
   416  	_, err = uc.connection.Write([]byte(msg))
   417  	// util.Logger.Debug("uxClient send", "message", msg)
   418  	return
   419  }
   420  
   421  func (uc *uxClient) close() (err error) {
   422  	return uc.connection.Close()
   423  }
   424  
   425  func uxCleanup() (err error) {
   426  	if _, err = os.Stat(unixsockAddr); err == nil {
   427  		if err = os.RemoveAll(unixsockAddr); err != nil {
   428  			return err
   429  		}
   430  	}
   431  	err = nil // doesn't exist
   432  	return
   433  }
   434  
   435  func uxForward(target chan string, msg string) {
   436  	// util.Logger.Debug("uxServe forward message to exChan", "msg", msg)
   437  	target <- msg
   438  }
   439  
   440  type workhorse struct {
   441  	child *os.Process
   442  	// ptmx     *os.File
   443  	shellPid int
   444  }
   445  
   446  type mainSrv struct {
   447  	workers map[int]*workhorse
   448  	// runWorker  func(*Config, chan string, chan workhorse) error // worker
   449  	exChan     chan string    // worker done or passing key
   450  	whChan     chan workhorse // workhorse
   451  	downChan   chan bool      // shutdown mainSrv
   452  	uxdownChan chan bool      // ux shutdown mainSrv
   453  	maxPort    int            // max worker port
   454  	timeout    int            // read udp time out,
   455  	port       int            // main listen port
   456  	conn       *net.UDPConn   // mainSrv listen port
   457  	wg         sync.WaitGroup
   458  }
   459  
   460  // func newMainSrv(conf *Config, runWorker func(*Config, chan string, chan workhorse) error) *mainSrv {
   461  func newMainSrv(conf *Config) *mainSrv {
   462  	m := mainSrv{}
   463  	// m.runWorker = runWorker
   464  	m.port, _ = strconv.Atoi(conf.desiredPort)
   465  	m.maxPort = m.port + 1
   466  	m.workers = make(map[int]*workhorse)
   467  	m.downChan = make(chan bool, 1)
   468  	m.uxdownChan = make(chan bool, 1)
   469  	m.exChan = make(chan string, 1)
   470  	m.whChan = make(chan workhorse, 1)
   471  	m.timeout = 20
   472  
   473  	return &m
   474  }
   475  
   476  // start mainSrv, which listen on the main udp port.
   477  // each new client send a shake hands message to mainSrv. mainSrv response
   478  // with the session key and target udp port for the new client.
   479  // mainSrv is shutdown by SIGTERM and all sessions must be done.
   480  // otherwise mainSrv will wait for the live session.
   481  func (m *mainSrv) start(conf *Config) {
   482  	// listen the port
   483  	if err := m.listen(conf); err != nil {
   484  		util.Logger.Warn("listen failed", "error", err)
   485  		return
   486  	}
   487  
   488  	uxConn, err := m.uxListen()
   489  	if err != nil {
   490  		util.Logger.Warn("listen unix domain socket failed", "error", err)
   491  		return
   492  	}
   493  
   494  	// start main server waiting for open/close message.
   495  	m.wg.Add(1)
   496  	go func() {
   497  		m.run(conf)
   498  		m.wg.Done()
   499  	}()
   500  
   501  	// start unix domain socket (datagram)
   502  	m.wg.Add(1)
   503  	go func() {
   504  		m.uxServe(uxConn, 2, uxForward)
   505  		m.wg.Done()
   506  	}()
   507  
   508  	// shutdown if the auto stop flag is set
   509  	if conf.autoStop > 0 {
   510  		time.AfterFunc(time.Duration(conf.autoStop)*time.Second, func() {
   511  			m.downChan <- true
   512  		})
   513  	}
   514  }
   515  
   516  func (m *mainSrv) wait() {
   517  	m.wg.Wait()
   518  	util.Logger.Info("quit " + frontend.CommandServerName)
   519  }
   520  
   521  /*
   522  upon receive frontend.AprilshMsgOpen message, run() stat a new worker
   523  to serve the client, response to the client with choosen port number
   524  and session key.
   525  
   526  sample request  : open aprilsh:TERM,user@server.domain
   527  
   528  sample response : open aprilsh:60001,31kR3xgfmNxhDESXQ8VIQw==
   529  
   530  upon receive frontend.AprishMsgClose message, run() stop the worker
   531  specified by port number.
   532  
   533  sample request  : close aprilsh:60001
   534  
   535  sample response : close aprilsh:done
   536  
   537  when shutdown message is received (via SIGTERM or SIGINT), run() will send
   538  sutdown message to all workers and wait for the workers to finish. when
   539  -auto flag is set, run() will shutdown after specified seconds.
   540  */
   541  func (m *mainSrv) run(conf *Config) {
   542  	if m.conn == nil {
   543  		return
   544  	}
   545  	// prepare to receive the signal
   546  	sig := make(chan os.Signal, 1)
   547  	signal.Notify(sig, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
   548  
   549  	// clean up
   550  	defer func() {
   551  		signal.Stop(sig)
   552  		if syslogSupport {
   553  			syslogWriter.Info(fmt.Sprintf("stop listening on %s.", m.conn.LocalAddr()))
   554  		}
   555  		util.Logger.Info("stop listening on", "port", m.port)
   556  		m.conn.Close()
   557  	}()
   558  
   559  	buf := make([]byte, 128)
   560  	shutdown := false
   561  
   562  	if syslogSupport {
   563  		syslogWriter.Info(fmt.Sprintf("start listening on %s.", m.conn.LocalAddr()))
   564  	}
   565  	util.Logger.Info("start listening on", "port", m.port, "gitTag", frontend.GitTag)
   566  
   567  	//TODO remove it?
   568  	// printWelcome(os.Getpid(), m.port, nil)
   569  	// printWelcome(nil)
   570  	for {
   571  		select {
   572  		case msg := <-m.exChan:
   573  			_, err := m.handleMessage(msg)
   574  			if len(m.workers) > 0 {
   575  				for port, wh := range m.workers {
   576  					util.Logger.Debug("there are clients:", "port", port, "childPid", wh.child.Pid)
   577  				}
   578  			} else {
   579  				util.Logger.Debug("there is no client remains")
   580  			}
   581  			if err != nil {
   582  				util.Logger.Warn("child failed", "error", err, "oldmsg", msg)
   583  			}
   584  		case ss := <-sig:
   585  			switch ss {
   586  			case syscall.SIGHUP: // TODO:reload the config?
   587  				util.Logger.Info("got signal: SIGHUP", "receiver", "run2")
   588  			case syscall.SIGTERM, syscall.SIGINT:
   589  				util.Logger.Info("got signal: SIGTERM or SIGINT", "receiver", "run2")
   590  				shutdown = true
   591  			}
   592  		case <-m.downChan: // another way to shutdown besides signal
   593  			util.Logger.Debug("got shutdown signal")
   594  			shutdown = true
   595  		default:
   596  		}
   597  
   598  		if shutdown {
   599  			m.shutdown()
   600  			return
   601  		}
   602  
   603  		// set read time out: 200ms
   604  		m.conn.SetDeadline(time.Now().Add(time.Millisecond * time.Duration(m.timeout)))
   605  		n, addr, err := m.conn.ReadFromUDP(buf)
   606  		if err != nil {
   607  			if errors.Is(err, os.ErrDeadlineExceeded) {
   608  				// fmt.Printf("#run read time out, workers=%d, shutdown=%t, err=%s\n", len(m.workers), shutdown, err)
   609  				continue
   610  			} else {
   611  				// take a break in case reading error
   612  				time.Sleep(time.Duration(5) * time.Millisecond)
   613  				// fmt.Println("#run read error: ", err)
   614  				continue
   615  			}
   616  		}
   617  
   618  		req := strings.TrimSpace(string(buf[0:n]))
   619  		if strings.HasPrefix(req, frontend.AprilshMsgOpen) { // 'open aprilsh:'
   620  			m.startChild(req, addr, *conf)
   621  		} else if strings.HasPrefix(req, frontend.AprishMsgClose) { // 'close aprilsh:[port]'
   622  			m.closeChild(req, addr)
   623  		} else {
   624  			resp := m.writeRespTo(addr, frontend.AprishMsgClose, "unknow request")
   625  			util.Logger.Warn("unknow request", "request", req, "response", resp)
   626  		}
   627  	}
   628  }
   629  
   630  // to support multiple clients, mainServer listen on the specified port.
   631  // for security reason, we only listen on localhost port.
   632  func (m *mainSrv) listen(conf *Config) error {
   633  	local_addr, err := net.ResolveUDPAddr("udp", "localhost:"+conf.desiredPort)
   634  	if err != nil {
   635  		return err
   636  	}
   637  
   638  	m.conn, err = net.ListenUDP("udp", local_addr)
   639  	if err != nil {
   640  		return err
   641  	}
   642  
   643  	return nil
   644  }
   645  
   646  func (m *mainSrv) uxListen() (conn *net.UnixConn, err error) {
   647  	if err = uxCleanup(); err != nil {
   648  		return
   649  	}
   650  
   651  	unixsockAddr = strings.Replace(unixsockAddr, "{}", strconv.Itoa(os.Getpid()), 1)
   652  	addr, _ := net.ResolveUnixAddr(unixsockNetwork, unixsockAddr)
   653  	conn, err = net.ListenUnixgram("unixgram", addr)
   654  	if err != nil {
   655  		return nil, err
   656  	}
   657  
   658  	err = os.Chmod(unixsockAddr, 0666)
   659  	if err != nil {
   660  		return nil, err
   661  	}
   662  	return
   663  }
   664  
   665  // get a message from unix docket and forward it to exChan
   666  func (m *mainSrv) uxServe(conn *net.UnixConn, timeout int, fn func(chan string, string)) {
   667  	// prepare to receive the signal
   668  	// sig := make(chan os.Signal, 1)
   669  	// signal.Notify(sig, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
   670  
   671  	// clean up
   672  	defer func() {
   673  		conn.Close()
   674  		uxCleanup()
   675  		// util.Log.Info("uxServe stopped")
   676  	}()
   677  
   678  	// util.Log.Info("uxServe started")
   679  	var buf [1024]byte
   680  	shutdown := false
   681  	for {
   682  		select {
   683  		// case ss := <-sig:
   684  		// 	switch ss {
   685  		// 	case syscall.SIGHUP: // TODO:reload the config?
   686  		// 		util.Log.Info("got signal: SIGHUP", "receiver", "uxServe")
   687  		// 	case syscall.SIGTERM, syscall.SIGINT:
   688  		// 		util.Log.Info("got signal: SIGTERM or SIGINT", "receiver", "uxServe")
   689  		// 		shutdown = true
   690  		// 	}
   691  		case <-m.uxdownChan:
   692  			shutdown = true
   693  		default:
   694  		}
   695  
   696  		if shutdown {
   697  			return
   698  		}
   699  
   700  		conn.SetReadDeadline(time.Now().Add(time.Millisecond * time.Duration(timeout)))
   701  		n, err := conn.Read(buf[:])
   702  		if err != nil {
   703  			if errors.Is(err, os.ErrDeadlineExceeded) {
   704  				continue
   705  			} else {
   706  				util.Logger.Warn("uxServe read failed", "error", err)
   707  				continue
   708  			}
   709  		}
   710  		resp := string(buf[:n])
   711  		fn(m.exChan, resp)
   712  	}
   713  }
   714  
   715  func (m *mainSrv) startChild(req string, addr *net.UDPAddr, conf2 Config) {
   716  	if len(m.workers) >= maxPortLimit {
   717  		resp := m.writeRespTo(addr, frontend.AprilshMsgOpen, "over max port limit")
   718  		util.Logger.Warn("over max port limit", "request", req, "response", resp)
   719  		return
   720  	}
   721  
   722  	// open aprilsh:TERM,user@server.domain
   723  	// parse term and destination from req
   724  	body := strings.Split(req, ":")
   725  	content := strings.Split(body[1], ",")
   726  	if len(content) != 2 {
   727  		resp := m.writeRespTo(addr, frontend.AprilshMsgOpen, "malform request")
   728  		util.Logger.Warn("malform request", "request", req, "response", resp)
   729  		return
   730  	}
   731  	conf2.term = content[0]
   732  	conf2.destination = content[1]
   733  
   734  	// parse user and host from destination
   735  	dest := strings.Split(content[1], "@")
   736  	if len(dest) != 2 {
   737  		resp := m.writeRespTo(addr, frontend.AprilshMsgOpen, "malform destination")
   738  		util.Logger.Warn("malform destination", "destination", content[1], "response", resp)
   739  		return
   740  	}
   741  	conf2.user = dest[0]
   742  	conf2.host = dest[1]
   743  
   744  	// prepare next port
   745  	var p int
   746  	for i := 0; i < 5; i++ {
   747  		p = m.getAvailabePort()
   748  		if checkPortAvailable(p) {
   749  			break
   750  		}
   751  		// add a placeholder for this port
   752  		m.workers[p] = &workhorse{}
   753  	}
   754  	conf2.desiredPort = fmt.Sprintf("%d", p)
   755  
   756  	// we don't need to check if user exist, ssh already done that before
   757  	// start child to serve this client
   758  	child, err := startChildProcess(&conf2)
   759  	if err != nil {
   760  		// if errors.Is(err, syscall.EPERM) {
   761  		// 	util.Logger.Warn("operation not permitted")
   762  		// } else {
   763  		// 	util.Logger.Warn("can't start child", "error", err)
   764  		// 	// fmt.Printf("can't start child, error=%#v\n", err)
   765  		// }
   766  		m.writeRespTo(addr, frontend.AprilshMsgOpen, "start child failed")
   767  		util.Logger.Warn("start child failed", "error", err)
   768  		return
   769  	}
   770  	util.Logger.Debug("start child successfully, wait for the key.")
   771  
   772  	// waiting for the child process to finish
   773  	m.wg.Add(1)
   774  	go func() {
   775  		ps, err := child.Wait()
   776  		if err != nil {
   777  			util.Logger.Warn("start child return", "error", err, "ProcessState", ps)
   778  		}
   779  		util.Logger.Debug("start child finished", "port", p)
   780  		m.wg.Done()
   781  	}()
   782  	// add this child to worker list
   783  	m.workers[p] = &workhorse{child: child}
   784  
   785  	// // start the worker
   786  	// m.wg.Add(1)
   787  	// go func(conf *Config, exChan chan string, whChan chan workhorse) {
   788  	// 	m.runWorker(conf, exChan, whChan)
   789  	// 	m.wg.Done()
   790  	// }(&conf2, m.exChan, m.whChan)
   791  
   792  	// timeout read key from worker
   793  	timer := time.NewTimer(time.Duration(145) * time.Millisecond)
   794  	select {
   795  	case <-timer.C:
   796  		delete(m.workers, p) // clear failed worker
   797  		resp := m.writeRespTo(addr, frontend.AprilshMsgOpen, "get key timeout")
   798  		util.Logger.Warn("start child got key timeout", "request", req, "response", resp)
   799  		return
   800  	case content := <-m.exChan:
   801  		// got session key
   802  		key, _ := m.handleMessage(content)
   803  		util.Logger.Debug("start child got key", "key", key)
   804  
   805  		//  send the key back to client
   806  		msg := fmt.Sprintf("%d,%s", p, key)
   807  		m.writeRespTo(addr, frontend.AprilshMsgOpen, msg)
   808  	}
   809  }
   810  
   811  func (m *mainSrv) closeChild(req string, addr *net.UDPAddr) {
   812  	// check port
   813  	pstr := strings.TrimPrefix(req, frontend.AprishMsgClose)
   814  	port, err := strconv.Atoi(pstr)
   815  	if err != nil {
   816  		resp := m.writeRespTo(addr, frontend.AprishMsgClose, "wrong port number")
   817  		util.Logger.Warn("wrong port number", "request", req, "response", resp)
   818  		return
   819  	}
   820  
   821  	// find worker
   822  	if _, ok := m.workers[port]; !ok {
   823  		resp := m.writeRespTo(addr, frontend.AprishMsgClose, "port does not exist")
   824  		util.Logger.Warn("port does not exist", "request", req, "response", resp)
   825  		return
   826  	}
   827  	// send kill message to the workers
   828  	if m.workers[port].child != nil {
   829  		m.workers[port].child.Signal(syscall.SIGTERM)
   830  		m.writeRespTo(addr, frontend.AprishMsgClose, "done")
   831  		util.Logger.Debug("close child done", "request", req)
   832  	} else {
   833  		resp := m.writeRespTo(addr, frontend.AprishMsgClose, "close port is a holder")
   834  		util.Logger.Warn("close port is a holder", "request", req, "response", resp)
   835  	}
   836  }
   837  
   838  func (m *mainSrv) handleMessage(content string) (string, error) {
   839  	msg := strings.Split(content, ":")
   840  
   841  	if len(msg) != 2 {
   842  		return "", &messageError{reason: "lack of ':'", err: errors.New(content)}
   843  	}
   844  
   845  	part2 := strings.Split(msg[1], ",")
   846  	if len(part2) != 2 {
   847  		return "", &messageError{reason: "lack of ','", err: errors.New(content)}
   848  	}
   849  	port, err := strconv.Atoi(part2[0])
   850  	if err != nil {
   851  		return "", &messageError{reason: "invalid port number", err: errors.New(content)}
   852  	}
   853  	if _, ok := m.workers[port]; !ok {
   854  		return "", &messageError{reason: "non-existence port number", err: errors.New(content)}
   855  	}
   856  
   857  	switch msg[0] {
   858  	case _ServeHeader: // stop the specified shell
   859  		if part2[1] != "shutdown" {
   860  			return "", &messageError{reason: "invalid shutdown", err: errors.New(content)}
   861  		}
   862  		shell, _ := os.FindProcess(m.workers[port].shellPid)
   863  		if err = shell.Kill(); err != nil {
   864  			if !errors.Is(err, os.ErrProcessDone) {
   865  				return "", &messageError{reason: "kill shell process failed", err: err}
   866  			}
   867  			// user quit shell actively.
   868  		}
   869  		util.Logger.Debug("handleMessage kill shell", "port", port)
   870  	case _RunHeader: // clean worker list
   871  		if part2[1] != "shutdown" {
   872  			return "", &messageError{reason: "invalid shutdown", err: errors.New(content)}
   873  		}
   874  		delete(m.workers, port)
   875  		util.Logger.Debug("handleMessage clean worker", "port", port)
   876  	case _KeyHeader: // return key
   877  		return part2[1], nil
   878  	case _ShellHeader: // add shell pid
   879  		shellPid, err := strconv.Atoi(part2[1])
   880  		if err != nil {
   881  			return "", &messageError{reason: "invalid shell pid", err: errors.New(content)}
   882  		}
   883  		m.workers[port].shellPid = shellPid
   884  		util.Logger.Debug("handleMessage got shell pid", "port", port, "shellPid", shellPid)
   885  	default:
   886  		return "", &messageError{reason: "unknown header", err: errors.New(content)}
   887  	}
   888  
   889  	return "", nil
   890  }
   891  
   892  func (m *mainSrv) shutdown() {
   893  	// util.Log.Info("run2", "shutdown", shutdown)
   894  	if len(m.workers) != 0 {
   895  		// stop all workers
   896  		for i := range m.workers {
   897  			if m.workers[i].child != nil { // check placeholder
   898  				m.workers[i].child.Signal(syscall.SIGTERM)
   899  				util.Logger.Debug("stop child", "port", i)
   900  			}
   901  		}
   902  
   903  		// wait for workers to shutdown
   904  		holder := 0
   905  		for holder != len(m.workers) {
   906  			timer := time.NewTimer(time.Duration(100) * time.Millisecond)
   907  			select {
   908  			case content := <-m.exChan: // some worker is done
   909  				m.handleMessage(content)
   910  				// counting placeholder
   911  				holder = 0
   912  				for i := range m.workers {
   913  					if m.workers[i].child == nil {
   914  						holder++
   915  					}
   916  				}
   917  				util.Logger.Debug("shutdown waiting for worker response", "holder", holder, "worker", m.workers)
   918  			case t := <-timer.C:
   919  				util.Logger.Warn("shutdown waiting for worker timeout", "timeout", t)
   920  			}
   921  		}
   922  		util.Logger.Debug("shutdown finish clean workers")
   923  	}
   924  	// finally, shutdown uxServe
   925  	m.uxdownChan <- true
   926  	util.Logger.Debug("shutdown stop uxServe")
   927  }
   928  
   929  // two kind of cmd: 60002 or 60002:shutdown.
   930  // the latter is used to stop the specified shell.
   931  // the former is used to clean the worker list.
   932  // func (m *mainSrv) cleanWorkers(cmd string) {
   933  // 	ps := strings.Split(cmd, ":")
   934  // 	if len(ps) == 1 {
   935  // 		p, err := strconv.Atoi(cmd)
   936  // 		if err != nil {
   937  // 			util.Log.Debug("cleanWorkers receive wrong portStr", "portStr", cmd, "err", err)
   938  // 		}
   939  //
   940  // 		// clean worker list
   941  // 		delete(m.workers, p)
   942  // 		// util.Log.Warn("#run clean worker","worker", ps[0])
   943  // 	} else if ps[1] == "shutdown" {
   944  // 		idx, err := strconv.Atoi(ps[0])
   945  // 		if err != nil {
   946  // 			util.Log.Warn("#run receive malform message", "portStr", cmd)
   947  // 		} else if _, ok := m.workers[idx]; ok {
   948  // 			// stop the specified shell
   949  // 			// m.workers[idx].shell.Kill()
   950  // 			util.Log.Debug("#run kill shell", "shell", idx)
   951  // 		}
   952  // 	}
   953  // }
   954  
   955  // return the minimal available port and increase the maxWorkerPort if necessary.
   956  // shrink the max port number if possible
   957  // https://coolaj86.com/articles/how-to-test-if-a-port-is-available-in-go/
   958  // https://github.com/devlights/go-unix-domain-socket-example
   959  func (m *mainSrv) getAvailabePort() (port int) {
   960  	port = m.port
   961  	if len(m.workers) > 0 {
   962  		// sort the current ports
   963  		ports := make([]int, 0, len(m.workers))
   964  		for k := range m.workers {
   965  			ports = append(ports, k)
   966  		}
   967  		sort.Ints(ports)
   968  		// shrink max if possible
   969  		m.maxPort = ports[len(ports)-1] + 1
   970  
   971  		// util.Log.Info("getAvailabePort",
   972  		// 	"ports", ports, "port", port, "maxPort", m.maxPort, "workers", len(m.workers))
   973  
   974  		// check minimal available port
   975  		for i := 0; i < m.maxPort-m.port; i++ {
   976  			if i < len(ports) && port+i+1 < ports[i] {
   977  				port = port + i + 1
   978  				break
   979  			}
   980  		}
   981  
   982  		// right most case
   983  		if port == m.port {
   984  			port = m.maxPort
   985  			m.maxPort++
   986  		}
   987  	} else if len(m.workers) == 0 {
   988  		// first port
   989  		port = m.port + 1
   990  		m.maxPort = port + 1
   991  	}
   992  
   993  	// util.Log.Info("getAvailabePort","port", port,"maxPort", m.maxPort,"workers", len(m.workers))
   994  	return port
   995  }
   996  
   997  // write header and message to addr
   998  func (m *mainSrv) writeRespTo(addr *net.UDPAddr, header, msg string) (resp string) {
   999  	resp = fmt.Sprintf("%s%s\n", header, msg)
  1000  	// util.Log.Debug("writeRespTo","resp", resp)
  1001  	m.conn.SetDeadline(time.Now().Add(time.Millisecond * time.Duration(m.timeout)))
  1002  	m.conn.WriteToUDP([]byte(resp), addr)
  1003  	return
  1004  }
  1005  
  1006  // Print the motd from a given file, if available
  1007  func printMotd(w io.Writer, filename string) bool {
  1008  	// fmt.Printf("#printMotd print %q\n", filename)
  1009  	// https://zetcode.com/golang/readfile/
  1010  
  1011  	motd, err := os.Open(filename)
  1012  	if err != nil {
  1013  		return false
  1014  	}
  1015  	defer motd.Close()
  1016  
  1017  	// read line by line, print each line to writer
  1018  	scanner := bufio.NewScanner(motd)
  1019  	for scanner.Scan() {
  1020  		fmt.Fprintf(w, "%s\n", scanner.Text())
  1021  	}
  1022  
  1023  	if err := scanner.Err(); err != nil {
  1024  		return false
  1025  	}
  1026  
  1027  	return true
  1028  }
  1029  
  1030  func chdirHomedir(home string) bool {
  1031  	var err error
  1032  	if home == "" {
  1033  		home, err = os.UserHomeDir()
  1034  		if err != nil {
  1035  			return false
  1036  		}
  1037  	}
  1038  
  1039  	err = os.Chdir(home)
  1040  	if err != nil {
  1041  		return false
  1042  	}
  1043  	os.Setenv("PWD", home)
  1044  
  1045  	return true
  1046  }
  1047  
  1048  // get current user home directory
  1049  func getHomeDir() string {
  1050  	home, err := os.UserHomeDir()
  1051  	if err != nil {
  1052  		return ""
  1053  	}
  1054  
  1055  	return home
  1056  }
  1057  
  1058  func motdHushed() bool {
  1059  	// must be in home directory already
  1060  	_, err := os.Lstat(".hushlogin")
  1061  	if err != nil {
  1062  		return false
  1063  	}
  1064  
  1065  	return true
  1066  }
  1067  
  1068  // extract server ip addresss from SSH_CONNECTION
  1069  func getSSHip() (string, bool) {
  1070  	env := os.Getenv("SSH_CONNECTION")
  1071  	if len(env) == 0 { // Older sshds don't set this
  1072  		return fmt.Sprintf("Warning: SSH_CONNECTION not found; binding to any interface."), false
  1073  	}
  1074  
  1075  	// SSH_CONNECTION' Identifies the client and server ends of the connection.
  1076  	// The variable contains four space-separated values: client IP address,
  1077  	// client port number, server IP address, and server port number.
  1078  	//
  1079  	// ipv4 sample: SSH_CONNECTION=172.17.0.1 58774 172.17.0.2 22
  1080  	sshConn := strings.Split(env, " ")
  1081  	if len(sshConn) != 4 {
  1082  		return fmt.Sprintf("Warning: Could not parse SSH_CONNECTION; binding to any interface."), false
  1083  	}
  1084  
  1085  	localInterfaceIP := strings.ToLower(sshConn[2])
  1086  	prefixIPv6 := "::ffff:"
  1087  
  1088  	// fmt.Printf("#getSSHip localInterfaceIP=%q, prefixIPv6=%q\n", localInterfaceIP, prefixIPv6)
  1089  	if len(localInterfaceIP) > len(prefixIPv6) && strings.HasPrefix(localInterfaceIP, prefixIPv6) {
  1090  		return localInterfaceIP[len(prefixIPv6):], true
  1091  	}
  1092  
  1093  	return localInterfaceIP, true
  1094  }
  1095  
  1096  // extract shell name from shellPath and prepend '-' to the returned shell name
  1097  func getShellNameFrom(shellPath string) (shellName string) {
  1098  	shellSplash := strings.LastIndex(shellPath, "/")
  1099  	if shellSplash == -1 {
  1100  		shellName = shellPath
  1101  	} else {
  1102  		shellName = shellPath[shellSplash+1:]
  1103  	}
  1104  
  1105  	// prepend '-' to make login shell
  1106  	shellName = "-" + shellName
  1107  
  1108  	return
  1109  }
  1110  
  1111  func getTimeFrom(env string, def int64) (ret int64) {
  1112  	ret = def
  1113  
  1114  	v, exist := os.LookupEnv(env)
  1115  	if exist {
  1116  		var err error
  1117  		ret, err = strconv.ParseInt(v, 10, 64)
  1118  		if err != nil {
  1119  			fmt.Fprintf(os.Stdout, "%s not a valid integer, ignoring\n", env)
  1120  		} else if ret < 0 {
  1121  			fmt.Fprintf(os.Stdout, "%s is negative, ignoring\n", env)
  1122  			ret = 0
  1123  		}
  1124  	}
  1125  	return
  1126  }
  1127  
  1128  func printWelcome(tty *os.File) {
  1129  	// func printWelcome(pid int, port int, tty *os.File) {
  1130  	// fmt.Printf("Copyright 2022~2023 wangqi.\n")
  1131  	// fmt.Printf("%s%s", "Use of this source code is governed by a MIT-style",
  1132  	// 	"license that can be found in the LICENSE file.\n")
  1133  	// logI.Printf("[%s detached, pid=%d]\n", COMMAND_NAME, pid)
  1134  
  1135  	if tty != nil {
  1136  		inputUTF8, err := util.CheckIUTF8(int(tty.Fd()))
  1137  		if err != nil {
  1138  			// fmt.Printf("Warning: %s\n", err)
  1139  			util.Logger.Warn(err.Error())
  1140  		}
  1141  
  1142  		if !inputUTF8 {
  1143  			// Input is UTF-8 (since Linux 2.6.4)
  1144  			// fmt.Printf("%s %s %s", "Warning: termios IUTF8 flag not defined.",
  1145  			// 	"Character-erase of multibyte character sequence",
  1146  			// 	"probably does not work properly on this platform.\n")
  1147  
  1148  			msg := fmt.Sprintf("%s %s %s", "Warning: termios IUTF8 flag not defined.",
  1149  				"Character-erase of multibyte character sequence",
  1150  				"probably does not work properly on this platform.")
  1151  			util.Logger.Warn(msg)
  1152  		}
  1153  	}
  1154  }
  1155  
  1156  // TODO can't get current user.
  1157  func getCurrentUser() string {
  1158  	user, err := user.Current()
  1159  	if err != nil {
  1160  		util.Logger.Warn("Get current user", "error", err)
  1161  		return ""
  1162  	}
  1163  
  1164  	return user.Username
  1165  }
  1166  
  1167  // easy for testing under linux
  1168  func setGetRecord(f func() *utmps.Utmpx) {
  1169  	funcGetRecord = f
  1170  }
  1171  
  1172  // check unattached session and print warning message if there is any
  1173  // unattached session: session started by client, but there is no client
  1174  // packet received recently. unattached session example: "apshd:8101".
  1175  // attached session example: "192.168.5.1 via apshd:8101"
  1176  func warnUnattached(w io.Writer, userName string, ignoreHost string) {
  1177  	// check unattached sessions
  1178  	unatttached := make([]string, 0)
  1179  	// unatttached := CheckUnattachedUtmpx(userName, ignoreHost, frontend.CommandServerName)
  1180  	util.Logger.Debug("warnUnattached", "get", "record", "funcGetRecord", funcGetRecord)
  1181  	r := funcGetRecord()
  1182  	for r != nil {
  1183  		util.Logger.Debug("warnUnattached", "user", r.GetUser(), "line", r.GetHost(), "type", r.GetType())
  1184  		if r.GetType() == utmps.USER_PROCESS && r.GetUser() == userName {
  1185  			// does line show unattached session
  1186  			host := r.GetHost()
  1187  			// if testing.Testing() {
  1188  			// 	fmt.Printf("#checkUnattachedRecord() MATCH user=(%q,%q) type=(%d,%d) host=%s\n",
  1189  			// 		r.GetUser(), userName, r.GetType(), utmps.USER_PROCESS, host)
  1190  			// }
  1191  			if len(host) >= 5 && strings.HasPrefix(host, frontend.CommandServerName) &&
  1192  				host != ignoreHost && utmps.DeviceExists(r.GetLine()) {
  1193  				unatttached = append(unatttached, host)
  1194  				// if testing.Testing() {
  1195  				// 	fmt.Printf("#checkUnattachedRecord() append host=%s, line=%q\n", host, r.GetLine())
  1196  				// }
  1197  			}
  1198  		} else {
  1199  			// if testing.Testing() {
  1200  			// 	fmt.Printf("#checkUnattachedRecord() skip user=%q,%q; type=%d, line=%s, host=%s, id=%d, pid=%d\n",
  1201  			// 		r.GetUser(), userName, r.GetType(), r.GetLine(), r.GetHost(), r.GetId(), r.GetPid())
  1202  			// }
  1203  		}
  1204  		r = funcGetRecord()
  1205  	}
  1206  
  1207  	if len(unatttached) == 0 {
  1208  		util.Logger.Debug("warnUnattached", "0", "record")
  1209  		return
  1210  	} else if len(unatttached) == 1 {
  1211  		fmt.Fprintf(w, "\033[37;44mAprilsh: You have a detached session on this server (%s).\033[m\n\n",
  1212  			unatttached[0])
  1213  		util.Logger.Debug("warnUnattached", "1", "record")
  1214  	} else {
  1215  		var sb strings.Builder
  1216  		for _, v := range unatttached {
  1217  			fmt.Fprintf(&sb, "- %s\n", v)
  1218  		}
  1219  
  1220  		fmt.Fprintf(w, "\033[37;44mAprilsh: You have %d detached sessions on this server, with PIDs:\n%s\033[m\n",
  1221  			len(unatttached), sb.String())
  1222  		util.Logger.Debug("warnUnattached", "x", "record")
  1223  	}
  1224  }
  1225  
  1226  func checkPortAvailable(port int) bool {
  1227  	laddr, err := net.ResolveUDPAddr("udp", ":"+strconv.Itoa(port))
  1228  	if err != nil {
  1229  		util.Logger.Debug("checkPort listen", "error", err, "laddr", laddr)
  1230  		return false
  1231  	}
  1232  
  1233  	conn, err := net.ListenUDP("udp", laddr)
  1234  	if err != nil {
  1235  		util.Logger.Debug("checkPort listen", "port", port, "error", err)
  1236  		return false
  1237  	}
  1238  
  1239  	conn.Close()
  1240  	// err = conn.Close()
  1241  	// if err != nil {
  1242  	// 	util.Logger.Debug("checkPort close", "port", port, "error", err)
  1243  	// 	return false
  1244  	// }
  1245  	return true
  1246  }
  1247  
  1248  type messageError struct {
  1249  	reason string
  1250  	err    error
  1251  }
  1252  
  1253  func (e *messageError) Error() string {
  1254  	if e.err == nil {
  1255  		return "<nil>"
  1256  	}
  1257  	return e.reason + ": " + e.err.Error()
  1258  }
  1259  
  1260  func startChildProcess(conf *Config) (*os.Process, error) {
  1261  	// conf{term,user,desiredPort,destination}
  1262  
  1263  	util.Logger.Debug("startChild", "user", conf.user, "term", conf.term,
  1264  		"desiredPort", conf.desiredPort, "destination", conf.destination)
  1265  
  1266  	// specify child process
  1267  	commandPath := "/usr/bin/apshd"
  1268  	if path2, ok := os.LookupEnv(apshdPath); ok {
  1269  		commandPath = path2
  1270  		// util.Logger.Debug("startChildProcess got commandPath from env", "commandPath", commandPath)
  1271  	}
  1272  	commandArgv := []string{commandPath, "-p", conf.desiredPort}
  1273  
  1274  	// hide the following command args from ps command
  1275  	args := []string{"-child", "-destination", conf.destination, "-term", conf.term}
  1276  	// inherit vervoce and source options form parent
  1277  	if conf.verbose == util.DebugLevel {
  1278  		args = append(args, "-v")
  1279  	} else if conf.verbose == util.TraceLevel {
  1280  		args = append(args, "-vv")
  1281  	}
  1282  	if conf.addSource {
  1283  		args = append(args, "-source")
  1284  	}
  1285  
  1286  	// var pts *os.File
  1287  	// var pr *io.PipeReader
  1288  	// var utmpHost string
  1289  
  1290  	// if conf.verbose == _VERBOSE_SKIP_START_SHELL {
  1291  	// 	return nil, failToStartShell
  1292  	// }
  1293  	// set IUTF8 if available
  1294  	// if err := util.SetIUTF8(int(pts.Fd())); err != nil {
  1295  	// 	return nil, err
  1296  	// }
  1297  
  1298  	var env []string
  1299  
  1300  	// set TERM based on client TERM
  1301  	env = append(env, "TERM="+conf.term)
  1302  	// if conf.term != "" {
  1303  	// 	env = append(env, "TERM="+conf.term)
  1304  	// } else {
  1305  	// 	env = append(env, "TERM=xterm-256color")
  1306  	// }
  1307  
  1308  	// TODO use the root's SHELL as replacement for user SHELL
  1309  	// shell, err := util.GetShell()
  1310  	// if shell == "" || err != nil {
  1311  	// 	err := errors.New("can't get shell from SHELL")
  1312  	// 	return nil, err
  1313  	// }
  1314  	// env = append(env, "SHELL="+shell)
  1315  
  1316  	// macOS need this, anyway we set it for both linux and macOS
  1317  	env = append(env, "LANG=en_US.UTF-8")
  1318  
  1319  	// clear STY environment variable so GNU screen regards us as top level
  1320  	// os.Unsetenv("STY")
  1321  
  1322  	// get login user info, we already checked the user exist when ssh perform authentication.
  1323  	u, _ := user.Lookup(conf.user)
  1324  	// uid, _ := strconv.ParseInt(u.Uid, 10, 32)
  1325  	// gid, _ := strconv.ParseInt(u.Gid, 10, 32)
  1326  	util.Logger.Debug("startChild check user", "user", u.Username, "gid", u.Gid, "HOME", u.HomeDir)
  1327  
  1328  	// set base env
  1329  	// TODO should we put LOGNAME, MAIL into env?
  1330  	env = append(env, "PWD="+u.HomeDir)
  1331  	env = append(env, "HOME="+u.HomeDir) // it's important for shell to source .profile
  1332  	env = append(env, "USER="+conf.user)
  1333  
  1334  	if v := os.Getenv("TZ"); len(v) > 0 {
  1335  		env = append(env, "TZ="+v)
  1336  	}
  1337  
  1338  	// TODO should we set ssh env ?
  1339  	if v := os.Getenv("SSH_CLIENT"); len(v) > 0 {
  1340  		env = append(env, "SSH_CLIENT="+v)
  1341  	}
  1342  	if v := os.Getenv("SSH_CONNECTION"); len(v) > 0 {
  1343  		env = append(env, "SSH_CONNECTION="+v)
  1344  	}
  1345  	if v := os.Getenv("PATH"); len(v) > 0 {
  1346  		env = append(env, "PATH="+v)
  1347  	}
  1348  
  1349  	// ask ncurses to send UTF-8 instead of ISO 2022 for line-drawing chars
  1350  	env = append(env, "NCURSES_NO_UTF8_ACS=1")
  1351  
  1352  	// decrease system thread number
  1353  	env = append(env, "GOMAXPROCS=1")
  1354  	if value, ok := os.LookupEnv("GOCOVERDIR"); ok {
  1355  		if value != "" {
  1356  			env = append(env, fmt.Sprintf("GOCOVERDIR=%s", value))
  1357  		}
  1358  	}
  1359  	// hidden parameter send via env
  1360  	env = append(env, envArgs+"="+strings.Join(args, " "))
  1361  	env = append(env, envUDS+"="+unixsockAddr)
  1362  
  1363  	util.Logger.Debug("startChild env:", "env", env)
  1364  	util.Logger.Debug("startChild command:", "commandPath", commandPath, "commandArgv", commandArgv)
  1365  
  1366  	sysProcAttr := &syscall.SysProcAttr{}
  1367  	sysProcAttr.Setsid = true // start a new session
  1368  	// sysProcAttr.Setctty = true                    // set controlling terminal
  1369  	// sysProcAttr.Credential = &syscall.Credential{ // change user
  1370  	// 	Uid: uint32(uid),
  1371  	// 	Gid: uint32(gid),
  1372  	// }
  1373  
  1374  	procAttr := os.ProcAttr{
  1375  		Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, // use pts as stdin, stdout, stderr
  1376  		Dir:   u.HomeDir,
  1377  		Sys:   sysProcAttr,
  1378  		Env:   env,
  1379  	}
  1380  
  1381  	return os.StartProcess(commandPath, commandArgv, &procAttr)
  1382  	// proc, err := os.StartProcess(commandPath, commandArgv, &procAttr)
  1383  	// if err != nil {
  1384  	// 	return nil, err
  1385  	// }
  1386  	// return proc, nil
  1387  }
  1388  
  1389  // open pts master and slave, set terminal size according to window size.
  1390  func openPTS(wsize *unix.Winsize) (ptmx *os.File, pts *os.File, err error) {
  1391  	// open pts master and slave
  1392  	ptmx, pts, err = pty.Open()
  1393  	if wsize == nil {
  1394  		err = errors.New("invalid parameter")
  1395  	}
  1396  	if err == nil {
  1397  		sz := util.ConvertWinsize(wsize)
  1398  		// fmt.Printf("#openPTS sz=%v\n", sz)
  1399  
  1400  		err = pty.Setsize(ptmx, sz) // set terminal size
  1401  	}
  1402  	return
  1403  }
  1404  
  1405  // set IUTF8 flag for pts file. start shell process according to Config.
  1406  func startShellProcess(pts *os.File, pr *io.PipeReader, utmpHost string, conf *Config) (*os.Process, error) {
  1407  	// close pipe will stop the Read operation
  1408  	defer pr.Close()
  1409  
  1410  	if conf.flowControl == _FC_SKIP_START_SHELL {
  1411  		return nil, failToStartShell
  1412  	}
  1413  	// set IUTF8 if available
  1414  	if err := util.SetIUTF8(int(pts.Fd())); err != nil {
  1415  		return nil, err
  1416  	}
  1417  
  1418  	var env []string
  1419  
  1420  	// set TERM based on client TERM
  1421  	if conf.term != "" {
  1422  		env = append(env, "TERM="+conf.term)
  1423  	} else {
  1424  		env = append(env, "TERM=xterm-256color")
  1425  	}
  1426  
  1427  	// clear STY environment variable so GNU screen regards us as top level
  1428  	// os.Unsetenv("STY")
  1429  
  1430  	// get login user info, we already checked the user exist when ssh perform authentication.
  1431  	// users := strings.Split(conf.destination, "@")
  1432  	var changeUser bool
  1433  	if conf.user != getCurrentUser() {
  1434  		changeUser = true
  1435  	}
  1436  	// util.Logger.Debug("start shell check user", "changeUser", changeUser)
  1437  
  1438  	u, err := user.Lookup(conf.user)
  1439  	if err != nil {
  1440  		return nil, err
  1441  	}
  1442  
  1443  	// prepare shell for target user.
  1444  	conf.commandArgv = []string{}
  1445  	conf.commandPath = ""
  1446  	conf.prepareShell(u)
  1447  
  1448  	var uid int64
  1449  	var gid int64
  1450  	if changeUser {
  1451  		uid, _ = strconv.ParseInt(u.Uid, 10, 32)
  1452  		gid, _ = strconv.ParseInt(u.Gid, 10, 32)
  1453  	}
  1454  
  1455  	// set base env
  1456  	// TODO should we put LOGNAME, MAIL into env?
  1457  	env = append(env, "PWD="+u.HomeDir)
  1458  	env = append(env, "HOME="+u.HomeDir) // it's important for shell to source .profile
  1459  	env = append(env, "USER="+conf.user)
  1460  	env = append(env, "SHELL="+conf.commandPath)
  1461  
  1462  	if v := os.Getenv("TZ"); len(v) > 0 {
  1463  		env = append(env, "TZ="+v)
  1464  	}
  1465  
  1466  	// TODO should we set ssh env ?
  1467  	if v := os.Getenv("SSH_CLIENT"); len(v) > 0 {
  1468  		env = append(env, "SSH_CLIENT="+v)
  1469  	}
  1470  	if v := os.Getenv("SSH_CONNECTION"); len(v) > 0 {
  1471  		env = append(env, "SSH_CONNECTION="+v)
  1472  	}
  1473  	if v := os.Getenv("PATH"); len(v) > 0 {
  1474  		env = append(env, "PATH="+v)
  1475  	}
  1476  
  1477  	// ask ncurses to send UTF-8 instead of ISO 2022 for line-drawing chars
  1478  	env = append(env, "NCURSES_NO_UTF8_ACS=1")
  1479  
  1480  	util.Logger.Debug("start shell check user", "user", u.Username, "gid", u.Gid, "HOME", u.HomeDir)
  1481  	util.Logger.Debug("start shell check env", "env", env)
  1482  	util.Logger.Debug("start shell check command",
  1483  		"commandPath", conf.commandPath, "commandArgv", conf.commandArgv)
  1484  
  1485  	sysProcAttr := &syscall.SysProcAttr{}
  1486  	sysProcAttr.Setsid = true  // start a new session
  1487  	sysProcAttr.Setctty = true // set controlling terminal
  1488  	if changeUser {
  1489  		sysProcAttr.Credential = &syscall.Credential{ // change user
  1490  			Uid: uint32(uid),
  1491  			Gid: uint32(gid),
  1492  		}
  1493  	}
  1494  
  1495  	procAttr := os.ProcAttr{
  1496  		Files: []*os.File{pts, pts, pts}, // use pts as stdin, stdout, stderr
  1497  		Dir:   u.HomeDir,
  1498  		Sys:   sysProcAttr,
  1499  		Env:   env,
  1500  	}
  1501  
  1502  	// https://stackoverflow.com/questions/21705950/running-external-commands-through-os-exec-under-another-user
  1503  	//
  1504  	util.Logger.Debug("start shell prepare to check motd and unattached session", "utmpSupport", utmpSupport)
  1505  	if conf.withMotd && !motdHushed() {
  1506  		// For Ubuntu, try and print one of {,/var}/run/motd.dynamic.
  1507  		// This file is only updated when pam_motd is run, but when
  1508  		// mosh-server is run in the usual way with ssh via the script,
  1509  		// this always happens.
  1510  		// XXX Hackish knowledge of Ubuntu PAM configuration.
  1511  		// But this seems less awful than build-time detection with autoconf.
  1512  		if !printMotd(pts, "/run/motd.dynamic") {
  1513  			printMotd(pts, "/var/run/motd.dynamic")
  1514  		}
  1515  		// Always print traditional /etc/motd.
  1516  		printMotd(pts, "/etc/motd")
  1517  
  1518  		// if utmpSupport {
  1519  		// 	warnUnattached(pts, conf.user, utmpHost)
  1520  		// }
  1521  	}
  1522  
  1523  	// set new title
  1524  	fmt.Fprintf(pts, "\x1B]0;%s %s:%s\a", frontend.CommandClientName, conf.destination, conf.desiredPort)
  1525  
  1526  	// encrypt.ReenableDumpingCore()
  1527  
  1528  	/*
  1529  		additional logic for pty.StartWithAttrs() end
  1530  	*/
  1531  
  1532  	util.Logger.Debug("start shell waiting for pipe unlock")
  1533  	// wait for serve() to release us
  1534  	if pr != nil && conf.flowControl != _FC_SKIP_PIPE_LOCK {
  1535  		ch := make(chan string, 0)
  1536  		timer := time.NewTimer(time.Duration(frontend.TimeoutIfNoConnect) * time.Millisecond)
  1537  
  1538  		// util.Log.Debug("start shell message", "action", "wait", "port", conf.desiredPort)
  1539  		// add timeout for pipe read
  1540  		go func(pr *io.PipeReader, ch chan string) {
  1541  			buf := make([]byte, 81)
  1542  
  1543  			n, err := pr.Read(buf)
  1544  			if err != nil && errors.Is(err, io.EOF) {
  1545  				ch <- string(buf[:n])
  1546  				// util.Logger.Debug("shell unlock", "action", "closed", "buf", buf[:n])
  1547  			} else {
  1548  				ch <- earlyShutdown
  1549  				// util.Logger.Debug("shell unlock", "action", earlyShutdown, "error", err)
  1550  			}
  1551  		}(pr, ch)
  1552  
  1553  		// waiting for time out or get the pipe reader send message
  1554  		select {
  1555  		case s := <-ch:
  1556  			if s == earlyShutdown {
  1557  				return nil, errors.New(earlyShutdown)
  1558  			}
  1559  		case <-timer.C:
  1560  			// util.Log.Debug("start shell message", "action", "timeout", "port", conf.desiredPort)
  1561  			return nil, fmt.Errorf("pipe read: %w", os.ErrDeadlineExceeded)
  1562  		}
  1563  		timer.Stop()
  1564  
  1565  		util.Logger.Info("start shell with pty", "pty", pts.Name())
  1566  	}
  1567  
  1568  	return os.StartProcess(conf.commandPath, conf.commandArgv, &procAttr)
  1569  	// proc, err := os.StartProcess(conf.commandPath, conf.commandArgv, &procAttr)
  1570  	// if err != nil {
  1571  	// 	return nil, err
  1572  	// }
  1573  	// // util.Logger.Info("start shell done", "shellPid", proc.Pid)
  1574  	// return proc, nil
  1575  }
  1576  
  1577  func serve(ptmx *os.File, pts *os.File, pw *io.PipeWriter, complete *statesync.Complete, // waitChan chan bool,
  1578  	server *network.Transport[*statesync.Complete, *statesync.UserStream],
  1579  	networkTimeout int64, networkSignaledTimeout int64, user string) error {
  1580  	// scale timeouts
  1581  	networkTimeoutMs := networkTimeout * 1000
  1582  	networkSignaledTimeoutMs := networkSignaledTimeout * 1000
  1583  
  1584  	lastRemoteNum := server.GetRemoteStateNum()
  1585  	var connectedUtmp bool
  1586  	var forceConnectionChangEvt bool
  1587  	var savedAddr net.Addr
  1588  
  1589  	if syslogSupport {
  1590  		syslogWriter.Info(fmt.Sprintf("user %s session begin -> port %s", user, server.GetServerPort()))
  1591  	}
  1592  	util.Logger.Info("user session begin", "user", user)
  1593  
  1594  	var terminalToHost strings.Builder
  1595  	var timeSinceRemoteState int64
  1596  
  1597  	// var networkChan chan frontend.Message
  1598  	networkChan := make(chan frontend.Message, 1)
  1599  	fileChan := make(chan frontend.Message, 1)
  1600  	fileDownChan := make(chan any, 1)
  1601  	networkDownChan := make(chan any, 1)
  1602  
  1603  	eg := errgroup.Group{}
  1604  	// read from socket
  1605  	eg.Go(func() error {
  1606  		frontend.ReadFromNetwork(1, networkChan, networkDownChan, server.GetConnection())
  1607  		return nil
  1608  	})
  1609  
  1610  	// read from pty master file
  1611  	// the following doesn't work for terminal, when the shell start, the file
  1612  	// is reset back to blocking IO mode.
  1613  	// syscall.SetNonblock(int(ptmx.Fd()), true)
  1614  	eg.Go(func() error {
  1615  		frontend.ReadFromFile(10, fileChan, fileDownChan, ptmx)
  1616  		return nil
  1617  	})
  1618  
  1619  	// intercept signal
  1620  	sigChan := make(chan os.Signal, 1)
  1621  	signal.Notify(sigChan, syscall.SIGUSR1, syscall.SIGINT, syscall.SIGTERM)
  1622  
  1623  	childReleased := false
  1624  	largeFeed := make(chan string, 1)
  1625  
  1626  mainLoop:
  1627  	for {
  1628  		timeout := math.MaxInt16
  1629  		now := time.Now().UnixMilli()
  1630  
  1631  		timeout = min(timeout, server.WaitTime()) // network.WaitTime cost time
  1632  		w0 := timeout
  1633  		w1 := complete.WaitTime(now)
  1634  		timeout = min(timeout, w1)
  1635  		// timeout = terminal.Min(timeout, complete.WaitTime(now))
  1636  
  1637  		if server.GetRemoteStateNum() > 0 || server.ShutdownInProgress() {
  1638  			timeout = min(timeout, 5000)
  1639  		}
  1640  
  1641  		// The server goes completely asleep if it has no remote peer.
  1642  		// We may want to wake up sooner.
  1643  		var networkSleep int64
  1644  		if networkTimeoutMs > 0 {
  1645  			rs := server.GetLatestRemoteState()
  1646  			networkSleep = networkTimeoutMs - (now - rs.GetTimestamp())
  1647  			if networkSleep < 0 {
  1648  				networkSleep = 0
  1649  			} else if networkSleep > math.MaxInt16 {
  1650  				networkSleep = math.MaxInt16
  1651  			}
  1652  			timeout = min(timeout, int(networkSleep))
  1653  		}
  1654  
  1655  		now = time.Now().UnixMilli()
  1656  		p := server.GetLatestRemoteState()
  1657  		timeSinceRemoteState = now - p.GetTimestamp()
  1658  		terminalToHost.Reset()
  1659  
  1660  		util.Logger.Log(context.Background(), util.LevelTrace, "mainLoop", "port", server.GetServerPort(),
  1661  			"network.WaitTime", w0, "complete.WaitTime", w1, "timeout", timeout)
  1662  		timer := time.NewTimer(time.Duration(timeout) * time.Millisecond)
  1663  		select {
  1664  		case <-timer.C:
  1665  			util.Logger.Log(context.Background(), util.LevelTrace, "mainLoop", "timeout", timeout,
  1666  				"complete", complete.WaitTime(now), "networkSleep", networkSleep)
  1667  		case s := <-sigChan:
  1668  			signals.Handler(s)
  1669  		case socketMsg := <-networkChan: // packet received from the network
  1670  			if socketMsg.Err != nil {
  1671  				// TODO handle "use of closed network connection" error?
  1672  				util.Logger.Warn("read from network", "error", socketMsg.Err)
  1673  				continue mainLoop
  1674  			}
  1675  			server.ProcessPayload(socketMsg.Data)
  1676  			p = server.GetLatestRemoteState()
  1677  			timeSinceRemoteState = now - p.GetTimestamp()
  1678  
  1679  			// is new user input available for the terminal?
  1680  			if server.GetRemoteStateNum() != lastRemoteNum {
  1681  				lastRemoteNum = server.GetRemoteStateNum()
  1682  
  1683  				us := &statesync.UserStream{}
  1684  				us.ApplyString(server.GetRemoteDiff())
  1685  
  1686  				// apply userstream to terminal
  1687  				for i := 0; i < us.Size(); i++ {
  1688  					action := us.GetAction(i)
  1689  					if res, ok := action.(terminal.Resize); ok {
  1690  						//  apply only the last consecutive Resize action
  1691  						if i < us.Size()-1 {
  1692  							if _, ok = us.GetAction(i + 1).(terminal.Resize); ok {
  1693  								continue
  1694  							}
  1695  						}
  1696  						// resize master
  1697  						winSize, err := unix.IoctlGetWinsize(int(ptmx.Fd()), unix.TIOCGWINSZ)
  1698  						if err != nil {
  1699  							fmt.Printf("#serve ioctl TIOCGWINSZ %s", err)
  1700  							server.StartShutdown()
  1701  						}
  1702  						winSize.Col = uint16(res.Width)
  1703  						winSize.Row = uint16(res.Height)
  1704  						if err = unix.IoctlSetWinsize(int(ptmx.Fd()), unix.TIOCSWINSZ, winSize); err != nil {
  1705  							fmt.Printf("#serve ioctl TIOCSWINSZ %s", err)
  1706  							server.StartShutdown()
  1707  						}
  1708  						// util.Log.Debug("input from remote", "col", winSize.Col, "row", winSize.Row)
  1709  						if !childReleased {
  1710  							// only do once
  1711  							server.InitSize(res.Width, res.Height)
  1712  						}
  1713  					}
  1714  					terminalToHost.WriteString(complete.ActOne(action))
  1715  				}
  1716  
  1717  				if terminalToHost.Len() > 0 {
  1718  					util.Logger.Debug("input from remote", "arise", "socket", "data", terminalToHost.String())
  1719  				}
  1720  
  1721  				if !us.Empty() {
  1722  					// register input frame number for future echo ack
  1723  					complete.RegisterInputFrame(lastRemoteNum, now)
  1724  				}
  1725  
  1726  				// update client with new state of terminal
  1727  				if !server.ShutdownInProgress() {
  1728  					server.SetCurrentState(complete)
  1729  				}
  1730  
  1731  				if utmpSupport || syslogSupport {
  1732  					if utmpSupport {
  1733  						if !connectedUtmp {
  1734  							forceConnectionChangEvt = true
  1735  						} else {
  1736  							forceConnectionChangEvt = false
  1737  						}
  1738  					} else {
  1739  						forceConnectionChangEvt = false
  1740  					}
  1741  
  1742  					// HAVE_UTEMPTER - update utmp entry if we have become "connected"
  1743  					// HAVE_SYSLOG - log connect to syslog
  1744  					//
  1745  					// update utmp entry if we have become "connected"
  1746  					if forceConnectionChangEvt || !reflect.DeepEqual(savedAddr, socketMsg.RAddr) {
  1747  						savedAddr = socketMsg.RAddr
  1748  						host := savedAddr.(*net.UDPAddr).IP.String() // default host name is ip string
  1749  						// convert savedAddr to host name
  1750  						// hostList, e := net.LookupAddr(host)
  1751  						// if e == nil {
  1752  						// 	host = hostList[0] // got the host name, use the first one
  1753  						// }
  1754  
  1755  						if utmpSupport {
  1756  							utmps.RemoveRecord(pts.Name(), os.Getpid())
  1757  							utmpHost := fmt.Sprintf("%s via %s:%s",
  1758  								host, frontend.CommandServerName, server.GetServerPort())
  1759  							// utmpHost := fmt.Sprintf("%s:%s", frontend.CommandServerName, server.GetServerPort())
  1760  							utmps.AddRecord(pts.Name(), user, utmpHost, os.Getpid())
  1761  							connectedUtmp = true
  1762  						}
  1763  						if syslogSupport {
  1764  							syslogWriter.Info(fmt.Sprintf("user %s connected from host: %s -> port %s",
  1765  								user, server.GetRemoteAddr(), server.GetServerPort()))
  1766  						}
  1767  						util.Logger.Info("connected from remote host", "user", user, "host", host)
  1768  					}
  1769  				}
  1770  
  1771  				// upon receive network message, perform the following one time action,
  1772  				// release startShell() to start login session
  1773  				if !childReleased {
  1774  					if err := pw.Close(); err != nil {
  1775  						util.Logger.Error("send start shell message failed", "error", err)
  1776  					}
  1777  					// util.Log.Debug("start shell message", "action", "send")
  1778  					childReleased = true
  1779  				}
  1780  			}
  1781  		case remains := <-largeFeed:
  1782  			if !server.ShutdownInProgress() {
  1783  				out := complete.ActLarge(remains, largeFeed)
  1784  				terminalToHost.WriteString(out)
  1785  
  1786  				util.Logger.Debug("ouput from host", "arise", "remains", "input", out)
  1787  
  1788  				// update client with new state of terminal
  1789  				server.SetCurrentState(complete)
  1790  			}
  1791  		case masterMsg := <-fileChan:
  1792  			// input from the host needs to be fed to the terminal
  1793  			if !server.ShutdownInProgress() {
  1794  
  1795  				// If the pty slave is closed, reading from the master can fail with
  1796  				// EIO (see #264).  So we treat errors on read() like EOF.
  1797  				if masterMsg.Err != nil {
  1798  					if len(masterMsg.Data) > 0 {
  1799  						util.Logger.Warn("read from master", "error", masterMsg.Err)
  1800  					}
  1801  					if !signals.AnySignal() { // avoid conflict with signal
  1802  						util.Logger.Debug("shutdown", "from", "read file failed", "port", server.GetServerPort())
  1803  						// &fs.PathError{Op:"read", Path:"/dev/ptmx", Err:0x5}
  1804  						server.StartShutdown()
  1805  					}
  1806  				} else {
  1807  					out := complete.ActLarge(masterMsg.Data, largeFeed)
  1808  					terminalToHost.WriteString(out)
  1809  
  1810  					util.Logger.Debug("output from host", "arise", "master", "ouput", masterMsg.Data, "input", out)
  1811  
  1812  					// update client with new state of terminal
  1813  					server.SetCurrentState(complete)
  1814  				}
  1815  			}
  1816  		}
  1817  
  1818  		// write user input and terminal writeback to the host
  1819  		if terminalToHost.Len() > 0 {
  1820  			_, err := ptmx.WriteString(terminalToHost.String())
  1821  			if err != nil && !signals.AnySignal() { // avoid conflict with signal
  1822  				server.StartShutdown()
  1823  			}
  1824  
  1825  			util.Logger.Debug("input to host", "arise", "merge-", "data", terminalToHost.String())
  1826  		}
  1827  
  1828  		idleShutdown := false
  1829  		if networkTimeoutMs > 0 && networkTimeoutMs <= timeSinceRemoteState {
  1830  			// if network timeout is set and over networkTimeoutMs quit this session.
  1831  			idleShutdown = true
  1832  			// fmt.Printf("Network idle for %d seconds.\n", timeSinceRemoteState/1000)
  1833  			util.Logger.Info("Network idle for x seconds", "seconds", timeSinceRemoteState/1000)
  1834  		}
  1835  
  1836  		if signals.GotSignal(syscall.SIGUSR1) {
  1837  			if networkSignaledTimeoutMs == 0 || networkSignaledTimeoutMs <= timeSinceRemoteState {
  1838  				idleShutdown = true
  1839  				// fmt.Printf("Network idle for %d seconds when SIGUSR1 received.\n", timeSinceRemoteState/1000)
  1840  				util.Logger.Info("Network idle for x seconds when SIGUSR1 received", "seconds",
  1841  					timeSinceRemoteState/1000)
  1842  			}
  1843  		}
  1844  
  1845  		if signals.AnySignal() || idleShutdown {
  1846  			util.Logger.Debug("got signal: start shutdown",
  1847  				"HasRemoteAddr", server.HasRemoteAddr(),
  1848  				"ShutdownInProgress", server.ShutdownInProgress())
  1849  			signals.Clear()
  1850  			// shutdown signal
  1851  			if server.HasRemoteAddr() && !server.ShutdownInProgress() {
  1852  				server.StartShutdown()
  1853  				util.Logger.Debug("serve start shutdown")
  1854  			} else {
  1855  				util.Logger.Debug("got signal: break loop",
  1856  					"HasRemoteAddr", server.HasRemoteAddr(),
  1857  					"ShutdownInProgress", server.ShutdownInProgress())
  1858  				break
  1859  			}
  1860  		}
  1861  
  1862  		// quit if our shutdown has been acknowledged
  1863  		if server.ShutdownInProgress() && server.ShutdownAcknowledged() {
  1864  			util.Logger.Debug("shutdown", "from", "acked", "port", server.GetServerPort())
  1865  			break
  1866  		}
  1867  
  1868  		// quit after shutdown acknowledgement timeout
  1869  		if server.ShutdownInProgress() && server.ShutdownAckTimedout() {
  1870  			util.Logger.Warn("shutdown", "from", "act timeout", "port", server.GetServerPort())
  1871  			break
  1872  		}
  1873  
  1874  		// quit if we received and acknowledged a shutdown request
  1875  		if server.CounterpartyShutdownAckSent() {
  1876  			util.Logger.Warn("shutdown", "from", "peer acked", "port", server.GetServerPort())
  1877  			break
  1878  		}
  1879  
  1880  		// update utmp if has been more than 30 seconds since heard from client
  1881  		if utmpSupport && connectedUtmp && timeSinceRemoteState > 30000 {
  1882  			if !server.Awaken(now) {
  1883  				utmps.RemoveRecord(pts.Name(), os.Getpid())
  1884  				utmpHost := fmt.Sprintf("%s:%s", frontend.CommandServerName, server.GetServerPort())
  1885  				utmps.AddRecord(pts.Name(), user, utmpHost, os.Getpid())
  1886  				connectedUtmp = false
  1887  				// util.Log.Info("serve doesn't heard from client over 16 minutes.")
  1888  			}
  1889  		}
  1890  
  1891  		if complete.SetEchoAck(now) && !server.ShutdownInProgress() {
  1892  			// update client with new echo ack
  1893  			server.SetCurrentState(complete)
  1894  		}
  1895  
  1896  		// util.Log.Debug("mainLoop","point", 500)
  1897  		err := server.Tick()
  1898  		if err != nil {
  1899  			util.Logger.Warn("#serve send failed", "error", err)
  1900  		}
  1901  		// util.Log.Debug("mainLoop","point", "d")
  1902  
  1903  		now = time.Now().UnixMilli()
  1904  		if server.GetRemoteStateNum() == 0 && server.ShutdownInProgress() {
  1905  			// abort if no connection over TimeoutIfNoConnect seconds
  1906  
  1907  			util.Logger.Warn("No connection within x seconds", "seconds", frontend.TimeoutIfNoConnect/1000,
  1908  				"timeout", "shutdown", "port", server.GetServerPort())
  1909  			break
  1910  		} else if server.GetRemoteStateNum() != 0 && timeSinceRemoteState >= frontend.TimeoutIfNoResp {
  1911  			// if no response from client over TimeoutIfNoResp seconds
  1912  			// if now-server.GetSentStateLastTimestamp() >= frontend.TimeoutIfNoResp-network.SERVER_ASSOCIATION_TIMEOUT {
  1913  			if !server.Awaken(now) {
  1914  				// abort if no request send over TimeoutIfNoResp seconds
  1915  				util.Logger.Warn("Time out for no client request", "seconds", frontend.TimeoutIfNoResp/1000,
  1916  					"port", server.GetServerPort(), "timeSinceRemoteState", timeSinceRemoteState)
  1917  				break
  1918  			}
  1919  			// }
  1920  		}
  1921  	}
  1922  
  1923  	// stop signal and network
  1924  	signal.Stop(sigChan)
  1925  	server.Close()
  1926  
  1927  	if !childReleased {
  1928  		util.Logger.Debug("release shell lock", "action", earlyShutdown)
  1929  		pw.Write([]byte(earlyShutdown))
  1930  		if err := pw.Close(); err != nil {
  1931  			util.Logger.Error("send start shell message failed", "error", err)
  1932  		}
  1933  		childReleased = true
  1934  	}
  1935  
  1936  	// shutdown the goroutines: file reader and network reader
  1937  	select {
  1938  	case fileDownChan <- "done":
  1939  	default:
  1940  	}
  1941  	select {
  1942  	case networkDownChan <- "done":
  1943  	default:
  1944  	}
  1945  
  1946  	// consume last message to free reader if possible
  1947  	select {
  1948  	case <-fileChan:
  1949  	default:
  1950  	}
  1951  	select {
  1952  	case <-networkChan:
  1953  	default:
  1954  	}
  1955  	eg.Wait()
  1956  
  1957  	if syslogSupport {
  1958  		syslogWriter.Info(fmt.Sprintf("user %s session end %s -> port %s",
  1959  			user, server.GetRemoteAddr(), server.GetServerPort()))
  1960  	}
  1961  	util.Logger.Info("user session end", "user", user)
  1962  
  1963  	return nil
  1964  }
  1965  
  1966  // worker started by mainSrv.run(). worker will listen on specified port and
  1967  // forward user input to shell (started by runWorker. the output is forward
  1968  // to the network.
  1969  func runChild(conf *Config) (err error) {
  1970  	// name := filepath.Join(os.TempDir(), fmt.Sprintf("%s-%d.%s", frontend.CommandServerName, os.Getpid(), "log"))
  1971  	// file, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
  1972  	// defer file.Close()
  1973  	//
  1974  	// if err != nil {
  1975  	// 	fmt.Printf("error %#v\n", err)
  1976  	// 	return
  1977  	// }
  1978  	// os.Stderr = file
  1979  	// util.Logger.CreateLogger(os.Stderr, true, slog.LevelDebug)
  1980  	// fmt.Println("log is ready", file)
  1981  
  1982  	// prepare unix socket client (datagram)
  1983  	uxClient, err := newUxClient()
  1984  	if err != nil {
  1985  		util.Logger.Error("init uds client failed", "error", err)
  1986  		return
  1987  	}
  1988  
  1989  	defer func() {
  1990  		// notify this child is done
  1991  		// exChan <- conf.desiredPort
  1992  		uxClient.send(fmt.Sprintf("%s:%s,%s", _RunHeader, conf.desiredPort, "shutdown"))
  1993  		uxClient.close()
  1994  	}()
  1995  
  1996  	// parse destination
  1997  	first := strings.Split(conf.destination, "@")
  1998  	if len(first) == 2 {
  1999  		conf.user = first[0]
  2000  		// second := strings.Split(first[1], ":")
  2001  		conf.host = ""
  2002  	}
  2003  	util.Logger.Debug("runChild", "user", conf.user, "host", conf.host, "term", conf.term,
  2004  		"desiredPort", conf.desiredPort, "destination", conf.destination)
  2005  	/*
  2006  		If this variable is set to a positive integer number, it specifies how
  2007  		long (in seconds) apshd will wait to receive an update from the
  2008  		client before exiting.  Since aprilsh is very useful for mobile
  2009  		clients with intermittent operation and connectivity, we suggest
  2010  		setting this variable to a high value, such as 604800 (one week) or
  2011  		2592000 (30 days).  Otherwise, apshd will wait indefinitely for a
  2012  		client to reappear.  This variable is somewhat similar to the TMOUT
  2013  		variable found in many Bourne shells. However, it is not a login-session
  2014  		inactivity timeout; it only applies to network connectivity.
  2015  	*/
  2016  	networkTimeout := getTimeFrom("APRILSH_SERVER_NETWORK_TMOUT", 0)
  2017  
  2018  	/*
  2019  		If this variable is set to a positive integer number, it specifies how
  2020  		long (in seconds) apshd will ignore SIGUSR1 while waiting to receive
  2021  		an update from the client.  Otherwise, SIGUSR1 will always terminate
  2022  		apshd. Users and administrators may implement scripts to clean up
  2023  		disconnected aprilsh sessions. With this variable set, a user or
  2024  		administrator can issue
  2025  
  2026  		$ pkill -SIGUSR1 aprilsh-server
  2027  
  2028  		to kill disconnected sessions without killing connected login
  2029  		sessions.
  2030  	*/
  2031  	networkSignaledTimeout := getTimeFrom("APRILSH_SERVER_SIGNAL_TMOUT", 0)
  2032  
  2033  	// util.Log.Debug("runWorker", "networkTimeout", networkTimeout,
  2034  	// 	"networkSignaledTimeout", networkSignaledTimeout)
  2035  
  2036  	// get initial window size
  2037  	var windowSize *unix.Winsize
  2038  	windowSize, err = unix.IoctlGetWinsize(int(os.Stdin.Fd()), unix.TIOCGWINSZ)
  2039  	// windowSize, err := pty.GetsizeFull(os.Stdin)
  2040  	if err != nil || windowSize.Col == 0 || windowSize.Row == 0 {
  2041  		// Fill in sensible defaults. */
  2042  		// They will be overwritten by client on first connection.
  2043  		windowSize.Col = 80
  2044  		windowSize.Row = 24
  2045  	}
  2046  	// util.Log.Debug("init terminal size", "cols", windowSize.Col, "rows", windowSize.Row)
  2047  
  2048  	// open parser and terminal
  2049  	savedLines := terminal.SaveLinesRowsOption
  2050  	terminal, err := statesync.NewComplete(int(windowSize.Col), int(windowSize.Row), savedLines)
  2051  
  2052  	// open network
  2053  	blank := &statesync.UserStream{}
  2054  	server := network.NewTransportServer(terminal, blank, conf.desiredIP, conf.desiredPort)
  2055  	server.SetVerbose(uint(conf.verbose))
  2056  	// defer server.Close()
  2057  
  2058  	/*
  2059  		// If server is run on a pty, then typeahead may echo and break mosh.pl's
  2060  		// detection of the CONNECT message.  Print it on a new line to bodge
  2061  		// around that.
  2062  
  2063  		if term.IsTerminal(int(os.Stdin.Fd())) {
  2064  			fmt.Printf("\r\n")
  2065  		}
  2066  	*/
  2067  
  2068  	// send the key to run()
  2069  	uxClient.send(fmt.Sprintf("%s:%s,%s", _KeyHeader, conf.desiredPort, server.GetKey()))
  2070  	// exChan <- server.GetKey()
  2071  
  2072  	// in mosh: the parent print this to stderr.
  2073  	// fmt.Printf("#runWorker %s CONNECT %s %s\n", COMMAND_NAME, network.Port(), network.GetKey())
  2074  	// printWelcome(os.Stdout, os.Getpid(), os.Stdin)
  2075  
  2076  	// prepare for openPTS fail
  2077  	if conf.flowControl == _FC_OPEN_PTS_FAIL {
  2078  		windowSize = nil
  2079  	}
  2080  
  2081  	ptmx, pts, err := openPTS(windowSize)
  2082  	if err != nil {
  2083  		util.Logger.Warn("openPTS fail", "error", err)
  2084  		return err
  2085  	}
  2086  	defer func() {
  2087  		ptmx.Close()
  2088  		// pts.Close()
  2089  	}() // Best effort.
  2090  	// fmt.Printf("#runWorker openPTS successfully.\n")
  2091  
  2092  	// SetProcessName(frontend.CommandClientName + ": [" + pts.Name() + "]")
  2093  
  2094  	// use pipe to signal when to start shell
  2095  	// pw and pr is close inside serve() and startShell()
  2096  	pr, pw := io.Pipe()
  2097  
  2098  	// prepare host field for utmp record
  2099  	utmpHost := fmt.Sprintf("%s:%s", frontend.CommandServerName, server.GetServerPort())
  2100  
  2101  	// start the udp server, serve the udp request
  2102  	var wg sync.WaitGroup
  2103  	wg.Add(1)
  2104  	go func() {
  2105  		// add utmp entry
  2106  		if utmpSupport {
  2107  			ok := utmps.AddRecord(pts.Name(), conf.user, conf.host, os.Getpid())
  2108  			if !ok {
  2109  				// first utmpSupport means: we can read from utmp
  2110  				// second utmpSupport means: we can write from utmp
  2111  				utmpSupport = false
  2112  				util.Logger.Warn("runChild can't update utmp")
  2113  			}
  2114  		}
  2115  		conf.serve(ptmx, pts, pw, terminal, server, networkTimeout, networkSignaledTimeout, conf.user)
  2116  		uxClient.send(fmt.Sprintf("%s:%s,%s", _ServeHeader, conf.desiredPort, "shutdown"))
  2117  
  2118  		// clear utmp entry
  2119  		if utmpSupport {
  2120  			utmps.RemoveRecord(pts.Name(), os.Getpid())
  2121  		}
  2122  		wg.Done()
  2123  	}()
  2124  	util.Logger.Info("start listening on", "port", conf.desiredPort, "clientTERM", conf.term)
  2125  
  2126  	// TODO update last log ?
  2127  	// util.UpdateLastLog(ptmxName, getCurrentUser(), utmpHost)
  2128  
  2129  	// start the shell with pts
  2130  	shell, err := startShellProcess(pts, pr, utmpHost, conf)
  2131  	pts.Close() // it's copied by shell process, it's safe to close it here.
  2132  	if err != nil {
  2133  		util.Logger.Warn("startShell fail", "error", err)
  2134  		uxClient.send(fmt.Sprintf("%s:%s,%d", _ShellHeader, conf.desiredPort, 0))
  2135  	} else {
  2136  
  2137  		uxClient.send(fmt.Sprintf("%s:%s,%d", _ShellHeader, conf.desiredPort, shell.Pid))
  2138  		// wait for the shell to finish.
  2139  		var state *os.ProcessState
  2140  		state, err = shell.Wait()
  2141  		if err != nil || state.Exited() {
  2142  			if err != nil {
  2143  				util.Logger.Warn("shell.Wait fail", "error", err, "state", state)
  2144  				// } else {
  2145  				// util.Log.Debug("shell.Wait quit", "state.exited", state.Exited())
  2146  			}
  2147  		}
  2148  	}
  2149  
  2150  	// util.Logger.Debug("runChild wait")
  2151  	// wait serve to finish
  2152  	wg.Wait()
  2153  	util.Logger.Info("stop listening on", "port", conf.desiredPort)
  2154  
  2155  	// fmt.Printf("[%s is exiting.]\n", frontend.COMMAND_SERVER_NAME)
  2156  	// https://www.dolthub.com/blog/2022-11-28-go-os-exec-patterns/
  2157  	// https://www.prakharsrivastav.com/posts/golang-context-and-cancellation/
  2158  
  2159  	// util.Log.Debug("runWorker quit", "port", conf.desiredPort)
  2160  	return err
  2161  }
  2162  
  2163  // parse the flag first, print help or version based on flag
  2164  // then run the main listening server
  2165  // aprilsh-server should be installed under $HOME/.local/bin
  2166  func main() {
  2167  	str, ok := os.LookupEnv(envArgs)
  2168  	if ok {
  2169  		os.Args = append(os.Args, strings.Split(str, " ")...)
  2170  		os.Unsetenv(envArgs)
  2171  	}
  2172  	str, ok = os.LookupEnv(envUDS)
  2173  	if ok {
  2174  		unixsockAddr = str
  2175  		os.Unsetenv(envArgs)
  2176  	}
  2177  
  2178  	conf, _, err := parseFlags(os.Args[0], os.Args[1:])
  2179  	if errors.Is(err, flag.ErrHelp) {
  2180  		frontend.PrintUsage("", usage)
  2181  		return
  2182  	} else if err != nil {
  2183  		frontend.PrintUsage(err.Error())
  2184  		return
  2185  	} else if hint, ok := conf.buildConfig(); !ok {
  2186  		frontend.PrintUsage(hint)
  2187  		return
  2188  	}
  2189  
  2190  	if conf.version {
  2191  		printVersion()
  2192  		return
  2193  	}
  2194  
  2195  	fmt.Fprintf(os.Stderr, "main process %d args=%s, uds=%s\n", os.Getpid(), os.Args, unixsockAddr)
  2196  
  2197  	// For security, make sure we don't dump core
  2198  	encrypt.DisableDumpingCore()
  2199  
  2200  	if conf.begin {
  2201  		beginChild(conf)
  2202  		return
  2203  	}
  2204  
  2205  	// setup client log file
  2206  	switch conf.verbose {
  2207  	case util.DebugLevel:
  2208  		util.Logger.CreateLogger(os.Stderr, conf.addSource, slog.LevelDebug)
  2209  	case util.TraceLevel:
  2210  		util.Logger.CreateLogger(os.Stderr, conf.addSource, util.LevelTrace)
  2211  	default:
  2212  		util.Logger.CreateLogger(os.Stderr, conf.addSource, slog.LevelInfo)
  2213  	}
  2214  
  2215  	// setup syslog
  2216  	syslogWriter, err = syslog.New(syslog.LOG_WARNING|syslog.LOG_LOCAL7, frontend.CommandServerName)
  2217  	if err != nil {
  2218  		util.Logger.Warn("can't find syslog service on this server.")
  2219  		syslogSupport = false
  2220  	} else {
  2221  		syslogSupport = true
  2222  	}
  2223  	defer func() {
  2224  		if syslogSupport {
  2225  			syslogWriter.Close()
  2226  		}
  2227  	}()
  2228  	// https://jvns.ca/blog/2017/09/24/profiling-go-with-pprof/
  2229  	//
  2230  	// cpuf, err := os.Create("cpu.profile")
  2231  	// if err != nil {
  2232  	// 	fmt.Println(err)
  2233  	// 	return
  2234  	// }
  2235  	// pprof.StartCPUProfile(cpuf)
  2236  	// defer pprof.StopCPUProfile()
  2237  
  2238  	// f, err := os.Create("mem.profile")
  2239  	// if err != nil {
  2240  	// 	fmt.Println(err)
  2241  	// 	return
  2242  	// }
  2243  	// pprof.WriteHeapProfile(f)
  2244  	// defer f.Close()
  2245  
  2246  	// we need a webserver to get the pprof webserver
  2247  	// go func() {
  2248  	// 	fmt.Println(http.ListenAndServe("localhost:6060", nil))
  2249  	// }()
  2250  
  2251  	// run child process
  2252  	if conf.child {
  2253  		runChild(conf)
  2254  		return
  2255  	}
  2256  
  2257  	// start mainSrv
  2258  	srv := newMainSrv(conf)
  2259  	srv.start(conf)
  2260  	srv.wait()
  2261  }