github.com/ericwq/aprilsh@v0.0.0-20240517091432-958bc568daa0/frontend/server/single.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  // func runWorker(conf *Config, exChan chan string, whChan chan workhorse) (err error) {
     8  // 	defer func() {
     9  // 		// notify this worker is done
    10  // 		exChan <- conf.desiredPort
    11  // 	}()
    12  //
    13  // 	/*
    14  // 		If this variable is set to a positive integer number, it specifies how
    15  // 		long (in seconds) apshd will wait to receive an update from the
    16  // 		client before exiting.  Since aprilsh is very useful for mobile
    17  // 		clients with intermittent operation and connectivity, we suggest
    18  // 		setting this variable to a high value, such as 604800 (one week) or
    19  // 		2592000 (30 days).  Otherwise, apshd will wait indefinitely for a
    20  // 		client to reappear.  This variable is somewhat similar to the TMOUT
    21  // 		variable found in many Bourne shells. However, it is not a login-session
    22  // 		inactivity timeout; it only applies to network connectivity.
    23  // 	*/
    24  // 	networkTimeout := getTimeFrom("APRILSH_SERVER_NETWORK_TMOUT", 0)
    25  //
    26  // 	/*
    27  // 		If this variable is set to a positive integer number, it specifies how
    28  // 		long (in seconds) apshd will ignore SIGUSR1 while waiting to receive
    29  // 		an update from the client.  Otherwise, SIGUSR1 will always terminate
    30  // 		apshd. Users and administrators may implement scripts to clean up
    31  // 		disconnected aprilsh sessions. With this variable set, a user or
    32  // 		administrator can issue
    33  //
    34  // 		$ pkill -SIGUSR1 aprilsh-server
    35  //
    36  // 		to kill disconnected sessions without killing connected login
    37  // 		sessions.
    38  // 	*/
    39  // 	networkSignaledTimeout := getTimeFrom("APRILSH_SERVER_SIGNAL_TMOUT", 0)
    40  //
    41  // 	// util.Log.Debug("runWorker", "networkTimeout", networkTimeout,
    42  // 	// 	"networkSignaledTimeout", networkSignaledTimeout)
    43  //
    44  // 	// get initial window size
    45  // 	var windowSize *unix.Winsize
    46  // 	windowSize, err = unix.IoctlGetWinsize(int(os.Stdin.Fd()), unix.TIOCGWINSZ)
    47  // 	// windowSize, err := pty.GetsizeFull(os.Stdin)
    48  // 	if err != nil || windowSize.Col == 0 || windowSize.Row == 0 {
    49  // 		// Fill in sensible defaults. */
    50  // 		// They will be overwritten by client on first connection.
    51  // 		windowSize.Col = 80
    52  // 		windowSize.Row = 24
    53  // 	}
    54  // 	// util.Log.Debug("init terminal size", "cols", windowSize.Col, "rows", windowSize.Row)
    55  //
    56  // 	// open parser and terminal
    57  // 	savedLines := terminal.SaveLinesRowsOption
    58  // 	terminal, err := statesync.NewComplete(int(windowSize.Col), int(windowSize.Row), savedLines)
    59  //
    60  // 	// open network
    61  // 	blank := &statesync.UserStream{}
    62  // 	server := network.NewTransportServer(terminal, blank, conf.desiredIP, conf.desiredPort)
    63  // 	server.SetVerbose(uint(conf.verbose))
    64  // 	// defer server.Close()
    65  //
    66  // 	/*
    67  // 		// If server is run on a pty, then typeahead may echo and break mosh.pl's
    68  // 		// detection of the CONNECT message.  Print it on a new line to bodge
    69  // 		// around that.
    70  //
    71  // 		if term.IsTerminal(int(os.Stdin.Fd())) {
    72  // 			fmt.Printf("\r\n")
    73  // 		}
    74  // 	*/
    75  //
    76  // 	exChan <- server.GetKey() // send the key to run()
    77  //
    78  // 	// in mosh: the parent print this to stderr.
    79  // 	// fmt.Printf("#runWorker %s CONNECT %s %s\n", COMMAND_NAME, network.Port(), network.GetKey())
    80  // 	// printWelcome(os.Stdout, os.Getpid(), os.Stdin)
    81  //
    82  // 	// prepare for openPTS fail
    83  // 	if conf.verbose == _VERBOSE_OPEN_PTS_FAIL {
    84  // 		windowSize = nil
    85  // 	}
    86  //
    87  // 	ptmx, pts, err := openPTS(windowSize)
    88  // 	if err != nil {
    89  // 		util.Log.Warn("openPTS fail", "error", err)
    90  // 		whChan <- workhorse{}
    91  // 		return err
    92  // 	}
    93  // 	defer func() {
    94  // 		ptmx.Close()
    95  // 		// pts.Close()
    96  // 	}() // Best effort.
    97  // 	// fmt.Printf("#runWorker openPTS successfully.\n")
    98  //
    99  // 	// use pipe to signal when to start shell
   100  // 	// pw and pr is close inside serve() and startShell()
   101  // 	pr, pw := io.Pipe()
   102  //
   103  // 	// prepare host field for utmp record
   104  // 	utmpHost := fmt.Sprintf("%s:%s", frontend.CommandServerName, server.GetServerPort())
   105  //
   106  // 	// add utmp entry
   107  // 	if utmpSupport {
   108  // 		ok := util.AddUtmpx(pts, utmpHost)
   109  // 		if !ok {
   110  // 			utmpSupport = false
   111  // 			util.Log.Warn("#runWorker can't update utmp")
   112  // 		}
   113  // 	}
   114  //
   115  // 	// start the udp server, serve the udp request
   116  // 	var wg sync.WaitGroup
   117  // 	wg.Add(1)
   118  // 	// waitChan := make(chan bool)
   119  // 	// go conf.serve(ptmx, pw, terminal, waitChan, network, networkTimeout, networkSignaledTimeout)
   120  // 	go func() {
   121  // 		conf.serve(ptmx, pts, pw, terminal, server, networkTimeout, networkSignaledTimeout, conf.user)
   122  // 		exChan <- fmt.Sprintf("%s:shutdown", conf.desiredPort)
   123  // 		wg.Done()
   124  // 	}()
   125  //
   126  // 	// TODO update last log ?
   127  // 	// util.UpdateLastLog(ptmxName, getCurrentUser(), utmpHost)
   128  //
   129  // 	defer func() { // clear utmp entry
   130  // 		if utmpSupport {
   131  // 			util.ClearUtmpx(pts)
   132  // 		}
   133  // 	}()
   134  //
   135  // 	util.Log.Info("start listening on", "port", conf.desiredPort, "clientTERM", conf.term)
   136  //
   137  // 	// start the shell with pts
   138  // 	shell, err := startShell(pts, pr, utmpHost, conf)
   139  // 	pts.Close() // it's copied by shell process, it's safe to close it here.
   140  // 	if err != nil {
   141  // 		util.Log.Warn("startShell fail", "error", err)
   142  // 		whChan <- workhorse{}
   143  // 	} else {
   144  //
   145  // 		whChan <- workhorse{shell, 0}
   146  // 		// wait for the shell to finish.
   147  // 		var state *os.ProcessState
   148  // 		state, err = shell.Wait()
   149  // 		if err != nil || state.Exited() {
   150  // 			if err != nil {
   151  // 				util.Log.Warn("shell.Wait fail", "error", err, "state", state)
   152  // 				// } else {
   153  // 				// util.Log.Debug("shell.Wait quit", "state.exited", state.Exited())
   154  // 			}
   155  // 		}
   156  // 	}
   157  //
   158  // 	// wait serve to finish
   159  // 	wg.Wait()
   160  // 	util.Log.Info("stop listening on", "port", conf.desiredPort)
   161  //
   162  // 	// fmt.Printf("[%s is exiting.]\n", frontend.COMMAND_SERVER_NAME)
   163  // 	// https://www.dolthub.com/blog/2022-11-28-go-os-exec-patterns/
   164  // 	// https://www.prakharsrivastav.com/posts/golang-context-and-cancellation/
   165  //
   166  // 	// util.Log.Debug("runWorker quit", "port", conf.desiredPort)
   167  // 	return err
   168  // }
   169  
   170  /*
   171  func (m *mainSrv) start(conf *Config) {
   172  	// listen the port
   173  	if err := m.listen(conf); err != nil {
   174  		util.Log.Warn("listen failed", "error", err)
   175  		return
   176  	}
   177  
   178  	// start main server waiting for open/close message.
   179  	m.wg.Add(1)
   180  	go func() {
   181  		m.run(conf)
   182  		m.wg.Done()
   183  	}()
   184  
   185  	// shutdown if the auto stop flag is set
   186  	if conf.autoStop > 0 {
   187  		time.AfterFunc(time.Duration(conf.autoStop)*time.Second, func() {
   188  			m.downChan <- true
   189  		})
   190  	}
   191  }
   192  
   193  func (m *mainSrv) run(conf *Config) {
   194  	if m.conn == nil {
   195  		return
   196  	}
   197  	// prepare to receive the signal
   198  	sig := make(chan os.Signal, 1)
   199  	signal.Notify(sig, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
   200  
   201  	// clean up
   202  	defer func() {
   203  		signal.Stop(sig)
   204  		if syslogSupport {
   205  			syslogWriter.Info(fmt.Sprintf("stop listening on %s.", m.conn.LocalAddr()))
   206  		}
   207  		m.conn.Close()
   208  		util.Log.Info("stop listening on", "port", m.port)
   209  	}()
   210  
   211  	buf := make([]byte, 128)
   212  	shutdown := false
   213  
   214  	if syslogSupport {
   215  		syslogWriter.Info(fmt.Sprintf("start listening on %s.", m.conn.LocalAddr()))
   216  	}
   217  
   218  	printWelcome(os.Getpid(), m.port, nil)
   219  	for {
   220  		select {
   221  		case portStr := <-m.exChan:
   222  			m.cleanWorkers(portStr)
   223  			// util.Log.Info("run some worker is done","port", portStr)
   224  		case ss := <-sig:
   225  			switch ss {
   226  			case syscall.SIGHUP: // TODO:reload the config?
   227  				util.Log.Info("got signal: SIGHUP")
   228  			case syscall.SIGTERM, syscall.SIGINT:
   229  				util.Log.Info("got signal: SIGTERM or SIGINT")
   230  				shutdown = true
   231  			}
   232  		case <-m.downChan:
   233  			// another way to shutdown besides signal
   234  			shutdown = true
   235  		default:
   236  		}
   237  
   238  		if shutdown {
   239  			// util.Log.Debug("run","shutdown", shutdown)
   240  			if len(m.workers) == 0 {
   241  				return
   242  			} else {
   243  				// send kill message to the workers
   244  				for i := range m.workers {
   245  					m.workers[i].child.Kill()
   246  					// util.Log.Debug("stop shell","port", i)
   247  				}
   248  				// wait for workers to finish, set time out to prevent dead lock
   249  				timeout := time.NewTimer(time.Duration(200) * time.Millisecond)
   250  				for len(m.workers) > 0 {
   251  					select {
   252  					case portStr := <-m.exChan: // some worker is done
   253  						m.cleanWorkers(portStr)
   254  					case t := <-timeout.C:
   255  						util.Log.Warn("run quit with timeout", "timeout", t)
   256  						return
   257  					default:
   258  					}
   259  				}
   260  				return
   261  			}
   262  		}
   263  
   264  		// set read time out: 200ms
   265  		m.conn.SetDeadline(time.Now().Add(time.Millisecond * time.Duration(m.timeout)))
   266  		n, addr, err := m.conn.ReadFromUDP(buf)
   267  		if err != nil {
   268  			if errors.Is(err, os.ErrDeadlineExceeded) {
   269  				// fmt.Printf("#run read time out, workers=%d, shutdown=%t, err=%s\n", len(m.workers), shutdown, err)
   270  				continue
   271  			} else {
   272  				// take a break in case reading error
   273  				time.Sleep(time.Duration(5) * time.Millisecond)
   274  				// fmt.Println("#run read error: ", err)
   275  				continue
   276  			}
   277  		}
   278  
   279  		req := strings.TrimSpace(string(buf[0:n]))
   280  		if strings.HasPrefix(req, frontend.AprilshMsgOpen) { // 'open aprilsh:'
   281  			if len(m.workers) >= maxPortLimit {
   282  				resp := m.writeRespTo(addr, frontend.AprishMsgClose, "over max port limit")
   283  				util.Log.Warn("over max port limit", "request", req, "response", resp)
   284  				continue
   285  			}
   286  			// prepare next port
   287  			p := m.getAvailabePort()
   288  
   289  			// open aprilsh:TERM,user@server.domain
   290  			// prepare configuration
   291  			conf2 := *conf
   292  			conf2.desiredPort = fmt.Sprintf("%d", p)
   293  			body := strings.Split(req, ":")
   294  			content := strings.Split(body[1], ",")
   295  			if len(content) != 2 {
   296  				resp := m.writeRespTo(addr, frontend.AprilshMsgOpen, "malform request")
   297  				util.Log.Warn("malform request", "request", req, "response", resp)
   298  				continue
   299  			}
   300  			conf2.term = content[0]
   301  			conf2.destination = content[1]
   302  
   303  			// parse user and host from destination
   304  			idx := strings.Index(content[1], "@")
   305  			if idx > 0 && idx < len(content[1])-1 {
   306  				conf2.host = content[1][idx+1:]
   307  				conf2.user = content[1][:idx]
   308  			} else {
   309  				// return "target parameter should be in the form of User@Server", false
   310  				resp := m.writeRespTo(addr, frontend.AprilshMsgOpen, "malform destination")
   311  				util.Log.Warn("malform destination", "destination", content[1], "response", resp)
   312  
   313  				continue
   314  			}
   315  
   316  			// we don't need to check if user exist, ssh already done that before
   317  
   318  			// For security, make sure we don't dump core
   319  			encrypt.DisableDumpingCore()
   320  
   321  			// start the worker
   322  			m.wg.Add(1)
   323  			go func(conf *Config, exChan chan string, whChan chan workhorse) {
   324  				m.runWorker(conf, exChan, whChan)
   325  				m.wg.Done()
   326  			}(&conf2, m.exChan, m.whChan)
   327  
   328  			// blocking read the key from worker
   329  			key := <-m.exChan
   330  
   331  			// response session key and udp port to client
   332  			msg := fmt.Sprintf("%d,%s", p, key)
   333  			m.writeRespTo(addr, frontend.AprilshMsgOpen, msg)
   334  
   335  			// blocking read the workhorse from runWorker
   336  			wh := <-m.whChan
   337  			if wh.child != nil {
   338  				m.workers[p] = &wh
   339  			}
   340  		} else if strings.HasPrefix(req, frontend.AprishMsgClose) { // 'close aprilsh:[port]'
   341  			pstr := strings.TrimPrefix(req, frontend.AprishMsgClose)
   342  			port, err := strconv.Atoi(pstr)
   343  			if err == nil {
   344  				// find workhorse
   345  				if wh, ok := m.workers[port]; ok {
   346  					// kill the process, TODO SIGKILL or SIGTERM?
   347  					wh.child.Kill()
   348  
   349  					m.writeRespTo(addr, frontend.AprishMsgClose, "done")
   350  				} else {
   351  					resp := m.writeRespTo(addr, frontend.AprishMsgClose, "port does not exist")
   352  					util.Log.Warn("port does not exist", "request", req, "response", resp)
   353  				}
   354  			} else {
   355  				resp := m.writeRespTo(addr, frontend.AprishMsgClose, "wrong port number")
   356  				util.Log.Warn("wrong port number", "request", req, "response", resp)
   357  			}
   358  		} else {
   359  			resp := m.writeRespTo(addr, frontend.AprishMsgClose, "unknow request")
   360  			util.Log.Warn("unknow request", "request", req, "response", resp)
   361  		}
   362  	}
   363  
   364  	// just for test purpose:
   365  	//
   366  	// in aprilsh: we can use nc client to get the key and send it back to client.
   367  	// we don't print it to the stdout as mosh did.
   368  	//
   369  	// send udp request and read reply
   370  	// % echo "open aprilsh:" | nc localhost 6000 -u -w 1
   371  	// % echo "close aprilsh:6001" | nc localhost 6000 -u -w 1
   372  	//
   373  	// send udp request to remote host
   374  	// % ssh ide@localhost  "echo 'open aprilsh:' | nc localhost 6000 -u -w 1"
   375  }
   376  */