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 */