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 }