From 2134d5d4c26dd73a19736182fdb1a91e1b6bc77f Mon Sep 17 00:00:00 2001 From: Subhra Das Gupta Date: Sat, 30 Mar 2024 19:31:00 +0530 Subject: [PATCH] . --- app/notes.txt | 21 + .../java/muon/dto/session/SessionInfo.java | 8 + .../muon/misc/ExtendedRemoteDirectory.java | 6 +- .../FolderViewTableCellRenderer.java | 2 +- .../filebrowser/sftp/SftpOperations.java | 35 +- .../sftp/archiving/ArchiveOperations.java | 17 +- .../foreground/ForegroundTransferPanel.java | 12 +- .../pages/process/ProcessViewerPage.java | 3 +- .../toolbox/pages/service/ServicePanel.java | 3 +- .../muon/screens/sessionmgr/SshInfoPanel.java | 17 + .../service/MultiplexingSessionService.java | 18 +- .../java/muon/service/SessionInstance.java | 110 ++++- .../main/java/muon/service/SftpSession.java | 6 +- app/src/main/java/muon/ssh/PacketReader.java | 107 +++++ .../main/java/muon/ssh/RemoteDirectory.java | 70 +++ app/src/main/java/muon/ssh/RemoteFile.java | 397 +++++++++++++++++ .../main/java/muon/ssh/RemoteResource.java | 65 +++ app/src/main/java/muon/ssh/SFTPClient.java | 241 ++++++++++ app/src/main/java/muon/ssh/SFTPEngine.java | 412 ++++++++++++++++++ .../main/java/muon/ssh/SubsystemWrapper.java | 129 ++++++ .../muon/util/FixedBufferInputStream.java | 3 +- .../muon/util/FixedBufferOutputStream.java | 4 +- app/src/main/java/muon/util/SudoUtils.java | 10 +- 23 files changed, 1635 insertions(+), 61 deletions(-) create mode 100644 app/notes.txt create mode 100644 app/src/main/java/muon/ssh/PacketReader.java create mode 100644 app/src/main/java/muon/ssh/RemoteDirectory.java create mode 100644 app/src/main/java/muon/ssh/RemoteFile.java create mode 100644 app/src/main/java/muon/ssh/RemoteResource.java create mode 100644 app/src/main/java/muon/ssh/SFTPClient.java create mode 100644 app/src/main/java/muon/ssh/SFTPEngine.java create mode 100644 app/src/main/java/muon/ssh/SubsystemWrapper.java diff --git a/app/notes.txt b/app/notes.txt new file mode 100644 index 00000000..816670c5 --- /dev/null +++ b/app/notes.txt @@ -0,0 +1,21 @@ + * sftp-server(8): add a "users-groups-by-id@openssh.com" extension + request that allows the client to obtain user/group names that + correspond to a set of uids/gids. + + * sftp(1): use "users-groups-by-id@openssh.com" sftp-server + extension (when available) to fill in user/group names for + directory listings. + + * sftp-server(8): support the "home-directory" extension request + defined in draft-ietf-secsh-filexfer-extensions-00. This overlaps + a bit with the existing "expand-path@openssh.com", but some other + clients support it. + + /usr/libexec/sftp-server + "expand-path@openssh.com" + https://bugzilla.mindrot.org/show_bug.cgi?id=2948 + limits@openssh.com + lsetstat@openssh.com + https://www.openssh.com/releasenotes.html + https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-extensions-00 + https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02 \ No newline at end of file diff --git a/app/src/main/java/muon/dto/session/SessionInfo.java b/app/src/main/java/muon/dto/session/SessionInfo.java index 9e722b4b..18d2da07 100644 --- a/app/src/main/java/muon/dto/session/SessionInfo.java +++ b/app/src/main/java/muon/dto/session/SessionInfo.java @@ -26,6 +26,7 @@ public class SessionInfo extends NamedItem { private transient String osName; private transient String osType; private transient Boolean shellAccess; + private transient boolean loginAsSudo; public int getAuthMode() { return authMode; @@ -356,4 +357,11 @@ public void setShellAccess(Boolean shellAccess) { this.shellAccess = shellAccess; } + public boolean isLoginAsSudo() { + return loginAsSudo; + } + + public void setLoginAsSudo(boolean loginAsSudo) { + this.loginAsSudo = loginAsSudo; + } } diff --git a/app/src/main/java/muon/misc/ExtendedRemoteDirectory.java b/app/src/main/java/muon/misc/ExtendedRemoteDirectory.java index 016b8833..00356859 100644 --- a/app/src/main/java/muon/misc/ExtendedRemoteDirectory.java +++ b/app/src/main/java/muon/misc/ExtendedRemoteDirectory.java @@ -3,8 +3,10 @@ */ package muon.misc; + +import muon.ssh.RemoteDirectory; +import muon.ssh.SFTPEngine; import net.schmizz.sshj.sftp.*; -import net.schmizz.sshj.sftp.Response.StatusCode; import java.io.IOException; import java.util.ArrayList; @@ -59,7 +61,7 @@ public List scanExtended( break; case STATUS: - res.ensureStatusIs(StatusCode.EOF); + res.ensureStatusIs(Response.StatusCode.EOF); break loop; default: diff --git a/app/src/main/java/muon/screens/appwin/filebrowser/FolderViewTableCellRenderer.java b/app/src/main/java/muon/screens/appwin/filebrowser/FolderViewTableCellRenderer.java index 66630c08..5bf86c9b 100644 --- a/app/src/main/java/muon/screens/appwin/filebrowser/FolderViewTableCellRenderer.java +++ b/app/src/main/java/muon/screens/appwin/filebrowser/FolderViewTableCellRenderer.java @@ -139,6 +139,6 @@ public Component getTableCellRendererComponent(JTable table, public int calculateRowHeight() { iconLbl.setIcon(folderIcon); label1.setText("some text"); - return p1.getPreferredSize().height; + return Math.max(p1.getPreferredSize().height, 30); } } diff --git a/app/src/main/java/muon/screens/appwin/filebrowser/sftp/SftpOperations.java b/app/src/main/java/muon/screens/appwin/filebrowser/sftp/SftpOperations.java index b81409c7..e2f5efbd 100644 --- a/app/src/main/java/muon/screens/appwin/filebrowser/sftp/SftpOperations.java +++ b/app/src/main/java/muon/screens/appwin/filebrowser/sftp/SftpOperations.java @@ -80,7 +80,7 @@ public void newOrRename(Action action) { if (retryWithSudo.get()) { var ret = SudoUtils.exec(fileBrowserView.getSessionInfo(), String.format("mv -f '%s' '%s'", fileBrowserView.getSelectedFiles().get(0).getPath(), - PathUtils.combineUnix(fileBrowserView.getPath(), finalFileName)), AppUtils::getPassword); + PathUtils.combineUnix(fileBrowserView.getPath(), finalFileName)), AppUtils::getPassword, cancellationToken); if (ret != 0) { throw new Exception("Operation failed"); } @@ -92,7 +92,7 @@ public void newOrRename(Action action) { case NewFile: if (retryWithSudo.get()) { var ret = SudoUtils.exec(fileBrowserView.getSessionInfo(), String.format("touch '%s'", - PathUtils.combineUnix(fileBrowserView.getPath(), finalFileName)), AppUtils::getPassword); + PathUtils.combineUnix(fileBrowserView.getPath(), finalFileName)), AppUtils::getPassword, cancellationToken); if (ret != 0) { throw new Exception("Operation failed"); } @@ -104,7 +104,7 @@ public void newOrRename(Action action) { case NewFolder: if (retryWithSudo.get()) { var ret = SudoUtils.exec(fileBrowserView.getSessionInfo(), String.format("mkdir '%s'", - PathUtils.combineUnix(fileBrowserView.getPath(), finalFileName)), AppUtils::getPassword); + PathUtils.combineUnix(fileBrowserView.getPath(), finalFileName)), AppUtils::getPassword, cancellationToken); if (ret != 0) { throw new Exception("Operation failed"); } @@ -119,14 +119,6 @@ public void newOrRename(Action action) { }, retryWithSudo); } - public void sameOriginCopy() { - var selected = fileBrowserView.getSelectedFiles(); - if (selected.size() == 0) { - return; - } - sameOriginCopy(selected, fileBrowserView.getPath(), false); - } - public void sameOriginCopy(List files, String folder, boolean overwrite) { var fileSet = fileBrowserView.getAllFiles().stream().map(FileInfo::getName).collect(Collectors.toSet()); var args = new ArrayList(); @@ -161,14 +153,14 @@ public void sameOriginCopy(List files, String folder, boolean overwrit } if (retryWithSudo.get()) { var ret = SudoUtils.exec(fileBrowserView.getSessionInfo(), - String.format("sh -c \"%s\"", command), AppUtils::getPassword); + String.format("sh -c \"%s\"", command), AppUtils::getPassword, cancellationToken); if (ret != 0) { throw new Exception("Operation failed"); } } else { if (App.getMultiplexingSessionService().executeCommand( this.fileBrowserView.getSession().getSessionInfo(), - command) != 0) { + command, cancellationToken) != 0) { throw new FSAccessException("Operation failed"); } } @@ -222,7 +214,7 @@ public void delete() { .collect(Collectors.joining(" "))); if (retryWithSudo.get()) { var ret = SudoUtils.exec(this.fileBrowserView.getSession().getSessionInfo(), - str, AppUtils::getPassword); + str, AppUtils::getPassword, cancellationToken); if (ret != 0) { if (ret == -2) { throw new Exception("Operation cancelled"); @@ -231,7 +223,7 @@ public void delete() { } return; } - if (App.getMultiplexingSessionService().executeCommand(this.fileBrowserView.getSession().getSessionInfo(), str) == 0) { + if (App.getMultiplexingSessionService().executeCommand(this.fileBrowserView.getSession().getSessionInfo(), str, cancellationToken) == 0) { System.out.println("Shell delete success!"); return; } @@ -273,18 +265,16 @@ public void delete() { } public void compress() { - var stopFlag = new AtomicBoolean(false); var selected = fileBrowserView.getSelectedFiles(); var arcOps = new ArchiveOperations(); - arcOps.createArchive(fileBrowserView.getSessionInfo(), selected.stream().map(FileInfo::getPath).toList(), fileBrowserView.getPath(), stopFlag); + arcOps.createArchive(fileBrowserView.getSessionInfo(), selected.stream().map(FileInfo::getPath).toList(), fileBrowserView.getPath()); } public void extract() { - var stopFlag = new AtomicBoolean(false); var selected = fileBrowserView.getSelectedFiles(); var arcOps = new ArchiveOperations(); - arcOps.extractArchive(fileBrowserView.getSessionInfo(), selected.get(0).getPath(), fileBrowserView.getPath(), stopFlag); + arcOps.extractArchive(fileBrowserView.getSessionInfo(), selected.get(0).getPath(), fileBrowserView.getPath()); } private void traverse(String path, ArrayList files, ArrayList folders, CancellationToken cancellationToken) @@ -296,6 +286,9 @@ private void traverse(String path, ArrayList files, ArrayList fo } for (var folder : fileList.getFolders()) { + if (cancellationToken.isCancellationRequested()) { + return; + } if (folder.getFileType() == FileType.Directory) { traverse(folder.getPath(), files, folders, cancellationToken); } else { @@ -432,12 +425,12 @@ public void copy(String targetFolder, List sourceFiles) { var str = String.format("cp -rf %s '%s'", String.join(" ", files), targetFolder); System.out.println(str); if (retryWithSudo.get()) { - var ret = SudoUtils.exec(fileBrowserView.getSessionInfo(), str, AppUtils::getPassword); + var ret = SudoUtils.exec(fileBrowserView.getSessionInfo(), str, AppUtils::getPassword, cancellationToken); if (ret != 0) { throw new Exception("Operation failed"); } } else { - if (App.getMultiplexingSessionService().executeCommand(this.fileBrowserView.getSession().getSessionInfo(), str) != 0) { + if (App.getMultiplexingSessionService().executeCommand(this.fileBrowserView.getSession().getSessionInfo(), str, cancellationToken) != 0) { throw new FSAccessException("Operation failed"); } } diff --git a/app/src/main/java/muon/screens/appwin/filebrowser/sftp/archiving/ArchiveOperations.java b/app/src/main/java/muon/screens/appwin/filebrowser/sftp/archiving/ArchiveOperations.java index e581ce0c..60cd4321 100644 --- a/app/src/main/java/muon/screens/appwin/filebrowser/sftp/archiving/ArchiveOperations.java +++ b/app/src/main/java/muon/screens/appwin/filebrowser/sftp/archiving/ArchiveOperations.java @@ -2,6 +2,7 @@ import muon.App; import muon.dto.session.SessionInfo; +import muon.misc.CancellationToken; import muon.util.AppUtils; import muon.util.PathUtils; @@ -89,8 +90,8 @@ private String getArchiveFileName(String archivePath) { public void extractArchive( SessionInfo sessionInfo, String archivePath, - String targetFolder, - AtomicBoolean stopFlag) { + String targetFolder) { + var cancellationToken = new CancellationToken(); String command = getExtractCommand(archivePath); if (command == null) { System.out.println("Unsupported file: " + archivePath); @@ -105,14 +106,14 @@ public void extractArchive( System.out.println("Invoke command: " + command); String finalCommand = command; App.getAppWindow().blockInput(true, "Please wait...", "", e -> { - stopFlag.set(true); + cancellationToken.requestCancellation(); }); AppUtils.runAsync(() -> { try { var ret = App.getMultiplexingSessionService().executeCommand( sessionInfo, finalCommand, - stopFlag); + cancellationToken); System.out.println("output: " + ret); if (ret != 0) { AppUtils.swingInvokeSync(() -> JOptionPane.showMessageDialog(null, "Operation failed")); @@ -128,8 +129,8 @@ public void extractArchive( public void createArchive( SessionInfo sessionInfo, List files, - String targetFolder, - AtomicBoolean stopFlag) { + String targetFolder) { + var cancellationToken = new CancellationToken(); String text = files.size() > 1 ? PathUtils.getFileName(targetFolder) : files.get(0); JTextField txtFileName = new JTextField(text); @@ -156,12 +157,12 @@ public void createArchive( String cd = String.format("cd \"%s\";", txtTargetFolder.getText()); System.out.println(cd + compressCmd); App.getAppWindow().blockInput(true, "Please wait...", "", e -> { - stopFlag.set(true); + cancellationToken.requestCancellation(); }); AppUtils.runAsync(() -> { try { var ret = App.getMultiplexingSessionService().executeCommand( - sessionInfo, cd + compressCmd, stopFlag); + sessionInfo, cd + compressCmd, cancellationToken); System.out.println("output: " + ret); if (ret != 0) { AppUtils.swingInvokeSync(() -> JOptionPane.showMessageDialog(null, "Operation failed")); diff --git a/app/src/main/java/muon/screens/appwin/filebrowser/transfer/foreground/ForegroundTransferPanel.java b/app/src/main/java/muon/screens/appwin/filebrowser/transfer/foreground/ForegroundTransferPanel.java index affddc4f..1ca18b64 100644 --- a/app/src/main/java/muon/screens/appwin/filebrowser/transfer/foreground/ForegroundTransferPanel.java +++ b/app/src/main/java/muon/screens/appwin/filebrowser/transfer/foreground/ForegroundTransferPanel.java @@ -1,6 +1,7 @@ package muon.screens.appwin.filebrowser.transfer.foreground; import muon.exceptions.FSAccessException; +import muon.misc.CancellationToken; import muon.service.SftpUploadTask; import muon.service.TransferTask; import muon.util.AppUtils; @@ -20,6 +21,7 @@ public class ForegroundTransferPanel extends JPanel { private Consumer callback; private TransferProgressPanel transferProgressPanel; private TransferRetryPanel transferRetryPanel; + private CancellationToken cancellationToken; public ForegroundTransferPanel() { super(new CardLayout()); @@ -34,7 +36,12 @@ public void submitTask(TransferTask transferTask, public void showProgress() { if (transferProgressPanel == null) { - transferProgressPanel = new TransferProgressPanel(e -> this.transferTask.requestCancellation()); + transferProgressPanel = new TransferProgressPanel(e -> { + this.transferTask.requestCancellation(); + if (Objects.nonNull(this.cancellationToken)) { + this.cancellationToken.requestCancellation(); + } + }); this.add(transferProgressPanel, "TransferProgressPanel"); } transferProgressPanel.setProgress(0); @@ -79,6 +86,7 @@ public void transfer() { AppUtils.runAsync(() -> { var retry = new AtomicBoolean(false); do { + cancellationToken = new CancellationToken(); try { transferFiles(); if (retry.get() && this.transferTask instanceof SftpUploadTask && !StringUtils.isEmpty(((SftpUploadTask) this.transferTask).getActualTargetPath())) { @@ -117,7 +125,7 @@ private void copyUsingSudo(String source, String target) throws Exception { transferProgressPanel.setStatus("Running shell command..."); }); var ret = SudoUtils.exec(((SftpUploadTask) this.transferTask).getSftpProvider().getSessionInfo(), - command, out, err, AppUtils::getPassword); + command, out, err, AppUtils::getPassword, cancellationToken); if (ret != 0) { if (ret == -2) { throw new Exception("Operation cancelled"); diff --git a/app/src/main/java/muon/screens/appwin/toolbox/pages/process/ProcessViewerPage.java b/app/src/main/java/muon/screens/appwin/toolbox/pages/process/ProcessViewerPage.java index d1eb46b4..f3297741 100644 --- a/app/src/main/java/muon/screens/appwin/toolbox/pages/process/ProcessViewerPage.java +++ b/app/src/main/java/muon/screens/appwin/toolbox/pages/process/ProcessViewerPage.java @@ -7,6 +7,7 @@ import muon.dto.session.SessionInfo; import muon.exceptions.FSAccessException; import muon.exceptions.FSConnectException; +import muon.misc.CancellationToken; import muon.screens.appwin.toolbox.PageContainer; import muon.util.AppUtils; import muon.util.ScriptLoader; @@ -105,7 +106,7 @@ private void kill(ActionEvent actionEvent) { try { var out = new ByteArrayOutputStream(); var err = new ByteArrayOutputStream(); - var ret = SudoUtils.exec(sessionInfo, "kill -9 " + pid, out, err, this.pageContainer::getPassword); + var ret = SudoUtils.exec(sessionInfo, "kill -9 " + pid, out, err, this.pageContainer::getPassword, new CancellationToken()); if (ret != 0) { //Notify sudo failed if (ret == -2) { diff --git a/app/src/main/java/muon/screens/appwin/toolbox/pages/service/ServicePanel.java b/app/src/main/java/muon/screens/appwin/toolbox/pages/service/ServicePanel.java index 8ed2c482..1ce25d17 100644 --- a/app/src/main/java/muon/screens/appwin/toolbox/pages/service/ServicePanel.java +++ b/app/src/main/java/muon/screens/appwin/toolbox/pages/service/ServicePanel.java @@ -5,6 +5,7 @@ import muon.App; import muon.dto.session.SessionInfo; +import muon.misc.CancellationToken; import muon.screens.appwin.toolbox.PageContainer; import muon.util.AppUtils; import muon.util.SudoUtils; @@ -411,7 +412,7 @@ private void performServiceAction(int option) { public boolean runCommandWithSudo(String command) throws Exception { var out = new ByteArrayOutputStream(); var err = new ByteArrayOutputStream(); - return SudoUtils.exec(this.sessionInfo, command, out, err, this.pageContainer::getPassword) == 0; + return SudoUtils.exec(this.sessionInfo, command, out, err, this.pageContainer::getPassword, new CancellationToken()) == 0; } public boolean runCommand(String command) throws Exception { diff --git a/app/src/main/java/muon/screens/sessionmgr/SshInfoPanel.java b/app/src/main/java/muon/screens/sessionmgr/SshInfoPanel.java index f6c22a5c..5f05a67b 100644 --- a/app/src/main/java/muon/screens/sessionmgr/SshInfoPanel.java +++ b/app/src/main/java/muon/screens/sessionmgr/SshInfoPanel.java @@ -25,6 +25,7 @@ public class SshInfoPanel extends JPanel { private SessionInfo sessionInfo; private JLabel lblPassword, lblKeyFile, lblIdentity, lblUserName; private JButton btnBrowseKey, btnEditIdentities; + private JCheckBox chkUseSudo; public SshInfoPanel() { super(new GridBagLayout()); @@ -46,6 +47,7 @@ public SshInfoPanel() { txtLocalFolder = new JTextField(); cmbStartPage = new JComboBox<>(new String[]{"SFTP+Terminal", "SFTP", "Terminal", "Port forwarding"}); swCombinedMode = new SwitchButton(); + chkUseSudo = new JCheckBox("Login with sudo privilege"); cmbStartPage.setFont(cmbFont); txtPass.setEchoChar('*'); @@ -73,6 +75,9 @@ public SshInfoPanel() { attachTextListener(txtKeyFile, text -> sessionInfo.setPrivateKeyFile(text)); attachTextListener(txtRemoteFolder, text -> sessionInfo.setRemoteFolder(text)); attachTextListener(txtLocalFolder, text -> sessionInfo.setLocalFolder(text)); + chkUseSudo.addChangeListener(e -> { + sessionInfo.setLoginAsSudo(chkUseSudo.isSelected()); + }); attachPasswordListener(txtPass, password -> { sessionInfo.setPassword(new String(password)); @@ -104,6 +109,7 @@ public void setValue(SessionInfo info) { txtLocalFolder.setText(info.getLocalFolder()); cmbAuthMethod.setSelectedIndex(info.getAuthMode()); txtPass.setText(info.getPassword()); + chkUseSudo.setSelected(false); } private void attachTextListener(JTextField txt, Consumer consumer) { @@ -371,6 +377,17 @@ private void createUI() { c++; + gc = new GridBagConstraints(); + gc.gridx = 1; + gc.gridy = c; + gc.fill = GridBagConstraints.HORIZONTAL; + gc.gridwidth = 2; + gc.weightx = 1; + gc.insets = insets; + this.add(chkUseSudo, gc); + + c++; + gc = new GridBagConstraints(); gc.gridx = 0; gc.gridy = c; diff --git a/app/src/main/java/muon/service/MultiplexingSessionService.java b/app/src/main/java/muon/service/MultiplexingSessionService.java index e9e3a138..58a12622 100644 --- a/app/src/main/java/muon/service/MultiplexingSessionService.java +++ b/app/src/main/java/muon/service/MultiplexingSessionService.java @@ -28,7 +28,7 @@ public synchronized int executeCommand(SessionInfo sessionInfo, String keyword, Supplier callback, boolean pty) throws FSConnectException { - return executeCommand(sessionInfo, command, out, err, keyword, callback, new AtomicBoolean(false), pty); + return executeCommand(sessionInfo, command, out, err, keyword, callback, new CancellationToken(), pty); } public synchronized int executeCommand(SessionInfo sessionInfo, @@ -37,14 +37,14 @@ public synchronized int executeCommand(SessionInfo sessionInfo, OutputStream err, String keyword, Supplier callback, - AtomicBoolean stopFlag, + CancellationToken cancellationToken, boolean pty) throws FSConnectException { var sessions = sessionMap.get(sessionInfo.getId()); if (Objects.isNull(sessions)) { sessions = new HashSet<>(); sessionMap.put(sessionInfo.getId(), sessions); } - return executeCommand(sessionInfo, command, out, err, keyword, callback, stopFlag, pty, sessions); + return executeCommand(sessionInfo, command, out, err, keyword, callback, cancellationToken, pty, sessions); } private synchronized int executeCommand(SessionInfo sessionInfo, @@ -53,13 +53,13 @@ private synchronized int executeCommand(SessionInfo sessionInfo, OutputStream err, String keyword, Supplier callback, - AtomicBoolean stopFlag, + CancellationToken cancellationToken, boolean pty, Set sessions) throws FSConnectException { for (var session : sessions) { if (session.isConnected() && !session.isExhausted()) { - var ret = session.executeCommand(command, out, err, keyword, callback, stopFlag, pty); + var ret = session.executeCommand(command, out, err, keyword, callback, cancellationToken, pty); if (Objects.nonNull(ret)) { return ret; } @@ -69,7 +69,7 @@ private synchronized int executeCommand(SessionInfo sessionInfo, try { session = new SessionInstance(sessionInfo, this::removeSession); session.connect(); - var ret = session.executeCommand(command, out, err, keyword, callback, stopFlag, pty); + var ret = session.executeCommand(command, out, err, keyword, callback, cancellationToken, pty); if (Objects.isNull(ret)) { throw new FSConnectException("Unable to create Exec channel"); } @@ -107,13 +107,13 @@ public synchronized int executeCommand( SessionInfo sessionInfo, String command ) throws FSConnectException { - return executeCommand(sessionInfo, command, new AtomicBoolean(false)); + return executeCommand(sessionInfo, command, new CancellationToken()); } public synchronized int executeCommand( SessionInfo sessionInfo, String command, - AtomicBoolean stopFlag + CancellationToken cancellationToken ) throws FSConnectException { var sessions = sessionMap.get(sessionInfo.getId()); if (Objects.isNull(sessions)) { @@ -124,7 +124,7 @@ public synchronized int executeCommand( sessionInfo, command, new ByteArrayOutputStream(), - new ByteArrayOutputStream(), null, null, stopFlag, false, sessions); + new ByteArrayOutputStream(), null, null, cancellationToken, false, sessions); } // private synchronized SessionInstance.ExecResult executeCommand(SessionInfo sessionInfo, String command, diff --git a/app/src/main/java/muon/service/SessionInstance.java b/app/src/main/java/muon/service/SessionInstance.java index 91877dbd..d9066ec6 100644 --- a/app/src/main/java/muon/service/SessionInstance.java +++ b/app/src/main/java/muon/service/SessionInstance.java @@ -3,6 +3,10 @@ import muon.AppContext; import muon.exceptions.FSConnectException; import muon.dto.session.SessionInfo; +import muon.misc.CancellationToken; +import muon.ssh.SFTPClient; +import muon.ssh.SFTPEngine; +import muon.ssh.SubsystemWrapper; import muon.util.AppUtils; import muon.util.ScriptLoader; import muon.util.StringUtils; @@ -125,6 +129,43 @@ private void copyChars(InputStream src, OutputStream dst, String keyword, Runnab } } + private boolean copyChars(InputStream src, OutputStream dst, String keyword1, String keyword2, Runnable callback, CountDownLatch cl) { + var b = new BufferedInputStream(src, 1); + var r = new InputStreamReader(b, StandardCharsets.UTF_8); + var w = new OutputStreamWriter(dst, StandardCharsets.UTF_8); + var success = false; + try { + var sb = new StringBuilder(); + while (true) { + var x = r.read(); + if (x == -1) { + System.out.println("Stream EOF"); + break; + } + w.write(x); + sb.append((char) x); + System.err.print((char) x); + if (Objects.nonNull(keyword1) && Objects.nonNull(callback)) { + if (sb.indexOf(keyword1) != -1) { + System.out.println("Sudo prompt matched"); + sb = new StringBuilder(); + AppUtils.runAsync(callback); + } + } + if (Objects.nonNull(keyword2) && sb.indexOf(keyword2) != -1) { + System.out.println("Second prompt matched"); + success = true; + break; + } + } + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + cl.countDown(); + } + return success; + } + private void copyBytes(InputStream src, OutputStream dst, CountDownLatch cl, int bufferSize) { try { byte[] b = new byte[bufferSize]; @@ -165,7 +206,7 @@ public Integer executeCommand( OutputStream err, String keyword, Supplier callback, - AtomicBoolean stopFlag, + CancellationToken cancellationToken, boolean pty) { AtomicBoolean shellInit = new AtomicBoolean(false); AtomicBoolean killed = new AtomicBoolean(false); @@ -204,7 +245,7 @@ public Integer executeCommand( AppUtils.runAsync(() -> { try { while (flag.get()) { - if (stopFlag.get()) { + if (cancellationToken.isCancellationRequested()) { killed.set(true); cmd.signal(Signal.KILL); } @@ -304,7 +345,10 @@ public synchronized SftpSession createSftpClient() { return null; } try { - var sftp = client.newSFTPClient(); + if (!(client.isAuthenticated() && client.isConnected())) { + throw new IllegalStateException("Not connected or authenticated"); + } + var sftp = sessionInfo.isLoginAsSudo() ? createSftpClientWithSudo() : new SFTPClient(client); sftp.getSFTPEngine().getSubsystem().setAutoExpand(true); var home = sftp.canonicalize(""); //attachListener(sftpClient.getClientChannel()); @@ -381,7 +425,7 @@ private void getServerDetails() { err, null, null, - new AtomicBoolean(false), + new CancellationToken(), false) == 0) { for (var line : out.toString(StandardCharsets.UTF_8).split("\n")) { System.out.println(line); @@ -461,4 +505,62 @@ private void closeImpl() { } connected.set(false); } + + private SFTPClient createSftpClientWithSudo() throws Exception { + var prompt = UUID.randomUUID().toString(); + String keyword = prompt; + var keyword2 = UUID.randomUUID().toString(); + var command = "sudo -S -p '" + prompt + "' /bin/sh -c 'echo " + keyword2 + " >&2 && /usr/libexec/sftp-server'"; + OutputStream out = new ByteArrayOutputStream(); + OutputStream err = new ByteArrayOutputStream(); + Supplier callback = () -> sessionInfo.getPassword(); + CancellationToken cancellationToken = new CancellationToken(); + boolean pty = false; + AtomicBoolean shellInit = new AtomicBoolean(false); + AtomicBoolean killed = new AtomicBoolean(false); + AtomicBoolean flag = new AtomicBoolean(true); + System.out.println("Executing: " + command); + try { + Session session = client.startSession(); + session.setAutoExpand(true); + if (pty) { + session.allocatePTY("vt100", 80, 24, 0, 0, Collections.emptyMap()); + } + Session.Command cmd = session.exec(command); + var _in = cmd.getInputStream(); + var _err = cmd.getErrorStream(); + var _out = cmd.getOutputStream(); + shellInit.set(true); + var cl = new CountDownLatch(1); + var success = new AtomicBoolean(false); + AppUtils.runAsync(() -> { + var result = copyChars(_err, err, keyword, keyword2, () -> { + try { + var input = callback.get(); + if (Objects.nonNull(input)) { + _out.write((input + "\n").getBytes(StandardCharsets.UTF_8)); + _out.flush(); + } else { + cmd.signal(Signal.KILL); + killed.set(true); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + }, cl); + success.set(result); + }); + cl.await(); + + if (success.get()) { + var engine = new SFTPEngine(new SubsystemWrapper(cmd), "/"); + engine.init(); + return new SFTPClient(engine); + } + throw new IOException("Sudo failed"); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } } diff --git a/app/src/main/java/muon/service/SftpSession.java b/app/src/main/java/muon/service/SftpSession.java index 2866cc22..43910842 100644 --- a/app/src/main/java/muon/service/SftpSession.java +++ b/app/src/main/java/muon/service/SftpSession.java @@ -29,14 +29,14 @@ public class SftpSession implements AutoCloseable { private SessionInfo sessionInfo; - private SFTPClient sftpClient; + private muon.ssh.SFTPClient sftpClient; private String homePath; private AtomicBoolean connected = new AtomicBoolean(false); private Runnable closeCallback; private static final int MAX_UNCONFIRMED_READ = 32; private static final int MAX_UNCONFIRMED_WRITE = 32; - public SftpSession(SessionInfo sessionInfo, SFTPClient sftpClient, Runnable closeCallback, String home) { + public SftpSession(SessionInfo sessionInfo, muon.ssh.SFTPClient sftpClient, Runnable closeCallback, String home) { this.sessionInfo = sessionInfo; this.sftpClient = sftpClient; this.closeCallback = closeCallback; @@ -49,7 +49,7 @@ public FileList list(String folder, CancellationToken cancellationToken) throws } private List ls(String path, CancellationToken cancellationToken) throws Exception { - final SFTPEngine requester = this.sftpClient.getSFTPEngine(); + final muon.ssh.SFTPEngine requester = this.sftpClient.getSFTPEngine(); final byte[] handle = requester .request(requester.newRequest(PacketType.OPENDIR).putString(path, requester.getSubsystem().getRemoteCharset())) diff --git a/app/src/main/java/muon/ssh/PacketReader.java b/app/src/main/java/muon/ssh/PacketReader.java new file mode 100644 index 00000000..8f2e7853 --- /dev/null +++ b/app/src/main/java/muon/ssh/PacketReader.java @@ -0,0 +1,107 @@ +package muon.ssh; + + + +import net.schmizz.concurrent.Promise; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.sftp.Response; +import net.schmizz.sshj.sftp.SFTPException; +import net.schmizz.sshj.sftp.SFTPPacket; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class PacketReader extends Thread { + + /** + * Logger + */ + private final Logger log; + + private final InputStream in; + private final Map> promises = new ConcurrentHashMap>(); + private final SFTPPacket packet = new SFTPPacket(); + private final byte[] lenBuf = new byte[4]; + private final SFTPEngine engine; + + public PacketReader(SFTPEngine engine) { + this.engine = engine; + log = engine.getLoggerFactory().getLogger(getClass()); + this.in = engine.getSubsystem().getInputStream(); + setName("sshj-PacketReader"); + setDaemon(true); + } + + private void readIntoBuffer(byte[] buf, int off, int len) + throws IOException { + int count = 0; + int read = 0; + while (count < len && ((read = in.read(buf, off + count, len - count)) != -1)) + count += read; + if (read == -1) + throw new SFTPException("EOF while reading packet"); + } + + private int getPacketLength() + throws IOException { + readIntoBuffer(lenBuf, 0, lenBuf.length); + + final long len = (lenBuf[0] << 24 & 0xff000000L + | lenBuf[1] << 16 & 0x00ff0000L + | lenBuf[2] << 8 & 0x0000ff00L + | lenBuf[3] & 0x000000ffL); + + if (len > SFTPPacket.MAX_SIZE) { + throw new SSHException(String.format("Indicated packet length %d too large", len)); + } + + return (int) len; + } + + public SFTPPacket readPacket() + throws IOException { + final int len = getPacketLength(); + packet.clear(); + packet.ensureCapacity(len); + readIntoBuffer(packet.array(), 0, len); + packet.wpos(len); + return packet; + } + + @Override + public void run() { + try { + while (!isInterrupted()) { + readPacket(); + handle(); + } + } catch (IOException e) { + for (Promise promise : promises.values()) + promise.deliverError(e); + } + } + + public void handle() + throws SFTPException { + Response resp = new Response(packet, engine.getOperativeProtocolVersion()); + Promise promise = promises.remove(resp.getRequestID()); + log.debug("Received {} packet", resp.getType()); + if (promise == null) + throw new SFTPException("Received [" + resp.readType() + "] response for request-id " + resp.getRequestID() + + ", no such request was made"); + else + promise.deliver(resp); + } + + public Promise expectResponseTo(long requestId) { + final Promise promise + = new Promise("sftp / " + requestId, SFTPException.chainer, engine.getLoggerFactory()); + promises.put(requestId, promise); + return promise; + } + +} + diff --git a/app/src/main/java/muon/ssh/RemoteDirectory.java b/app/src/main/java/muon/ssh/RemoteDirectory.java new file mode 100644 index 00000000..e5bcbbd7 --- /dev/null +++ b/app/src/main/java/muon/ssh/RemoteDirectory.java @@ -0,0 +1,70 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package muon.ssh; + +import net.schmizz.sshj.sftp.RemoteResourceFilter; +import net.schmizz.sshj.sftp.*; +import net.schmizz.sshj.sftp.Response; +import net.schmizz.sshj.sftp.Response.StatusCode; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class RemoteDirectory + extends RemoteResource { + + public RemoteDirectory(SFTPEngine requester, String path, byte[] handle) { + super(requester, path, handle); + } + + public List scan(RemoteResourceFilter filter) + throws IOException { + List rri = new LinkedList(); + // TODO: Remove GOTO! + loop: + for (; ; ) { + final Response res = requester.request(newRequest(PacketType.READDIR)) + .retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS); + switch (res.getType()) { + + case NAME: + final int count = res.readUInt32AsInt(); + for (int i = 0; i < count; i++) { + final String name = res.readString(requester.sub.getRemoteCharset()); + res.readString(); // long name - IGNORED - shdve never been in the protocol + final FileAttributes attrs = res.readFileAttributes(); + final PathComponents comps = requester.getPathHelper().getComponents(path, name); + final RemoteResourceInfo inf = new RemoteResourceInfo(comps, attrs); + if (!(".".equals(name) || "..".equals(name)) && (filter == null || filter.accept(inf))) { + rri.add(inf); + } + } + break; + + case STATUS: + res.ensureStatusIs(StatusCode.EOF); + break loop; + + default: + throw new SFTPException("Unexpected packet: " + res.getType()); + } + } + return rri; + } + +} diff --git a/app/src/main/java/muon/ssh/RemoteFile.java b/app/src/main/java/muon/ssh/RemoteFile.java new file mode 100644 index 00000000..ad94f706 --- /dev/null +++ b/app/src/main/java/muon/ssh/RemoteFile.java @@ -0,0 +1,397 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package muon.ssh; + +import net.schmizz.concurrent.Promise; +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.sftp.*; +import net.schmizz.sshj.sftp.Response.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.TimeUnit; + +public class RemoteFile + extends RemoteResource { + + public RemoteFile(SFTPEngine requester, String path, byte[] handle) { + super(requester, path, handle); + } + + public FileAttributes fetchAttributes() throws IOException { + return requester.request(newRequest(PacketType.FSTAT)) + .retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS) + .ensurePacketTypeIs(PacketType.ATTRS) + .readFileAttributes(); + } + + public long length() throws IOException { + return fetchAttributes().getSize(); + } + + public void setLength(long len) throws IOException { + setAttributes(new FileAttributes.Builder().withSize(len).build()); + } + + public int read(long fileOffset, byte[] to, int offset, int len) throws IOException { + final Response res = asyncRead(fileOffset, len).retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS); + return checkReadResponse(res, to, offset); + } + + protected Promise asyncRead(long fileOffset, int len) throws IOException { + return requester.request(newRequest(PacketType.READ).putUInt64(fileOffset).putUInt32(len)); + } + + protected int checkReadResponse(Response res, byte[] to, int offset) throws Buffer.BufferException, SFTPException { + switch (res.getType()) { + case DATA: + int recvLen = res.readUInt32AsInt(); + System.arraycopy(res.array(), res.rpos(), to, offset, recvLen); + return recvLen; + + case STATUS: + res.ensureStatusIs(StatusCode.EOF); + return -1; + + default: + throw new SFTPException("Unexpected packet: " + res.getType()); + } + } + + public void write(long fileOffset, byte[] data, int off, int len) throws IOException { + checkWriteResponse(asyncWrite(fileOffset, data, off, len)); + } + + protected Promise asyncWrite(long fileOffset, byte[] data, int off, int len) + throws IOException { + return requester.request(newRequest(PacketType.WRITE) + .putUInt64(fileOffset) + .putString(data, off, len) + ); + } + + private void checkWriteResponse(Promise responsePromise) throws SFTPException { + responsePromise.retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS).ensureStatusPacketIsOK(); + } + + public void setAttributes(FileAttributes attrs) throws IOException { + requester.request(newRequest(PacketType.FSETSTAT).putFileAttributes(attrs)) + .retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS).ensureStatusPacketIsOK(); + } + + public int getOutgoingPacketOverhead() { + return 1 + // packet type + 4 + // request id + 4 + // next length + handle.length + // next + 8 + // file offset + 4 + // data length + 4; // packet length + } + + public class RemoteFileOutputStream + extends OutputStream { + + private final byte[] b = new byte[1]; + + private final int maxUnconfirmedWrites; + private final Queue> unconfirmedWrites; + + private long fileOffset; + + public RemoteFileOutputStream() { + this(0); + } + + public RemoteFileOutputStream(long startingOffset) { + this(startingOffset, 0); + } + + public RemoteFileOutputStream(long startingOffset, int maxUnconfirmedWrites) { + this.fileOffset = startingOffset; + this.maxUnconfirmedWrites = maxUnconfirmedWrites; + this.unconfirmedWrites = new LinkedList>(); + } + + @Override + public void write(int w) throws IOException { + b[0] = (byte) w; + write(b, 0, 1); + } + + @Override + public void write(byte[] buf, int off, int len) throws IOException { + if (unconfirmedWrites.size() > maxUnconfirmedWrites) { + checkWriteResponse(unconfirmedWrites.remove()); + } + unconfirmedWrites.add(RemoteFile.this.asyncWrite(fileOffset, buf, off, len)); + fileOffset += len; + } + + @Override + public void flush() throws IOException { + while (!unconfirmedWrites.isEmpty()) { + checkWriteResponse(unconfirmedWrites.remove()); + } + } + + @Override + public void close() throws IOException { + flush(); + } + + } + + public class RemoteFileInputStream extends InputStream { + + private final byte[] b = new byte[1]; + + private long fileOffset; + private long markPos; + private long readLimit; + + public RemoteFileInputStream() { + this(0); + } + + public RemoteFileInputStream(long fileOffset) { + this.fileOffset = fileOffset; + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public void mark(int readLimit) { + this.readLimit = readLimit; + markPos = fileOffset; + } + + @Override + public void reset() throws IOException { + fileOffset = markPos; + } + + @Override + public long skip(long n) throws IOException { + final long fileLength = length(); + final Long previousFileOffset = fileOffset; + fileOffset = Math.min(fileOffset + n, fileLength); + return fileOffset - previousFileOffset; + } + + @Override + public int read() throws IOException { + return read(b, 0, 1) == -1 ? -1 : b[0] & 0xff; + } + + @Override + public int read(byte[] into, int off, int len) throws IOException { + int read = RemoteFile.this.read(fileOffset, into, off, len); + if (read != -1) { + fileOffset += read; + if (markPos != 0 && read > readLimit) { + // Invalidate mark position + markPos = 0; + } + } + return read; + } + + } + + public class ReadAheadRemoteFileInputStream + extends InputStream { + private class UnconfirmedRead { + private final long offset; + private final Promise promise; + private final int length; + + private UnconfirmedRead(long offset, int length, Promise promise) { + this.offset = offset; + this.length = length; + this.promise = promise; + } + + UnconfirmedRead(long offset, int length) throws IOException { + this(offset, length, RemoteFile.this.asyncRead(offset, length)); + } + + public long getOffset() { + return offset; + } + + public Promise getPromise() { + return promise; + } + + public int getLength() { + return length; + } + } + + private final byte[] b = new byte[1]; + + private final int maxUnconfirmedReads; + private final long readAheadLimit; + private final Deque unconfirmedReads = new ArrayDeque<>(); + + private long currentOffset; + private int maxReadLength = Integer.MAX_VALUE; + private boolean eof; + + public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads) { + this(maxUnconfirmedReads, 0L); + } + + /** + * + * @param maxUnconfirmedReads Maximum number of unconfirmed requests to send + * @param fileOffset Initial offset in file to read from + */ + public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset) { + this(maxUnconfirmedReads, fileOffset, -1L); + } + + /** + * + * @param maxUnconfirmedReads Maximum number of unconfirmed requests to send + * @param fileOffset Initial offset in file to read from + * @param readAheadLimit Read ahead is disabled after this limit has been reached + */ + public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset, long readAheadLimit) { + assert 0 <= maxUnconfirmedReads; + assert 0 <= fileOffset; + + this.maxUnconfirmedReads = maxUnconfirmedReads; + this.currentOffset = fileOffset; + this.readAheadLimit = readAheadLimit > 0 ? fileOffset + readAheadLimit : Long.MAX_VALUE; + } + + private ByteArrayInputStream pending = new ByteArrayInputStream(new byte[0]); + + private boolean retrieveUnconfirmedRead(boolean blocking) throws IOException { + final UnconfirmedRead unconfirmedRead = unconfirmedReads.peek(); + if (unconfirmedRead == null || !blocking && !unconfirmedRead.getPromise().isDelivered()) { + return false; + } + unconfirmedReads.remove(unconfirmedRead); + + final Response res = unconfirmedRead.promise.retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS); + switch (res.getType()) { + case DATA: + int recvLen = res.readUInt32AsInt(); + if (unconfirmedRead.offset == currentOffset) { + currentOffset += recvLen; + pending = new ByteArrayInputStream(res.array(), res.rpos(), recvLen); + + if (recvLen < unconfirmedRead.length) { + // The server returned a packet smaller than the client had requested. + // It can be caused by at least one of the following: + // * The file has been read fully. Then, few futile read requests can be sent during + // the next read(), but the file will be downloaded correctly anyway. + // * The server shapes the request length. Then, the read window will be adjusted, + // and all further read-ahead requests won't be shaped. + // * The file on the server is not a regular file, it is something like fifo. + // Then, the window will shrink, and the client will start reading the file slower than it + // hypothetically can. It must be a rare case, and it is not worth implementing a sort of + // congestion control algorithm here. + maxReadLength = recvLen; + unconfirmedReads.clear(); + } + } + break; + + case STATUS: + res.ensureStatusIs(Response.StatusCode.EOF); + eof = true; + break; + + default: + throw new SFTPException("Unexpected packet: " + res.getType()); + } + return true; + } + + @Override + public int read() + throws IOException { + return read(b, 0, 1) == -1 ? -1 : b[0] & 0xff; + } + + @Override + public int read(byte[] into, int off, int len) throws IOException { + + while (!eof && pending.available() <= 0) { + + // we also need to go here for len <= 0, because pending may be at + // EOF in which case it would return -1 instead of 0 + + long requestOffset; + if (unconfirmedReads.isEmpty()) { + requestOffset = currentOffset; + } + else { + final UnconfirmedRead lastRequest = unconfirmedReads.getLast(); + requestOffset = lastRequest.offset + lastRequest.length; + } + while (unconfirmedReads.size() <= maxUnconfirmedReads) { + // Send read requests as long as there is no EOF and we have not reached the maximum parallelism + int reqLen = Math.min(Math.max(1024, len), maxReadLength); + if (readAheadLimit > requestOffset) { + long remaining = readAheadLimit - requestOffset; + if (reqLen > remaining) { + reqLen = (int) remaining; + } + } + unconfirmedReads.add(new UnconfirmedRead(requestOffset, reqLen)); + requestOffset += reqLen; + if (requestOffset >= readAheadLimit) { + break; + } + } + + if (!retrieveUnconfirmedRead(true /*blocking*/)) { + + // this may happen if we change prefetch strategy + // currently, we should never get here... + + throw new IllegalStateException("Could not retrieve data for pending read request"); + } + } + + return pending.read(into, off, len); + } + + @Override + public int available() throws IOException { + boolean lastRead = true; + while (!eof && (pending.available() <= 0) && lastRead) { + lastRead = retrieveUnconfirmedRead(false /*blocking*/); + } + return pending.available(); + } + } +} + diff --git a/app/src/main/java/muon/ssh/RemoteResource.java b/app/src/main/java/muon/ssh/RemoteResource.java new file mode 100644 index 00000000..c5e2caf1 --- /dev/null +++ b/app/src/main/java/muon/ssh/RemoteResource.java @@ -0,0 +1,65 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package muon.ssh; + +import net.schmizz.sshj.sftp.PacketType; +import net.schmizz.sshj.sftp.Request; +import org.slf4j.Logger; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public abstract class RemoteResource + implements Closeable { + + /** Logger */ + protected final Logger log; + + protected final SFTPEngine requester; + protected final String path; + protected final byte[] handle; + + protected RemoteResource(SFTPEngine requester, String path, byte[] handle) { + this.requester = requester; + log = requester.getLoggerFactory().getLogger(getClass()); + this.path = path; + this.handle = handle; + } + + public String getPath() { + return path; + } + + protected Request newRequest(PacketType type) { + return requester.newRequest(type).putString(handle); + } + + @Override + public void close() + throws IOException { + log.debug("Closing `{}`", this); + requester.request(newRequest(PacketType.CLOSE)) + .retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS) + .ensureStatusPacketIsOK(); + } + + @Override + public String toString() { + return "RemoteResource{" + path + "}"; + } + +} diff --git a/app/src/main/java/muon/ssh/SFTPClient.java b/app/src/main/java/muon/ssh/SFTPClient.java new file mode 100644 index 00000000..3d3f815f --- /dev/null +++ b/app/src/main/java/muon/ssh/SFTPClient.java @@ -0,0 +1,241 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package muon.ssh; + +import net.schmizz.sshj.connection.channel.direct.SessionFactory; +import net.schmizz.sshj.sftp.*; +import net.schmizz.sshj.sftp.RemoteResourceInfo; +import net.schmizz.sshj.xfer.FilePermission; +import net.schmizz.sshj.xfer.LocalDestFile; +import net.schmizz.sshj.xfer.LocalSourceFile; +import org.slf4j.Logger; + +import java.io.Closeable; +import java.io.IOException; +import java.util.*; + +public class SFTPClient + implements Closeable { + + /** Logger */ + protected final Logger log; + + protected final SFTPEngine engine; + + public SFTPClient(SFTPEngine engine) { + this.engine = engine; + log = engine.getLoggerFactory().getLogger(getClass()); + } + + public SFTPClient(SessionFactory sessionFactory) throws IOException { + this.engine = new SFTPEngine(sessionFactory); + this.engine.init(); + log = engine.getLoggerFactory().getLogger(getClass()); + } + + public SFTPEngine getSFTPEngine() { + return engine; + } + + + public List ls(String path) + throws IOException { + return ls(path, null); + } + + public List ls(String path, RemoteResourceFilter filter) + throws IOException { + final RemoteDirectory dir = engine.openDir(path); + try { + return dir.scan(filter); + } finally { + dir.close(); + } + } + + public RemoteFile open(String filename, Set mode, FileAttributes attrs) + throws IOException { + log.debug("Opening `{}`", filename); + return engine.open(filename, mode, attrs); + } + + public RemoteFile open(String filename, Set mode) + throws IOException { + return open(filename, mode, FileAttributes.EMPTY); + } + + public RemoteFile open(String filename) + throws IOException { + return open(filename, EnumSet.of(OpenMode.READ)); + } + + public void mkdir(String dirname) + throws IOException { + engine.makeDir(dirname); + } + + public void mkdirs(String path) + throws IOException { + final Deque dirsToMake = new LinkedList(); + for (PathComponents current = engine.getPathHelper().getComponents(path); ; + current = engine.getPathHelper().getComponents(current.getParent())) { + final FileAttributes attrs = statExistence(current.getPath()); + if (attrs == null) { + dirsToMake.push(current.getPath()); + } else if (attrs.getType() != FileMode.Type.DIRECTORY) { + throw new SFTPException(current.getPath() + " exists but is not a directory"); + } else { + break; + } + } + while (!dirsToMake.isEmpty()) { + mkdir(dirsToMake.pop()); + } + } + + public FileAttributes statExistence(String path) + throws IOException { + try { + return engine.stat(path); + } catch (SFTPException sftpe) { + if (sftpe.getStatusCode() == Response.StatusCode.NO_SUCH_FILE) { + return null; + } else { + throw sftpe; + } + } + } + + public void rename(String oldpath, String newpath) throws IOException { + rename(oldpath, newpath, EnumSet.noneOf(RenameFlags.class)); + } + + public void rename(String oldpath, String newpath, Set renameFlags) + throws IOException { + engine.rename(oldpath, newpath, renameFlags); + } + + public void rm(String filename) + throws IOException { + engine.remove(filename); + } + + public void rmdir(String dirname) + throws IOException { + engine.removeDir(dirname); + } + + public void symlink(String linkpath, String targetpath) + throws IOException { + engine.symlink(linkpath, targetpath); + } + + public int version() { + return engine.getOperativeProtocolVersion(); + } + + public void setattr(String path, FileAttributes attrs) + throws IOException { + engine.setAttributes(path, attrs); + } + + public int uid(String path) + throws IOException { + return stat(path).getUID(); + } + + public int gid(String path) + throws IOException { + return stat(path).getGID(); + } + + public long atime(String path) + throws IOException { + return stat(path).getAtime(); + } + + public long mtime(String path) + throws IOException { + return stat(path).getMtime(); + } + + public Set perms(String path) + throws IOException { + return stat(path).getPermissions(); + } + + public FileMode mode(String path) + throws IOException { + return stat(path).getMode(); + } + + public FileMode.Type type(String path) + throws IOException { + return stat(path).getType(); + } + + public String readlink(String path) + throws IOException { + return engine.readLink(path); + } + + public FileAttributes stat(String path) + throws IOException { + return engine.stat(path); + } + + public FileAttributes lstat(String path) + throws IOException { + return engine.lstat(path); + } + + public void chown(String path, int uid) + throws IOException { + setattr(path, new FileAttributes.Builder().withUIDGID(uid, gid(path)).build()); + } + + public void chmod(String path, int perms) + throws IOException { + setattr(path, new FileAttributes.Builder().withPermissions(perms).build()); + } + + public void chgrp(String path, int gid) + throws IOException { + setattr(path, new FileAttributes.Builder().withUIDGID(uid(path), gid).build()); + } + + public void truncate(String path, long size) + throws IOException { + setattr(path, new FileAttributes.Builder().withSize(size).build()); + } + + public String canonicalize(String path) + throws IOException { + return engine.canonicalize(path); + } + + public long size(String path) + throws IOException { + return stat(path).getSize(); + } + + @Override + public void close() + throws IOException { + engine.close(); + } + +} diff --git a/app/src/main/java/muon/ssh/SFTPEngine.java b/app/src/main/java/muon/ssh/SFTPEngine.java new file mode 100644 index 00000000..a2c71103 --- /dev/null +++ b/app/src/main/java/muon/ssh/SFTPEngine.java @@ -0,0 +1,412 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package muon.ssh; + +import com.hierynomus.sshj.common.ThreadNameProvider; +import net.schmizz.concurrent.Promise; +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.LoggerFactory; +import net.schmizz.sshj.common.*; +import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.connection.channel.direct.*; +import net.schmizz.sshj.sftp.*; +import org.slf4j.Logger; + +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +public class SFTPEngine + implements Requester, Closeable { + + public static final int MAX_SUPPORTED_VERSION = 3; + public static final int DEFAULT_TIMEOUT_MS = 30 * 1000; // way too long, but it was the original default + + /** Logger */ + protected final LoggerFactory loggerFactory; + protected final Logger log; + + protected volatile int timeoutMs = DEFAULT_TIMEOUT_MS; + + protected final PathHelper pathHelper; + + protected final Session.Subsystem sub; + protected final PacketReader reader; + protected final OutputStream out; + + protected long reqID; + protected int operativeVersion; + protected final Map serverExtensions = new HashMap(); + + public SFTPEngine(SessionFactory ssh) + throws SSHException { + this(ssh, PathHelper.DEFAULT_PATH_SEPARATOR); + } + + public SFTPEngine(SessionFactory ssh, String pathSep) + throws SSHException { + Session session = ssh.startSession(); + loggerFactory = session.getLoggerFactory(); + log = loggerFactory.getLogger(getClass()); + sub = session.startSubsystem("sftp"); + out = sub.getOutputStream(); + reader = new PacketReader(this); + ThreadNameProvider.setThreadName(reader, ssh); + pathHelper = new PathHelper(new PathHelper.Canonicalizer() { + @Override + public String canonicalize(String path) + throws IOException { + return SFTPEngine.this.canonicalize(path); + } + }, pathSep); + } + + public SFTPEngine(Session.Subsystem subsystem, String pathSep) + throws SSHException { + loggerFactory = subsystem.getLoggerFactory(); + log = loggerFactory.getLogger(getClass()); + sub = subsystem; + out = sub.getOutputStream(); + reader = new PacketReader(this); + //ThreadNameProvider.setThreadName(reader, ssh); + pathHelper = new PathHelper(new PathHelper.Canonicalizer() { + @Override + public String canonicalize(String path) + throws IOException { + return SFTPEngine.this.canonicalize(path); + } + }, pathSep); + } + + public SFTPEngine init() + throws IOException { + return init(MAX_SUPPORTED_VERSION); + } + + /** + * Introduced for internal use by testcases. + * @param requestedVersion + * @throws IOException + */ + protected SFTPEngine init(int requestedVersion) + throws IOException { + if (requestedVersion > MAX_SUPPORTED_VERSION) + throw new SFTPException("You requested an unsupported protocol version: " + requestedVersion + " (requested) > " + MAX_SUPPORTED_VERSION + " (supported)"); + + if (requestedVersion < MAX_SUPPORTED_VERSION) + log.debug("Client version {} is smaller than MAX_SUPPORTED_VERSION {}", requestedVersion, MAX_SUPPORTED_VERSION); + + transmit(new SFTPPacket(PacketType.INIT).putUInt32(requestedVersion)); + + final SFTPPacket response = reader.readPacket(); + + final PacketType type = response.readType(); + if (type != PacketType.VERSION) + throw new SFTPException("Expected INIT packet, received: " + type); + + operativeVersion = response.readUInt32AsInt(); + log.debug("Server version {}", operativeVersion); + if (requestedVersion < operativeVersion) + throw new SFTPException("Server reported incompatible protocol version: " + operativeVersion); + + while (response.available() > 0) + serverExtensions.put(response.readString(), response.readString()); + + // Start reader thread + reader.start(); + return this; + } + + public Session.Subsystem getSubsystem() { + return sub; + } + + public int getOperativeProtocolVersion() { + return operativeVersion; + } + + public boolean supportsServerExtension(final String extension, final String domain) { + return serverExtensions.containsKey(extension + "@" + domain); + } + + public String getServerExtensionData(final String extension, final String domain) { + return serverExtensions.get(extension + "@" + domain); + } + + public Request newExtendedRequest(String reqName) { + return newRequest(PacketType.EXTENDED).putString(reqName); + } + + @Override + public PathHelper getPathHelper() { + return pathHelper; + } + + @Override + public synchronized Request newRequest(PacketType type) { + return new Request(type, reqID = reqID + 1 & 0xffffffffL); + } + + @Override + public Promise request(Request req) + throws IOException { + final Promise promise = reader.expectResponseTo(req.getRequestID()); + log.debug("Sending {}", req); + transmit(req); + return promise; + } + + private Response doRequest(Request req) + throws IOException { + return request(req).retrieve(getTimeoutMs(), TimeUnit.MILLISECONDS); + } + + public RemoteFile open(String path, Set modes, FileAttributes fa) + throws IOException { + final byte[] handle = doRequest( + newRequest(PacketType.OPEN).putString(path, sub.getRemoteCharset()).putUInt32(OpenMode.toMask(modes)).putFileAttributes(fa) + ).ensurePacketTypeIs(PacketType.HANDLE).readBytes(); + return new RemoteFile(this, path, handle); + } + + public RemoteFile open(String filename, Set modes) + throws IOException { + return open(filename, modes, FileAttributes.EMPTY); + } + + public RemoteFile open(String filename) + throws IOException { + return open(filename, EnumSet.of(OpenMode.READ)); + } + + public RemoteDirectory openDir(String path) + throws IOException { + final byte[] handle = doRequest( + newRequest(PacketType.OPENDIR).putString(path, sub.getRemoteCharset()) + ).ensurePacketTypeIs(PacketType.HANDLE).readBytes(); + return new RemoteDirectory(this, path, handle); + } + + public void setAttributes(String path, FileAttributes attrs) + throws IOException { + doRequest( + newRequest(PacketType.SETSTAT).putString(path, sub.getRemoteCharset()).putFileAttributes(attrs) + ).ensureStatusPacketIsOK(); + } + + public String readLink(String path) + throws IOException { + if (operativeVersion < 3) + throw new SFTPException("READLINK is not supported in SFTPv" + operativeVersion); + return readSingleName( + doRequest( + newRequest(PacketType.READLINK).putString(path, sub.getRemoteCharset()) + ), sub.getRemoteCharset()); + } + + public void makeDir(String path, FileAttributes attrs) + throws IOException { + doRequest(newRequest(PacketType.MKDIR).putString(path, sub.getRemoteCharset()).putFileAttributes(attrs)).ensureStatusPacketIsOK(); + } + + public void makeDir(String path) + throws IOException { + makeDir(path, FileAttributes.EMPTY); + } + + public void symlink(String linkpath, String targetpath) + throws IOException { + if (operativeVersion < 3) + throw new SFTPException("SYMLINK is not supported in SFTPv" + operativeVersion); + doRequest( + newRequest(PacketType.SYMLINK).putString(linkpath, sub.getRemoteCharset()).putString(targetpath, sub.getRemoteCharset()) + ).ensureStatusPacketIsOK(); + } + + public void remove(String filename) + throws IOException { + doRequest( + newRequest(PacketType.REMOVE).putString(filename, sub.getRemoteCharset()) + ).ensureStatusPacketIsOK(); + } + + public void removeDir(String path) + throws IOException { + doRequest( + newRequest(PacketType.RMDIR).putString(path, sub.getRemoteCharset()) + ).ensureStatusIs(Response.StatusCode.OK); + } + + public FileAttributes stat(String path) + throws IOException { + return stat(PacketType.STAT, path); + } + + public FileAttributes lstat(String path) + throws IOException { + return stat(PacketType.LSTAT, path); + } + + public void rename(String oldPath, String newPath, Set flags) + throws IOException { + if (operativeVersion < 1) { + throw new SFTPException("RENAME is not supported in SFTPv" + operativeVersion); + } + + // request variables to be determined + PacketType type = PacketType.RENAME; // Default + long renameFlagMask = 0L; + String serverExtension = null; + + if (!flags.isEmpty()) { + // SFTP Version 5 introduced rename flags according to Section 6.5 of the specification + if (operativeVersion >= 5) { + for (RenameFlags flag : flags) { + renameFlagMask = renameFlagMask | flag.longValue(); + } + } + // Try to find a fallback solution if flags are not supported by the server. + + // "posix-rename@openssh.com" provides ATOMIC and OVERWRITE behaviour. + // From the SFTP-spec, Section 6.5: + // "If SSH_FXP_RENAME_OVERWRITE is specified, the server MAY perform an atomic rename even if it is + // not requested." + // So, if overwrite is allowed we can always use the posix-rename as a fallback. + else if (flags.contains(RenameFlags.OVERWRITE) && + supportsServerExtension("posix-rename","openssh.com")) { + + type = PacketType.EXTENDED; + serverExtension = "posix-rename@openssh.com"; + } + + // Because the OVERWRITE flag changes the behaviour in a possibly unintended way, it has to be + // explicitly requested for the above fallback to be applicable. + // Tell this to the developer if ATOMIC is requested without OVERWRITE. + else if (flags.contains(RenameFlags.ATOMIC) && + !flags.contains(RenameFlags.OVERWRITE) && + !flags.contains(RenameFlags.NATIVE) && // see next case below + supportsServerExtension("posix-rename","openssh.com")) { + throw new SFTPException("RENAME-FLAGS are not supported in SFTPv" + operativeVersion + " but " + + "the \"posix-rename@openssh.com\" extension could be used as fallback if OVERWRITE " + + "behaviour is acceptable (needs to be activated via RenameFlags.OVERWRITE)."); + } + + // From the SFTP-spec, Section 6.5: + // "If flags includes SSH_FXP_RENAME_NATIVE, the server is free to do the rename operation in whatever + // fashion it deems appropriate. Other flag values are considered hints as to desired behavior, but not + // requirements." + else if (flags.contains(RenameFlags.NATIVE)) { + log.debug("Flags are not supported but NATIVE-flag allows to ignore other requested flags: " + + flags.toString()); + } + + // finally: let the user know that the server does not support what was asked + else { + throw new SFTPException("RENAME-FLAGS are not supported in SFTPv" + operativeVersion + " and no " + + "supported server extension could be found to achieve a similar result."); + } + } + + // build and send request + final Request request = newRequest(type); + + if (serverExtension != null) { + request.putString(serverExtension); + } + + request.putString(oldPath, sub.getRemoteCharset()) + .putString(newPath, sub.getRemoteCharset()); + + if (renameFlagMask != 0L) { + request.putUInt32(renameFlagMask); + } + + doRequest(request).ensureStatusPacketIsOK(); + } + + public String canonicalize(String path) + throws IOException { + return readSingleName( + doRequest( + newRequest(PacketType.REALPATH).putString(path, sub.getRemoteCharset()) + ), sub.getRemoteCharset()); + } + + public void setTimeoutMs(int timeoutMs) { + this.timeoutMs = timeoutMs; + } + + public int getTimeoutMs() { + return timeoutMs; + } + + @Override + public void close() + throws IOException { + sub.close(); + reader.interrupt(); + } + + protected LoggerFactory getLoggerFactory() { + return loggerFactory; + } + + protected FileAttributes stat(PacketType pt, String path) + throws IOException { + return doRequest(newRequest(pt).putString(path, sub.getRemoteCharset())) + .ensurePacketTypeIs(PacketType.ATTRS) + .readFileAttributes(); + } + + private static byte[] readSingleNameAsBytes(Response res) + throws IOException { + res.ensurePacketTypeIs(PacketType.NAME); + if (res.readUInt32AsInt() == 1) + return res.readStringAsBytes(); + else + throw new SFTPException("Unexpected data in " + res.getType() + " packet"); + } + + /** Using UTF-8 */ + protected static String readSingleName(Response res) + throws IOException { + return readSingleName(res, IOUtils.UTF8); + } + + /** Using any character set */ + protected static String readSingleName(Response res, Charset charset) + throws IOException { + return new String(readSingleNameAsBytes(res), charset); + } + + protected synchronized void transmit(SFTPPacket payload) + throws IOException { + final int len = payload.available(); + out.write((len >>> 24) & 0xff); + out.write((len >>> 16) & 0xff); + out.write((len >>> 8) & 0xff); + out.write(len & 0xff); + out.write(payload.array(), payload.rpos(), len); + out.flush(); + } + +} diff --git a/app/src/main/java/muon/ssh/SubsystemWrapper.java b/app/src/main/java/muon/ssh/SubsystemWrapper.java new file mode 100644 index 00000000..cedb9f75 --- /dev/null +++ b/app/src/main/java/muon/ssh/SubsystemWrapper.java @@ -0,0 +1,129 @@ +package muon.ssh; + +import net.schmizz.sshj.common.LoggerFactory; +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.transport.TransportException; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.concurrent.TimeUnit; + +public class SubsystemWrapper implements Session.Subsystem { + private Session.Command command; + + public SubsystemWrapper(Session.Command command) { + this.command = command; + } + + @Override + public Integer getExitStatus() { + return command.getExitStatus(); + } + + @Override + public void close() throws TransportException, ConnectionException { + System.err.println("SubsystemWrapper closing..."); + command.close(); + System.err.println("SubsystemWrapper closed"); + } + + @Override + public boolean getAutoExpand() { + return command.getAutoExpand(); + } + + @Override + public int getID() { + return command.getID(); + } + + @Override + public InputStream getInputStream() { + return command.getInputStream(); + } + + @Override + public int getLocalMaxPacketSize() { + return command.getLocalMaxPacketSize(); + } + + @Override + public long getLocalWinSize() { + return command.getLocalWinSize(); + } + + @Override + public OutputStream getOutputStream() { + return command.getOutputStream(); + } + + @Override + public int getRecipient() { + return command.getRecipient(); + } + + @Override + public Charset getRemoteCharset() { + return command.getRemoteCharset(); + } + + @Override + public int getRemoteMaxPacketSize() { + return command.getRemoteMaxPacketSize(); + } + + @Override + public long getRemoteWinSize() { + return command.getRemoteWinSize(); + } + + @Override + public String getType() { + return command.getType(); + } + + @Override + public boolean isOpen() { + return command.isOpen(); + } + + @Override + public void setAutoExpand(boolean autoExpand) { + command.setAutoExpand(autoExpand); + } + + @Override + public void join() throws ConnectionException { + command.join(); + } + + @Override + public void join(long timeout, TimeUnit unit) throws ConnectionException { + command.join(timeout, unit); + } + + @Override + public boolean isEOF() { + return command.isEOF(); + } + + @Override + public LoggerFactory getLoggerFactory() { + return command.getLoggerFactory(); + } + + @Override + public void notifyError(SSHException error) { + command.notifyError(error); + } + + @Override + public void handle(Message msg, SSHPacket buf) throws SSHException { + command.handle(msg, buf); + } +} diff --git a/app/src/main/java/muon/util/FixedBufferInputStream.java b/app/src/main/java/muon/util/FixedBufferInputStream.java index b8914c98..9b79fbef 100644 --- a/app/src/main/java/muon/util/FixedBufferInputStream.java +++ b/app/src/main/java/muon/util/FixedBufferInputStream.java @@ -1,8 +1,7 @@ package muon.util; import net.schmizz.sshj.sftp.OpenMode; -import net.schmizz.sshj.sftp.RemoteFile; -import net.schmizz.sshj.sftp.SFTPClient; +import muon.ssh.*; import org.jetbrains.annotations.NotNull; import java.io.IOException; diff --git a/app/src/main/java/muon/util/FixedBufferOutputStream.java b/app/src/main/java/muon/util/FixedBufferOutputStream.java index a81b1635..0201eeee 100644 --- a/app/src/main/java/muon/util/FixedBufferOutputStream.java +++ b/app/src/main/java/muon/util/FixedBufferOutputStream.java @@ -1,11 +1,9 @@ package muon.util; -import muon.service.SftpSession; import net.schmizz.sshj.sftp.OpenMode; -import net.schmizz.sshj.sftp.RemoteFile; -import net.schmizz.sshj.sftp.SFTPClient; import org.jetbrains.annotations.NotNull; +import muon.ssh.*; import java.io.IOException; import java.io.OutputStream; import java.util.EnumSet; diff --git a/app/src/main/java/muon/util/SudoUtils.java b/app/src/main/java/muon/util/SudoUtils.java index 5f0dd4ab..95ae47d5 100644 --- a/app/src/main/java/muon/util/SudoUtils.java +++ b/app/src/main/java/muon/util/SudoUtils.java @@ -5,6 +5,7 @@ import muon.dto.session.SessionInfo; import muon.exceptions.FSAccessException; import muon.exceptions.FSConnectException; +import muon.misc.CancellationToken; import muon.service.MultiplexingSessionService; import java.io.*; @@ -32,7 +33,7 @@ public static int exec1(SessionInfo sessionInfo, String command, OutputStream ou } } - public static int exec(SessionInfo sessionInfo, String command, Supplier passwordSupplier) + public static int exec(SessionInfo sessionInfo, String command, Supplier passwordSupplier, CancellationToken cancellationToken) throws FSConnectException, FSAccessException { try { var prompt = UUID.randomUUID().toString(); @@ -40,7 +41,8 @@ public static int exec(SessionInfo sessionInfo, String command, Supplier var fullCommand = "sudo -S -p '" + prompt + "' " + command; System.err.println(fullCommand); var ret = App.getMultiplexingSessionService().executeCommand(sessionInfo, fullCommand, - new ByteArrayOutputStream(), new ByteArrayOutputStream(), prompt, passwordSupplier, false); + new ByteArrayOutputStream(), new ByteArrayOutputStream(), prompt, + passwordSupplier, cancellationToken, false); System.out.println("Sudo RET: " + ret); return ret; } catch (Exception e) { @@ -49,14 +51,14 @@ public static int exec(SessionInfo sessionInfo, String command, Supplier } } - public static int exec(SessionInfo sessionInfo, String command, OutputStream out, OutputStream err, Supplier passwordSupplier) + public static int exec(SessionInfo sessionInfo, String command, OutputStream out, OutputStream err, Supplier passwordSupplier, CancellationToken cancellationToken) throws FSConnectException, FSAccessException { try { var prompt = UUID.randomUUID().toString(); System.out.println(prompt); var fullCommand = "sudo -S -p '" + prompt + "' " + command; System.err.println(fullCommand); - var ret = App.getMultiplexingSessionService().executeCommand(sessionInfo, fullCommand, out, err, prompt, passwordSupplier, false); + var ret = App.getMultiplexingSessionService().executeCommand(sessionInfo, fullCommand, out, err, prompt, passwordSupplier, cancellationToken, false); System.out.println("Sudo RET: " + ret); return ret; } catch (Exception e) {