feat: «Здесь» from a machine's file pane opens the terminal in that pane's folder (the agent starts the shell there; an older agent, or a folder gone, at home)
This commit is contained in:
1 parent
4fb26a9652
commit
57c2f1338f
10 files changed
+143
-12
No files matched your search
@@ -23,7 +23,8 @@ import (
|
||||
|
||||
// POST /api/panel/term/{agentID}/open - create a terminal session: a shell
|
||||
// on the agent, or with {"ssh": {...}} in the body a shell the agent opens
|
||||
// by SSH on another host.
|
||||
// by SSH on another host. {"dir": "/path"} starts the shell in that folder
|
||||
// (an older agent, or a folder not there: at home).
|
||||
func (s *Server) handlePanelOpen(w http.ResponseWriter, r *http.Request) {
|
||||
agentID := r.PathValue("agentID")
|
||||
agent, err := s.db.GetAgent(agentID)
|
||||
@@ -34,6 +35,7 @@ func (s *Server) handlePanelOpen(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var body struct {
|
||||
SSH *proto.SSHTarget `json:"ssh"`
|
||||
Dir string `json:"dir"`
|
||||
}
|
||||
if r.ContentLength != 0 {
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&body); err != nil && err != io.EOF {
|
||||
@@ -63,7 +65,11 @@ func (s *Server) handlePanelOpen(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, map[string]string{"session_id": sess.ID})
|
||||
return
|
||||
}
|
||||
sess := s.hub.CreateSession(agentID, agent.LoginUser)
|
||||
dir := body.Dir
|
||||
if !strings.HasPrefix(dir, "/") || strings.ContainsAny(dir, "\x00\r\n") || len(dir) > 4096 {
|
||||
dir = ""
|
||||
}
|
||||
sess := s.hub.CreateSession(agentID, agent.LoginUser, dir)
|
||||
if admin := s.currentAdmin(r); admin != nil {
|
||||
s.db.Audit(admin.ID, agentID, "open_terminal:"+agent.TerminalUser())
|
||||
}
|
||||
|
||||
@@ -94,9 +94,9 @@ func (h *Hub) newSession(agentID string) *Session {
|
||||
|
||||
// CreateSession registers a terminal session and queues it for the agent. user
|
||||
// is the OS user to run the shell as ("" = the agent's own user).
|
||||
func (h *Hub) CreateSession(agentID, user string) *Session {
|
||||
func (h *Hub) CreateSession(agentID, user, dir string) *Session {
|
||||
s := h.newSession(agentID)
|
||||
h.enqueueJob(agentID, proto.ControlMessage{Type: proto.CtrlOpenTerminal, SessionID: s.ID, User: user})
|
||||
h.enqueueJob(agentID, proto.ControlMessage{Type: proto.CtrlOpenTerminal, SessionID: s.ID, User: user, Dir: dir})
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A terminal opened with a folder asks the agent to start there; a folder
|
||||
// that is not an absolute path is left out (the shell starts at home).
|
||||
func TestPanelOpenDir(t *testing.T) {
|
||||
s, cookie := newTestServer(t)
|
||||
for body, want := range map[string]string{
|
||||
`{"dir":"/srv/my app"}`: "/srv/my app",
|
||||
`{"dir":"relative"}`: "",
|
||||
``: "",
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/panel/term/x1/open", strings.NewReader(body))
|
||||
req.AddCookie(cookie)
|
||||
w := httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(w, req)
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("%s: code %d %s", body, w.Code, w.Body)
|
||||
}
|
||||
s.hub.mu.Lock()
|
||||
jobs := s.hub.jobs["x1"]
|
||||
s.hub.jobs["x1"] = nil
|
||||
s.hub.mu.Unlock()
|
||||
if len(jobs) != 1 || jobs[0].Dir != want {
|
||||
t.Fatalf("%s: jobs %+v, want dir %q", body, jobs, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,9 +262,11 @@ window.TermKeys = (function () {
|
||||
class Session {
|
||||
// ssh, when given, is where the agent connects by SSH for this shell:
|
||||
// {host, port, user} and the secret to sign in with, sent once on open.
|
||||
constructor(agent, ssh) {
|
||||
// dir, for a shell on the machine, is the folder it starts in.
|
||||
constructor(agent, ssh, dir) {
|
||||
this.agent = agent;
|
||||
this.ssh = ssh || null;
|
||||
this.dir = dir || '';
|
||||
this.kind = ssh ? 'ssh' : 'term';
|
||||
// Where its files are, for dropped paths: the machine, or the SSH host
|
||||
// through it.
|
||||
@@ -310,6 +312,7 @@ window.TermKeys = (function () {
|
||||
async open() {
|
||||
try {
|
||||
let opt = { method: 'POST' };
|
||||
if (this.dir) opt = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dir: this.dir }) };
|
||||
if (this.ssh) {
|
||||
opt = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ssh: this.ssh }) };
|
||||
// The password or key goes once, to the agent; the page keeps no copy.
|
||||
@@ -461,7 +464,7 @@ window.TermKeys = (function () {
|
||||
}
|
||||
|
||||
|
||||
W.paneTypes.term = agent => new Session(agent);
|
||||
W.paneTypes.term = (agent, opts) => new Session(agent, null, opts.dir);
|
||||
W.paneTypes.ssh = (agent, opts) => new Session(agent, opts.ssh);
|
||||
// For the touch helpers: the terminal being typed in.
|
||||
window.TermPane = {active: () => { const p = W.activePane(); return p && p.term ? p : null; }};
|
||||
|
||||
@@ -565,7 +565,12 @@
|
||||
list.appendChild(inner);
|
||||
g.after(list);
|
||||
if (here) item(content, '<span class="ico">+</span><span><b>Здесь</b><small>' + esc(here.name) + ' · ' + esc(here.user + '@' + here.host) + '</small></span>',
|
||||
() => { closeMenu(); openPane(kind, here); });
|
||||
() => {
|
||||
closeMenu();
|
||||
// A terminal from a machine's file pane starts in its folder.
|
||||
const dir = kind === 'term' && activePane.kind === 'files' ? activePane.cwd : '';
|
||||
openPane(kind, here, dir ? {dir} : null);
|
||||
});
|
||||
const box = document.createElement('div');
|
||||
box.innerHTML = '<div class="muted" style="padding:8px 10px;font-size:13px">загрузка…</div>';
|
||||
content.appendChild(box);
|
||||
|
||||
Reference in new issue
Block a user