Skip to content

Commit

Permalink
Polishing, fix deadlock
Browse files Browse the repository at this point in the history
  • Loading branch information
Philipp Heckel committed Sep 26, 2021
1 parent 3a4bfeb commit 7fb5e45
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 24 deletions.
37 changes: 26 additions & 11 deletions bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ const (
"the main `channel`, or in `split` mode, use the respective keywords (default: `%s`). To define the terminal size, use the words " +
"`tiny`, `small`, `medium` or `large` (default: `%s`). Use `full` or `trim` to set the window mode (default: `%s`), and `everyone` " +
"or `only-me` to define who can send commands (default: `%s`). Send `record` or `norecord` to define if your session should be " +
"recorded (default: `%s`). To start a private REPL session, just DM me."
"recorded (default: `%s`)."
shareMessage = "Using the word `share` will allow you to share your own terminal here in the chat. Terminal sharing " +
"sessions are always started in `only-me` mode, unless overridden."
webMessage = "Use the word `web` or `noweb` to enable a web-based terminal for this session (default: `%s`)."
unknownCommandMessage = "I am not quite sure what you mean by _%s_ ⁉"
misconfiguredMessage = "😭 Oh no. It looks like REPLbot is misconfigured. I couldn't find any scripts to run."
maxTotalSessionsExceededMessage = "😭 There are too many active sessions. Please wait until another session is closed."
Expand Down Expand Up @@ -137,6 +138,9 @@ func (b *Bot) Stop() {
if sess.conf.share != nil {
delete(b.shareUser, sess.conf.share.user)
}
if sess.webPrefix != "" {
delete(b.webPrefix, sess.webPrefix)
}
}
b.cancelFn() // This must be at the end, see app.go
}
Expand Down Expand Up @@ -228,8 +232,6 @@ func (b *Bot) parseSessionConfig(ev *messageEvent) (*sessionConfig, error) {
conf.size = config.Sizes[field]
case recordCommand, noRecordCommand:
conf.record = field == recordCommand
case webCommand, noWebCommand:
conf.web = field == webCommand
default:
if b.config.ShareEnabled() && field == shareCommand {
relayPort, err := util.RandomPort()
Expand All @@ -251,6 +253,8 @@ func (b *Bot) parseSessionConfig(ev *messageEvent) (*sessionConfig, error) {
hostKeyPair: hostKeyPair,
clientKeyPair: clientKeyPair,
}
} else if b.config.WebHost != "" && (field == webCommand || field == noWebCommand) {
conf.web = field == webCommand
} else if s := b.config.Script(field); conf.script == "" && s != "" {
conf.script = s
} else {
Expand Down Expand Up @@ -353,6 +357,9 @@ func (b *Bot) startSession(conf *sessionConfig) error {
if conf.share != nil {
delete(b.shareUser, conf.share.user)
}
if sess.webPrefix != "" {
delete(b.webPrefix, sess.webPrefix)
}
b.mu.Unlock()
}()
return nil
Expand All @@ -370,6 +377,13 @@ func (b *Bot) handleHelp(channel, thread string, err error) error {
} else {
messageTemplate = err.Error() + "\n\n" + mentionMessage
}
if b.config.WebHost != "" {
defaultWebCommand := webCommand
if !b.config.DefaultWeb {
defaultWebCommand = noWebCommand
}
messageTemplate += " " + fmt.Sprintf(webMessage, defaultWebCommand)
}
if b.config.ShareEnabled() {
messageTemplate += "\n\n" + shareMessage
scripts = append(scripts, shareCommand)
Expand Down Expand Up @@ -413,24 +427,25 @@ func (b *Bot) webHandler(w http.ResponseWriter, r *http.Request) {

func (b *Bot) webHandlerInternal(w http.ResponseWriter, r *http.Request) error {
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 3 { // must be /prefix/, not just /prefix
if len(parts) < 2 {
return errors.New("invalid prefix")
}
prefix := parts[1]
b.mu.RLock()
var webPort int
session := b.webPrefix[prefix]
if session != nil {
webPort = session.webPort
}
b.mu.RUnlock()
if session == nil {
if session == nil || webPort == 0 {
return errors.New("session not found")
}
b.mu.RLock()
webPort := session.webPort
if webPort == 0 {
return errors.New("no active web session")
if len(parts) < 3 { // must be /prefix/, not just /prefix
http.Redirect(w, r, r.URL.String()+"/", http.StatusTemporaryRedirect)
return nil
}
b.mu.RUnlock()
proxy := &httputil.ReverseProxy{Director: func(r *http.Request) {
log.Printf("[%s] proxying web request: %s", session.conf.id, r.URL.Path)
r.URL.Scheme = "http"
r.URL.Host = fmt.Sprintf("127.0.0.1:%d", webPort)
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/"+prefix)
Expand Down
28 changes: 17 additions & 11 deletions bot/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ const (
cannotAddOwnerToDenyList = "🙁 I don't think adding the session owner to the deny list is a good idea. I must protest."
recordingTooLargeMessage = "🙁 I'm sorry, but you've produced too much output in this session. You may want to run a session with `norecord` to avoid this problem."
shareStartCommandMessage = "To start your terminal sharing session, please run the following command from your terminal:\n\n```bash -c \"$(ssh -T -p %s %s@%s $USER)\"```"
sessionWithWebStartReadOnlyMessage = "You can also view the session via http://%s/%s/. Use `!web rw` to switch the web terminal to read-write mode, or `!web off` to turn if off."
sessionWithWebStartReadWriteMessage = "You can also view *and control* the session via http://%s/%s/. Use `!web ro` to switch the web terminal to read-only mode, or `!web off` to turn if off."
sessionWithWebStartReadOnlyMessage = "You can also view the session via http://%s/%s. Use `!web rw` to switch the web terminal to read-write mode, or `!web off` to turn if off."
sessionWithWebStartReadWriteMessage = "You can also view *and control* the session via http://%s/%s. Use `!web ro` to switch the web terminal to read-only mode, or `!web off` to turn if off."
allowCommandHelpMessage = "To allow other users to interact with this session, use the `!allow` command like so: !allow %s\n\nYou may tag multiple users, or use the words " +
"`everyone`/`all` to allow all users, or `nobody`/`only-me` to only yourself access."
denyCommandHelpMessage = "To deny users from interacting with this session, use the `!deny` command like so: !deny %s\n\nYou may tag multiple users, or use the words " +
Expand All @@ -64,10 +64,11 @@ const (
webStoppedMessage = "👍 Okay, I stopped the web terminal."
webIsReadOnlyMessage = "The terminal is *read-only*. Use `!web rw` to change it to read-write, and `!web off` to turn if off completely."
webIsWritableMessage = "*Everyone in this channel* can write to this terminal. Use `!web ro` to change it to read-only, and `!web off` to turn if off completely."
webEnabledMessage = "The web terminal is available at http://%s/%s/"
webEnabledMessage = "The web terminal is available at http://%s/%s"
webDisabledMessage = "The web terminal is disabled."
webHelpMessage = "To enable it, simply type `!web rw` (read-write) or `!web ro` (read-only). Type `!web off` to turn if back off."
webNotWorkingMessage = "🙁 I'm sorry but I can't start the web terminal for you."
webNotWorkingMessage = "🙁 I'm sorry, but I can't start the web terminal for you."
webNotSupportedMessage = "🙁 I'm sorry, but the web terminal feature is not enabled."
helpMessage = "Alright, buckle up. Here's a list of all the things you can do in this REPL session.\n\n" +
"Sending text:\n" +
" `TEXT` - Sends _TEXT\\n_\n" +
Expand Down Expand Up @@ -495,9 +496,6 @@ func (s *session) shutdownHandler() error {
if s.shareConn != nil {
s.shareConn.Close()
}
if s.webCmd != nil {
s.conf.notifyWeb(s, false, s.webPrefix)
}
s.mu.Unlock()
return nil
}
Expand Down Expand Up @@ -875,6 +873,9 @@ func (s *session) handleScreenCommand(_ string) error {
}

func (s *session) handleWebCommand(input string) error {
if s.conf.global.WebHost == "" {
return s.conn.Send(s.conf.control, webNotSupportedMessage)
}
toggle := strings.TrimSpace(strings.TrimPrefix(input, "!web"))
s.mu.RLock()
enabled := s.webCmd != nil
Expand Down Expand Up @@ -934,11 +935,16 @@ func (s *session) startWeb(permitWrite bool) error {
s.webPrefix = util.RandomString(10)
}
s.webWritable = permitWrite
if permitWrite {
s.webCmd = exec.Command("gotty", "--permit-write", "--address", "127.0.0.1", "--port", strconv.Itoa(s.webPort), "tmux", "attach", "-t", s.tmux.MainID())
} else {
s.webCmd = exec.Command("gotty", "--address", "localhost", "--port", strconv.Itoa(s.webPort), "tmux", "attach", "-t", s.tmux.MainID())
args := []string{
"--address", "127.0.0.1",
"--port", strconv.Itoa(s.webPort),
"--reconnect",
"tmux", "attach", "-t", s.tmux.MainID(),
}
if s.webWritable {
args = append([]string{"--permit-write"}, args...)
}
s.webCmd = exec.Command("gotty", args...)
if err := s.webCmd.Start(); err != nil {
s.webCmd = nil // Disable web!
return err
Expand Down
10 changes: 10 additions & 0 deletions config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ bot-token: MUST_BE_SET
#
# web-address:

# Start web terminal by default (only applicable if "web-address" is set). If turned on, a gotty web terminal
# will be started by default for each session. This option defines the default behavior. It can be changed
# using the "web" or "noweb" settings.
#
# Format: true or false
# Default: false
# Required: No
#
# default-web: false

# Timeout after which REPL sessions are terminated if there is no user input.
#
# Format: <number>(hms), must be >1m
Expand Down
9 changes: 7 additions & 2 deletions util/tmux.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@ func CheckTmuxVersion() error {
return nil
}

// Tmux represents a tmux(1) process with one window and three panes, to allow us to resize the terminal of the
// main pane (.2). The main pane is .2, so that if it exits there is no other pane to take its place.
// Tmux represents a very special tmux(1) setup, specifially used for REPLbot. It consists of
// two tmux sessions:
// - session "replbot_$id_frame": session with one window and three panes to allow us to resize the terminal of the
// main pane (.2). The main pane is .2, so that if it exits there is no other pane to take its place, which is easily
// detectable by the other panes. The main pane (.2) connects to the main session (see below).
// - session "replbot_$id_main": main session running the actual shell/REPL.
type Tmux struct {
id string
width, height int
Expand Down Expand Up @@ -179,6 +183,7 @@ func (s *Tmux) Stop() error {
return nil
}

// MainID returns the session identifier for the main tmux session
func (s *Tmux) MainID() string {
return s.mainID()
}
Expand Down

0 comments on commit 7fb5e45

Please sign in to comment.