mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-06 03:50:02 +02:00
Merge remote-tracking branch 'origin/GP-1977_Dan_terminalEmulation--SQUASHED'
This commit is contained in:
commit
09170c9e8b
98 changed files with 7972 additions and 141 deletions
|
@ -16,6 +16,7 @@
|
|||
package docking;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.KeyStroke;
|
||||
|
@ -24,12 +25,12 @@ import docking.action.DockingActionIf;
|
|||
import docking.actions.KeyBindingUtils;
|
||||
|
||||
/**
|
||||
* A class that can be used as an interface for using actions associated with keybindings. This
|
||||
* A class that can be used as an interface for using actions associated with keybindings. This
|
||||
* class is meant to only by used by internal Ghidra key event processing.
|
||||
*/
|
||||
public abstract class DockingKeyBindingAction extends AbstractAction {
|
||||
|
||||
private DockingActionIf docakbleAction;
|
||||
private DockingActionIf dockingAction;
|
||||
|
||||
protected final KeyStroke keyStroke;
|
||||
protected final Tool tool;
|
||||
|
@ -37,7 +38,7 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
|
|||
public DockingKeyBindingAction(Tool tool, DockingActionIf action, KeyStroke keyStroke) {
|
||||
super(KeyBindingUtils.parseKeyStroke(keyStroke));
|
||||
this.tool = tool;
|
||||
this.docakbleAction = action;
|
||||
this.dockingAction = action;
|
||||
this.keyStroke = keyStroke;
|
||||
}
|
||||
|
||||
|
@ -63,7 +64,7 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
|
|||
ComponentProvider provider = tool.getActiveComponentProvider();
|
||||
ActionContext context = getLocalContext(provider);
|
||||
context.setSourceObject(e.getSource());
|
||||
docakbleAction.actionPerformed(context);
|
||||
dockingAction.actionPerformed(context);
|
||||
}
|
||||
|
||||
protected ActionContext getLocalContext(ComponentProvider localProvider) {
|
||||
|
@ -78,4 +79,8 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
|
|||
|
||||
return new DefaultActionContext(localProvider, null);
|
||||
}
|
||||
|
||||
public List<DockingActionIf> getActions() {
|
||||
return List.of(dockingAction);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -705,11 +705,13 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
placeholderManager.removeComponent(provider);
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Package-level Action Methods
|
||||
//==================================================================================================
|
||||
|
||||
Iterator<DockingActionIf> getComponentActions(ComponentProvider provider) {
|
||||
/**
|
||||
* Get the local actions installed on the given provider
|
||||
*
|
||||
* @param provider the provider
|
||||
* @return an iterator over the actions
|
||||
*/
|
||||
public Iterator<DockingActionIf> getComponentActions(ComponentProvider provider) {
|
||||
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
|
||||
if (placeholder != null) {
|
||||
return placeholder.getActions();
|
||||
|
@ -719,6 +721,10 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
return emptyList.iterator();
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Package-level Action Methods
|
||||
//==================================================================================================
|
||||
|
||||
void removeProviderAction(ComponentProvider provider, DockingActionIf action) {
|
||||
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
|
||||
if (placeholder != null) {
|
||||
|
|
|
@ -318,6 +318,7 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
|
|||
return dwm.getActiveWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DockingActionIf> getActions() {
|
||||
List<DockingActionIf> list = new ArrayList<>(actions.size());
|
||||
for (ActionData actionData : actions) {
|
||||
|
|
|
@ -57,7 +57,7 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets this scroll pane to never show scroll bars. This is useful when you want a container
|
||||
* Sets this scroll pane to never show scroll bars. This is useful when you want a container
|
||||
* whose view is always as big as the component in this scroll pane.
|
||||
*/
|
||||
public void setNeverScroll(boolean b) {
|
||||
|
@ -67,6 +67,20 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
|
|||
useViewSizeAsPreferredSize = b;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see JScrollPane#setVerticalScrollBarPolicy(int)
|
||||
*/
|
||||
public void setVerticalScrollBarPolicy(int policy) {
|
||||
scrollPane.setVerticalScrollBarPolicy(policy);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see JScrollPane#setHorizontalScrollBarPolicy(int)
|
||||
*/
|
||||
public void setHorizontalScrollBarPolicy(int policy) {
|
||||
scrollPane.setHorizontalScrollBarPolicy(policy);
|
||||
}
|
||||
|
||||
private ViewToIndexMapper createIndexMapper() {
|
||||
if (neverScroll) {
|
||||
return new PreMappedViewToIndexMapper(scrollable);
|
||||
|
@ -215,6 +229,7 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation,
|
||||
int direction) {
|
||||
|
||||
|
@ -296,8 +311,8 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
|
|||
|
||||
/**
|
||||
* Sets whether the scroll wheel triggers scrolling <b>when over the scroll pane</b> of this
|
||||
* class. When disabled, scrolling will still work when over the component inside of
|
||||
* this class, but not when over the scroll bar.
|
||||
* class. When disabled, scrolling will still work when over the component inside of this class,
|
||||
* but not when over the scroll bar.
|
||||
*
|
||||
* @param enabled true to enable
|
||||
*/
|
||||
|
|
|
@ -322,7 +322,7 @@ public abstract class ThemeManager {
|
|||
FontValue font = currentValues.getFont(id);
|
||||
|
||||
if (font == null) {
|
||||
error("No color value registered for: '" + id + "'");
|
||||
error("No font value registered for: '" + id + "'");
|
||||
return DEFAULT_FONT;
|
||||
}
|
||||
return font.get(currentValues);
|
||||
|
|
0
Ghidra/Framework/Pty/Module.manifest
Normal file
0
Ghidra/Framework/Pty/Module.manifest
Normal file
30
Ghidra/Framework/Pty/build.gradle
Normal file
30
Ghidra/Framework/Pty/build.gradle
Normal file
|
@ -0,0 +1,30 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
import org.gradle.plugins.ide.eclipse.model.Container;
|
||||
|
||||
apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle"
|
||||
apply from: "$rootProject.projectDir/gradle/javaProject.gradle"
|
||||
apply from: "$rootProject.projectDir/gradle/jacocoProject.gradle"
|
||||
apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle"
|
||||
apply from: "$rootProject.projectDir/gradle/javadoc.gradle"
|
||||
|
||||
apply plugin: 'eclipse'
|
||||
eclipse.project.name = 'Framework Pty'
|
||||
|
||||
dependencies {
|
||||
api project(':Framework-Debugging')
|
||||
api "com.jcraft:jsch:0.1.55"
|
||||
}
|
4
Ghidra/Framework/Pty/certification.manifest
Normal file
4
Ghidra/Framework/Pty/certification.manifest
Normal file
|
@ -0,0 +1,4 @@
|
|||
##VERSION: 2.0
|
||||
##MODULE IP: Apache License 2.0
|
||||
Module.manifest||GHIDRA||||END|
|
||||
data/gui.palette.theme.properties||GHIDRA||||END|
|
100
Ghidra/Framework/Pty/src/main/java/ghidra/pty/Pty.java
Normal file
100
Ghidra/Framework/Pty/src/main/java/ghidra/pty/Pty.java
Normal file
|
@ -0,0 +1,100 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A pseudo-terminal
|
||||
*
|
||||
* <p>
|
||||
* A pseudo-terminal is essentially a two way pipe where one end acts as the parent, and the other
|
||||
* acts as the child. The process opening the pseudo-terminal is given a handle to both ends. The
|
||||
* child end is generally given to a subprocess, possibly designating the pty as the controlling tty
|
||||
* of a new session. This scheme is how, for example, an SSH daemon starts a new login shell. The
|
||||
* shell is given the child end, and the parent end is presented to the SSH client.
|
||||
*
|
||||
* <p>
|
||||
* This is more powerful than controlling a process via standard in and standard out. 1) Some
|
||||
* programs detect whether or not stdin/out/err refer to the controlling tty. For example, a program
|
||||
* should avoid prompting for passwords unless stdin is the controlling tty. Using a pty can provide
|
||||
* a controlling tty that is not necessarily controlled by a user. 2) Terminals have other
|
||||
* properties and can, e.g., send signals to the foreground process group (job) by sending special
|
||||
* characters. Normal characters are passed to the child, but special characters may be interpreted
|
||||
* by the terminal's <em>line discipline</em>. A rather common case is to send Ctrl-C (character
|
||||
* 003). Using stdin, the subprocess simply reads 003. With a properly-configured pty and session,
|
||||
* the subprocess is interrupted (sent SIGINT) instead.
|
||||
*
|
||||
* <p>
|
||||
* This class opens a pseudo-terminal and presents both ends as individual handles. The parent end
|
||||
* simply provides an input and output stream. These are typical byte-oriented streams, except that
|
||||
* the data passes through the pty, subject to interpretation by the OS kernel. On Linux, this means
|
||||
* the pty will apply the configured line discipline. Consult the host OS documentation for special
|
||||
* character sequences.
|
||||
*
|
||||
* <p>
|
||||
* The child end also provides the input and output streams, but it is uncommon to use them from the
|
||||
* same process. More likely, subprocess is launched in a new session, configuring the child as the
|
||||
* controlling terminal. Thus, the child handle provides methods for obtaining the child pty file
|
||||
* name and/or spawning a new session. Once spawned, the parent end is used to control the session.
|
||||
*
|
||||
* <p>
|
||||
* Example:
|
||||
*
|
||||
* <pre>
|
||||
* Pty pty = factory.openpty();
|
||||
* pty.getChild().session("bash");
|
||||
*
|
||||
* PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
|
||||
* writer.println("echo test");
|
||||
* BufferedReader reader =
|
||||
* new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
|
||||
* System.out.println(reader.readLine());
|
||||
* System.out.println(reader.readLine());
|
||||
*
|
||||
* pty.close();
|
||||
* </pre>
|
||||
*/
|
||||
public interface Pty extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* Get a handle to the parent side of the pty
|
||||
*
|
||||
* @return the parent handle
|
||||
*/
|
||||
PtyParent getParent();
|
||||
|
||||
/**
|
||||
* Get a handle to the child side of the pty
|
||||
*
|
||||
* @return the child handle
|
||||
*/
|
||||
PtyChild getChild();
|
||||
|
||||
/**
|
||||
* Closes both ends of the pty
|
||||
*
|
||||
* <p>
|
||||
* This only closes this process's handles to the pty. For the parent end, this should be the
|
||||
* only process with a handle. The child end may be opened by any number of other processes.
|
||||
* More than likely, however, those processes will terminate once the parent end is closed,
|
||||
* since reads or writes on the child will produce EOF or an error.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
void close() throws IOException;
|
||||
}
|
88
Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java
Normal file
88
Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java
Normal file
|
@ -0,0 +1,88 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* The child (UNIX "slave") end of a pseudo-terminal
|
||||
*/
|
||||
public interface PtyChild extends PtyEndpoint {
|
||||
|
||||
/**
|
||||
* A terminal mode flag
|
||||
*/
|
||||
interface TermMode {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode flag for local echo
|
||||
*/
|
||||
enum Echo implements TermMode {
|
||||
/**
|
||||
* Input is echoed to output by the terminal itself.
|
||||
*/
|
||||
ON,
|
||||
/**
|
||||
* No local echo.
|
||||
*/
|
||||
OFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a subprocess in a new session whose controlling tty is this pseudo-terminal
|
||||
*
|
||||
* <p>
|
||||
* This method or {@link #nullSession(Collection)} can only be invoked once per pty.
|
||||
*
|
||||
* @param args the image path and arguments
|
||||
* @param env the environment
|
||||
* @param mode the terminal mode. If a mode is not implemented, it may be silently ignored.
|
||||
* @return a handle to the subprocess
|
||||
* @throws IOException if the session could not be started
|
||||
*/
|
||||
PtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode)
|
||||
throws IOException;
|
||||
|
||||
default PtySession session(String[] args, Map<String, String> env, TermMode... mode)
|
||||
throws IOException {
|
||||
return session(args, env, List.of(mode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a session without a real leader, instead obtaining the pty's name
|
||||
*
|
||||
* <p>
|
||||
* This method or {@link #session(String[], Map, Collection)} can only be invoked once per pty.
|
||||
* It must be called before anyone reads the parent's output stream, since obtaining the
|
||||
* filename may be implemented by the parent sending commands to its child.
|
||||
*
|
||||
* <p>
|
||||
* If the child end of the pty is on a remote system, this should be the file (or other
|
||||
* resource) name as it would be accessed on that remote system.
|
||||
*
|
||||
* @param mode the terminal mode. If a mode is not implemented, it may be silently ignored.
|
||||
* @return the file name
|
||||
* @throws IOException if the session could not be started or the pty name could not be
|
||||
* determined
|
||||
*/
|
||||
String nullSession(Collection<TermMode> mode) throws IOException;
|
||||
|
||||
default String nullSession(TermMode... mode) throws IOException {
|
||||
return nullSession(List.of(mode));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* One end of a pseudo-terminal
|
||||
*/
|
||||
public interface PtyEndpoint {
|
||||
|
||||
/**
|
||||
* Get the output stream for this end of the pty
|
||||
*
|
||||
* <p>
|
||||
* Writes to this stream arrive on the input stream for the opposite end, subject to the
|
||||
* terminal's line discipline.
|
||||
*
|
||||
* @return the output stream
|
||||
* @throws UnsupportedOperationException if this end is not local
|
||||
*/
|
||||
OutputStream getOutputStream();
|
||||
|
||||
/**
|
||||
* Get the input stream for this end of the pty
|
||||
*
|
||||
* <p>
|
||||
* Writes to the output stream of the opposite end arrive here, subject to the terminal's line
|
||||
* discipline.
|
||||
*
|
||||
* @return the input stream
|
||||
* @throws UnsupportedOperationException if this end is not local
|
||||
*/
|
||||
InputStream getInputStream();
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ghidra.framework.OperatingSystem;
|
||||
import ghidra.pty.linux.LinuxPtyFactory;
|
||||
import ghidra.pty.macos.MacosPtyFactory;
|
||||
import ghidra.pty.windows.ConPtyFactory;
|
||||
|
||||
/**
|
||||
* A mechanism for opening pseudo-terminals
|
||||
*/
|
||||
public interface PtyFactory {
|
||||
|
||||
/**
|
||||
* Choose a factory of local pty's for the host operating system
|
||||
*
|
||||
* @return the factory
|
||||
*/
|
||||
static PtyFactory local() {
|
||||
switch (OperatingSystem.CURRENT_OPERATING_SYSTEM) {
|
||||
case MAC_OS_X:
|
||||
return new MacosPtyFactory();
|
||||
case LINUX:
|
||||
return new LinuxPtyFactory();
|
||||
case WINDOWS:
|
||||
return new ConPtyFactory();
|
||||
default:
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new pseudo-terminal
|
||||
*
|
||||
* @return new new Pty
|
||||
* @throws IOException for an I/O error, including cancellation
|
||||
*/
|
||||
Pty openpty() throws IOException;
|
||||
|
||||
String getDescription();
|
||||
}
|
23
Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyParent.java
Normal file
23
Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyParent.java
Normal file
|
@ -0,0 +1,23 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty;
|
||||
|
||||
/**
|
||||
* The parent (UNIX "master") end of a pseudo-terminal
|
||||
*/
|
||||
public interface PtyParent extends PtyEndpoint {
|
||||
void setWindowSize(int cols, int rows);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty;
|
||||
|
||||
/**
|
||||
* A session led by the child pty
|
||||
*
|
||||
* <p>
|
||||
* This is typically a handle to the (local or remote) process designated as the "session leader"
|
||||
*/
|
||||
public interface PtySession {
|
||||
|
||||
/**
|
||||
* Wait for the session leader to exit, returning its optional exit status code
|
||||
*
|
||||
* @return the status code, if applicable and implemented
|
||||
* @throws InterruptedException if the wait is interrupted
|
||||
*/
|
||||
int waitExited() throws InterruptedException;
|
||||
|
||||
/**
|
||||
* Take the greatest efforts to terminate the session (leader and descendants)
|
||||
*
|
||||
* <p>
|
||||
* If this represents a remote session, this should strive to release the remote resources
|
||||
* consumed by this session. If that is not possible, this should at the very least release
|
||||
* whatever local resources are used in maintaining and controlling the remote session.
|
||||
*/
|
||||
void destroyForcibly();
|
||||
}
|
31
Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Err.java
Normal file
31
Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Err.java
Normal file
|
@ -0,0 +1,31 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.linux;
|
||||
|
||||
import com.sun.jna.LastErrorException;
|
||||
import com.sun.jna.Native;
|
||||
|
||||
public interface Err {
|
||||
PosixC BARE_POSIX = PosixC.BARE;
|
||||
|
||||
static int checkLt0(int result) {
|
||||
if (result < 0) {
|
||||
int errno = Native.getLastError();
|
||||
throw new LastErrorException("[" + errno + "] " + BARE_POSIX.strerror(errno));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.linux;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import com.sun.jna.Memory;
|
||||
|
||||
/**
|
||||
* An input stream that wraps a native POSIX file descriptor
|
||||
*
|
||||
* <p>
|
||||
* <b>WARNING:</b> This class makes use of jnr-ffi to invoke native functions. An invalid file
|
||||
* descriptor is generally detected, but an incorrect, but valid file descriptor may cause undefined
|
||||
* behavior.
|
||||
*/
|
||||
public class FdInputStream extends InputStream {
|
||||
private static final PosixC LIB_POSIX = PosixC.INSTANCE;
|
||||
|
||||
private final int fd;
|
||||
private boolean closed = false;
|
||||
|
||||
/**
|
||||
* Wrap the given file descriptor in an {@link InputStream}
|
||||
*
|
||||
* @param fd the file descriptor
|
||||
*/
|
||||
FdInputStream(int fd) {
|
||||
this.fd = fd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int read() throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException("Stream closed");
|
||||
}
|
||||
byte[] buf = new byte[1];
|
||||
if (0 == read(buf)) {
|
||||
return -1;
|
||||
}
|
||||
return buf[0] & 0x0FF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int read(byte[] b) throws IOException {
|
||||
return read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int read(byte[] b, int off, int len) throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException("Stream closed");
|
||||
}
|
||||
if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
Memory buf = new Memory(len);
|
||||
int ret = LIB_POSIX.read(fd, buf, len);
|
||||
buf.read(0, b, off, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
closed = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.linux;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import com.sun.jna.Memory;
|
||||
|
||||
/**
|
||||
* An output stream that wraps a native POSIX file descriptor
|
||||
*
|
||||
* <p>
|
||||
* <b>WARNING:</b> This class makes use of jnr-ffi to invoke native functions. An invalid file
|
||||
* descriptor is generally detected, but an incorrect, but valid file descriptor may cause undefined
|
||||
* behavior.
|
||||
*/
|
||||
public class FdOutputStream extends OutputStream {
|
||||
private static final PosixC LIB_POSIX = PosixC.INSTANCE;
|
||||
|
||||
private final int fd;
|
||||
private boolean closed = false;
|
||||
|
||||
/**
|
||||
* Wrap the given file descriptor in an {@link OutputStream}
|
||||
*
|
||||
* @param fd the file descriptor
|
||||
*/
|
||||
FdOutputStream(int fd) {
|
||||
this.fd = fd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(int b) throws IOException {
|
||||
write(new byte[] { (byte) b });
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] b) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] b, int off, int len) throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException("Stream closed");
|
||||
}
|
||||
Memory buf = new Memory(len);
|
||||
buf.write(0, b, off, len);
|
||||
int total = 0;
|
||||
do {
|
||||
int ret = LIB_POSIX.write(fd, buf, len - total);
|
||||
total += ret;
|
||||
}
|
||||
while (total < len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
closed = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.linux;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.sun.jna.*;
|
||||
import com.sun.jna.ptr.IntByReference;
|
||||
|
||||
import ghidra.pty.Pty;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public class LinuxPty implements Pty {
|
||||
|
||||
static final PosixC LIB_POSIX = PosixC.INSTANCE;
|
||||
|
||||
private final int aparent;
|
||||
private final int achild;
|
||||
//private final String name;
|
||||
private boolean closed = false;
|
||||
|
||||
private final LinuxPtyParent parent;
|
||||
private final LinuxPtyChild child;
|
||||
|
||||
public static LinuxPty openpty() throws IOException {
|
||||
// TODO: Support termp and winp?
|
||||
IntByReference p = new IntByReference();
|
||||
IntByReference c = new IntByReference();
|
||||
Memory n = new Memory(1024);
|
||||
Util.INSTANCE.openpty(p, c, n, null, null);
|
||||
return new LinuxPty(p.getValue(), c.getValue(), n.getString(0));
|
||||
}
|
||||
|
||||
LinuxPty(int aparent, int achild, String name) {
|
||||
Msg.debug(this, "New Pty: " + name + " at (" + aparent + "," + achild + ")");
|
||||
this.aparent = aparent;
|
||||
this.achild = achild;
|
||||
|
||||
this.parent = new LinuxPtyParent(aparent);
|
||||
this.child = new LinuxPtyChild(achild, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinuxPtyParent getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinuxPtyChild getChild() {
|
||||
return child;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
LIB_POSIX.close(achild);
|
||||
LIB_POSIX.close(aparent);
|
||||
closed = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.linux;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
|
||||
import ghidra.pty.PtyChild;
|
||||
import ghidra.pty.PtySession;
|
||||
import ghidra.pty.linux.PosixC.Termios;
|
||||
import ghidra.pty.local.LocalProcessPtySession;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
|
||||
static final PosixC LIB_POSIX = PosixC.INSTANCE;
|
||||
|
||||
private final String name;
|
||||
|
||||
LinuxPtyChild(int fd, String name) {
|
||||
super(fd);
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nullSession(Collection<TermMode> mode) {
|
||||
applyMode(mode);
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @implNote This uses {@link ProcessBuilder} to launch the subprocess. See its documentation
|
||||
* for more details of the parameters of this method.
|
||||
* @implNote This actually launches a special "leader" subprocess, which sets up the session and
|
||||
* then executes the requested program. The requested program image replaces the
|
||||
* leader so that the returned process is indeed a handle to the requested program.
|
||||
* Ordinarily, this does not matter, but it may be useful to know when debugging.
|
||||
* Furthermore, if special characters are sent on the parent before the image is
|
||||
* replaced, they may be received by the leader instead. For example, Ctrl-C might be
|
||||
* received by the leader by mistake if sent immediately upon spawning a new session.
|
||||
* Users should send a simple command, e.g., "echo", to confirm that the requested
|
||||
* program is active before sending special characters.
|
||||
*/
|
||||
@Override
|
||||
public PtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode)
|
||||
throws IOException {
|
||||
return sessionUsingJavaLeader(args, env, mode);
|
||||
}
|
||||
|
||||
protected PtySession sessionUsingJavaLeader(String[] args, Map<String, String> env,
|
||||
Collection<TermMode> mode) throws IOException {
|
||||
final List<String> argsList = new ArrayList<>();
|
||||
String javaCommand =
|
||||
System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
|
||||
argsList.add(javaCommand);
|
||||
argsList.add("-cp");
|
||||
argsList.add(System.getProperty("java.class.path"));
|
||||
argsList.add(LinuxPtySessionLeader.class.getCanonicalName());
|
||||
|
||||
argsList.add(name);
|
||||
argsList.addAll(Arrays.asList(args));
|
||||
ProcessBuilder builder = new ProcessBuilder(argsList);
|
||||
if (env != null) {
|
||||
builder.environment().putAll(env);
|
||||
}
|
||||
builder.inheritIO();
|
||||
|
||||
applyMode(mode);
|
||||
|
||||
try {
|
||||
return new LocalProcessPtySession(builder.start());
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Could not start process with args " + Arrays.toString(args), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected PtySession sessionUsingPythonLeader(String[] args, Map<String, String> env)
|
||||
throws IOException {
|
||||
final List<String> argsList = new ArrayList<>();
|
||||
argsList.add("python");
|
||||
argsList.add("-m");
|
||||
argsList.add("session");
|
||||
|
||||
argsList.add(name);
|
||||
argsList.addAll(Arrays.asList(args));
|
||||
ProcessBuilder builder = new ProcessBuilder(argsList);
|
||||
if (env != null) {
|
||||
builder.environment().putAll(env);
|
||||
}
|
||||
String sourceLoc = getSourceLocationForResource("session.py").getAbsolutePath();
|
||||
//System.err.println("PYTHONPATH=" + sourceLoc);
|
||||
builder.environment().put("PYTHONPATH", sourceLoc);
|
||||
builder.inheritIO();
|
||||
|
||||
return new LocalProcessPtySession(builder.start());
|
||||
}
|
||||
|
||||
public static File getSourceLocationForResource(String name) {
|
||||
// TODO: Refactor this with SystemUtilities.getSourceLocationForClass()
|
||||
URL url = LinuxPtyChild.class.getClassLoader().getResource(name);
|
||||
String urlFile = url.getFile();
|
||||
try {
|
||||
urlFile = URLDecoder.decode(urlFile, "UTF-8");
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
// can't happen, since we know the encoding is correct
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
if ("file".equals(url.getProtocol())) {
|
||||
int packageLevel = Paths.get(name).getNameCount();
|
||||
File file = new File(urlFile);
|
||||
for (int i = 0; i < packageLevel; i++) {
|
||||
file = file.getParentFile();
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
if ("jar".equals(url.getProtocol())) {
|
||||
// Running from Jar file
|
||||
String jarPath = urlFile;
|
||||
if (!jarPath.startsWith("file:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// strip off the 'file:' prefix and the jar path suffix after the
|
||||
// '!'
|
||||
jarPath = jarPath.substring(5, jarPath.indexOf('!'));
|
||||
return new File(jarPath);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applyMode(Collection<TermMode> mode) {
|
||||
if (mode.contains(Echo.OFF)) {
|
||||
disableEcho();
|
||||
}
|
||||
}
|
||||
|
||||
private void disableEcho() {
|
||||
Termios.ByReference tmios = new Termios.ByReference();
|
||||
LIB_POSIX.tcgetattr(fd, tmios);
|
||||
tmios.c_lflag &= ~Termios.ECHO;
|
||||
LIB_POSIX.tcsetattr(fd, Termios.TCSANOW, tmios);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.linux;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import ghidra.pty.PtyEndpoint;
|
||||
|
||||
public class LinuxPtyEndpoint implements PtyEndpoint {
|
||||
protected final int fd;
|
||||
private final FdOutputStream outputStream;
|
||||
private final FdInputStream inputStream;
|
||||
|
||||
LinuxPtyEndpoint(int fd) {
|
||||
this.fd = fd;
|
||||
this.outputStream = new FdOutputStream(fd);
|
||||
this.inputStream = new FdInputStream(fd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return inputStream;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.linux;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ghidra.pty.Pty;
|
||||
import ghidra.pty.PtyFactory;
|
||||
|
||||
public class LinuxPtyFactory implements PtyFactory {
|
||||
@Override
|
||||
public Pty openpty() throws IOException {
|
||||
return LinuxPty.openpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "local (Linux)";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.linux;
|
||||
|
||||
import ghidra.pty.PtyParent;
|
||||
import ghidra.pty.linux.PosixC.Winsize;
|
||||
|
||||
public class LinuxPtyParent extends LinuxPtyEndpoint implements PtyParent {
|
||||
LinuxPtyParent(int fd) {
|
||||
super(fd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWindowSize(int cols, int rows) {
|
||||
if (cols > 0xffff || rows > 0xffff) {
|
||||
throw new IllegalArgumentException(
|
||||
"Dimensions limited to unsigned shorts. Got cols=" + cols + ",rows=" + rows);
|
||||
}
|
||||
Winsize.ByReference ws = new Winsize.ByReference();
|
||||
ws.ws_col = (short) cols;
|
||||
ws.ws_row = (short) rows;
|
||||
ws.write();
|
||||
PosixC.INSTANCE.ioctl(fd, Winsize.TIOCSWINSZ, ws.getPointer());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.linux;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class LinuxPtySessionLeader {
|
||||
private static final PosixC LIB_POSIX = PosixC.INSTANCE;
|
||||
private static final int O_RDWR = 2; // TODO: Find this in libs
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
LinuxPtySessionLeader leader = new LinuxPtySessionLeader();
|
||||
leader.parseArgs(args);
|
||||
leader.run();
|
||||
}
|
||||
|
||||
protected String ptyPath;
|
||||
protected List<String> subArgs;
|
||||
|
||||
protected void parseArgs(String[] args) {
|
||||
ptyPath = args[0];
|
||||
subArgs = List.of(args).subList(1, args.length);
|
||||
}
|
||||
|
||||
protected void run() throws Exception {
|
||||
/** This tells Linux to make this process the leader of a new session. */
|
||||
LIB_POSIX.setsid();
|
||||
|
||||
/**
|
||||
* Open the TTY. On Linux, the first TTY opened since becoming a session leader becomes the
|
||||
* session's controlling TTY. Other platforms, e.g., BSD may require an explicit IOCTL.
|
||||
*/
|
||||
int bk = -1;
|
||||
try {
|
||||
int fd = LIB_POSIX.open(ptyPath, O_RDWR, 0);
|
||||
|
||||
/** Copy stderr to a backup descriptor, in case something goes wrong. */
|
||||
int bkt = fd + 1;
|
||||
LIB_POSIX.dup2(2, bkt);
|
||||
bk = bkt;
|
||||
|
||||
/**
|
||||
* Copy the TTY fd over all standard streams. This effectively redirects the leader's
|
||||
* standard streams to the TTY.
|
||||
*/
|
||||
LIB_POSIX.dup2(fd, 0);
|
||||
LIB_POSIX.dup2(fd, 1);
|
||||
LIB_POSIX.dup2(fd, 2);
|
||||
|
||||
/**
|
||||
* At this point, we are the session leader and the named TTY is the controlling PTY.
|
||||
* Now, exec the specified image with arguments as the session leader. Recall, this
|
||||
* replaces the image of this process.
|
||||
*/
|
||||
LIB_POSIX.execv(subArgs.get(0), subArgs.toArray(new String[0]));
|
||||
}
|
||||
catch (Throwable t) {
|
||||
if (bk != -1) {
|
||||
try {
|
||||
int bkt = bk;
|
||||
LIB_POSIX.dup2(bkt, 2);
|
||||
}
|
||||
catch (Throwable t2) {
|
||||
// Catastrophic
|
||||
System.exit(-1);
|
||||
}
|
||||
}
|
||||
System.err.println("Could not execute " + subArgs.get(0) + ": " + t.getMessage());
|
||||
System.exit(127);
|
||||
}
|
||||
}
|
||||
}
|
149
Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/PosixC.java
Normal file
149
Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/PosixC.java
Normal file
|
@ -0,0 +1,149 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.linux;
|
||||
|
||||
import com.sun.jna.*;
|
||||
import com.sun.jna.Structure.FieldOrder;
|
||||
|
||||
/**
|
||||
* Interface for POSIX functions in libc
|
||||
*
|
||||
* <p>
|
||||
* The functions are not documented here. Instead see the POSIX manual pages.
|
||||
*/
|
||||
public interface PosixC extends Library {
|
||||
|
||||
@FieldOrder({ "ws_row", "ws_col", "ws_xpixel", "ws_ypixel" })
|
||||
class Winsize extends Structure {
|
||||
public static final int TIOCSWINSZ = 0x5414; // This may actually be Linux-specific
|
||||
|
||||
public short ws_row;
|
||||
public short ws_col;
|
||||
public short ws_xpixel; // Unused
|
||||
public short ws_ypixel; // Unused
|
||||
|
||||
public static class ByReference extends Winsize implements Structure.ByReference {
|
||||
}
|
||||
}
|
||||
|
||||
@FieldOrder({ "c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "c_cc", "c_ispeed",
|
||||
"c_ospeed" })
|
||||
class Termios extends Structure {
|
||||
public static final int TCSANOW = 0;
|
||||
|
||||
public static final int ECHO = 0000010; // Octal
|
||||
|
||||
public int c_iflag;
|
||||
public int c_oflag;
|
||||
public int c_cflag;
|
||||
public int c_lflag;
|
||||
|
||||
public byte c_line;
|
||||
public byte[] c_cc = new byte[32];
|
||||
|
||||
public int c_ispeed;
|
||||
public int c_ospeed;
|
||||
|
||||
public static class ByReference extends Termios implements Structure.ByReference {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The bare library without error handling
|
||||
*
|
||||
* @see Util#BARE
|
||||
*/
|
||||
PosixC BARE = Native.load("c", PosixC.class);
|
||||
|
||||
PosixC INSTANCE = new PosixC() {
|
||||
@Override
|
||||
public String strerror(int errnum) {
|
||||
return BARE.strerror(errnum);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int close(int fd) {
|
||||
return Err.checkLt0(BARE.close(fd));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(int fd, Pointer buf, int len) {
|
||||
return Err.checkLt0(BARE.read(fd, buf, len));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(int fd, Pointer buf, int i) {
|
||||
return Err.checkLt0(BARE.write(fd, buf, i));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int setsid() {
|
||||
return Err.checkLt0(BARE.setsid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int open(String path, int mode, int flags) {
|
||||
return Err.checkLt0(BARE.open(path, mode, flags));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int dup2(int oldfd, int newfd) {
|
||||
return Err.checkLt0(BARE.dup2(oldfd, newfd));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int execv(String path, String[] argv) {
|
||||
return Err.checkLt0(BARE.execv(path, argv));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int ioctl(int fd, int cmd, Pointer... args) {
|
||||
return Err.checkLt0(BARE.ioctl(fd, cmd, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int tcgetattr(int fd, Termios.ByReference termios_p) {
|
||||
return Err.checkLt0(BARE.tcgetattr(fd, termios_p));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int tcsetattr(int fd, int optional_actions, Termios.ByReference termios_p) {
|
||||
return Err.checkLt0(BARE.tcsetattr(fd, optional_actions, termios_p));
|
||||
}
|
||||
};
|
||||
|
||||
String strerror(int errnum);
|
||||
|
||||
int close(int fd);
|
||||
|
||||
int read(int fd, Pointer buf, int len);
|
||||
|
||||
int write(int fd, Pointer buf, int i);
|
||||
|
||||
int setsid();
|
||||
|
||||
int open(String path, int mode, int flags);
|
||||
|
||||
int dup2(int oldfd, int newfd);
|
||||
|
||||
int execv(String path, String[] argv);
|
||||
|
||||
int ioctl(int fd, int cmd, Pointer... args);
|
||||
|
||||
int tcgetattr(int fd, Termios.ByReference termios_p);
|
||||
|
||||
int tcsetattr(int fd, int optional_actions, Termios.ByReference termios_p);
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.linux;
|
||||
|
||||
import com.sun.jna.*;
|
||||
import com.sun.jna.ptr.IntByReference;
|
||||
|
||||
/**
|
||||
* The interface for linking to {@code openpty} via jna
|
||||
*
|
||||
* <p>
|
||||
* See the UNIX manual pages
|
||||
*/
|
||||
public interface Util extends Library {
|
||||
|
||||
/**
|
||||
* The bare library without error handling
|
||||
*
|
||||
* <p>
|
||||
* For error handling, use {@link #INSTANCE}, or check for errors manually, perhaps using
|
||||
* {@link Err}.
|
||||
*
|
||||
* <p>
|
||||
* We cannot just use {@code throws }{@link LastErrorException} to handle errors, because the
|
||||
* idiom it applies is not correct for errno on UNIX. See
|
||||
* https://man7.org/linux/man-pages/man3/errno.3.html, in particular:
|
||||
*
|
||||
* <blockquote>The value in errno is significant only when the return value of the call
|
||||
* indicated an error (i.e., -1 from most system calls; -1 or NULL from most library functions);
|
||||
* a function that succeeds is allowed to change errno.</blockquote>
|
||||
*
|
||||
* <p>
|
||||
* This actually happens on our test setup when invoking the native {@code openpty} from a
|
||||
* Docker container. It returns 0, but sets errno. JNA will incorrectly interpret this as
|
||||
* failure.
|
||||
*/
|
||||
Util BARE = Native.load("util", Util.class);
|
||||
|
||||
Util INSTANCE = new Util() {
|
||||
@Override
|
||||
public int openpty(IntByReference amaster, IntByReference aslave, Pointer name,
|
||||
Pointer termp, Pointer winp) {
|
||||
return Err.checkLt0(BARE.openpty(amaster, aslave, name, termp, winp));
|
||||
}
|
||||
};
|
||||
|
||||
int openpty(IntByReference amaster, IntByReference aslave, Pointer name, Pointer termp,
|
||||
Pointer winp);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.local;
|
||||
|
||||
import ghidra.pty.PtySession;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
/**
|
||||
* A pty session consisting of a local process and its descendants
|
||||
*/
|
||||
public class LocalProcessPtySession implements PtySession {
|
||||
private final Process process;
|
||||
|
||||
public LocalProcessPtySession(Process process) {
|
||||
this.process = process;
|
||||
Msg.info(this, "local Pty session. PID = " + process.pid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int waitExited() throws InterruptedException {
|
||||
return process.waitFor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyForcibly() {
|
||||
process.destroyForcibly();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.local;
|
||||
|
||||
import com.sun.jna.LastErrorException;
|
||||
import com.sun.jna.platform.win32.Kernel32;
|
||||
import com.sun.jna.platform.win32.WinBase;
|
||||
import com.sun.jna.ptr.IntByReference;
|
||||
|
||||
import ghidra.pty.PtySession;
|
||||
import ghidra.pty.windows.Handle;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public class LocalWindowsNativeProcessPtySession implements PtySession {
|
||||
//private final int pid;
|
||||
//private final int tid;
|
||||
private final Handle processHandle;
|
||||
//private final Handle threadHandle;
|
||||
|
||||
public LocalWindowsNativeProcessPtySession(int pid, int tid, Handle processHandle,
|
||||
Handle threadHandle) {
|
||||
//this.pid = pid;
|
||||
//this.tid = tid;
|
||||
this.processHandle = processHandle;
|
||||
//this.threadHandle = threadHandle;
|
||||
|
||||
Msg.info(this, "local Windows Pty session. PID = " + pid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int waitExited() throws InterruptedException {
|
||||
while (true) {
|
||||
switch (Kernel32.INSTANCE.WaitForSingleObject(processHandle.getNative(), -1)) {
|
||||
case Kernel32.WAIT_OBJECT_0:
|
||||
case Kernel32.WAIT_ABANDONED:
|
||||
IntByReference lpExitCode = new IntByReference();
|
||||
Kernel32.INSTANCE.GetExitCodeProcess(processHandle.getNative(), lpExitCode);
|
||||
if (lpExitCode.getValue() != WinBase.STILL_ACTIVE) {
|
||||
return lpExitCode.getValue();
|
||||
}
|
||||
case Kernel32.WAIT_TIMEOUT:
|
||||
throw new AssertionError();
|
||||
case Kernel32.WAIT_FAILED:
|
||||
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyForcibly() {
|
||||
if (!Kernel32.INSTANCE.TerminateProcess(processHandle.getNative(), 1)) {
|
||||
int error = Kernel32.INSTANCE.GetLastError();
|
||||
switch (error) {
|
||||
case Kernel32.ERROR_ACCESS_DENIED:
|
||||
/**
|
||||
* This indicates the process has already terminated. It's unclear to me whether
|
||||
* or not that is the only possible cause of this error.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
throw new LastErrorException(error);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.macos;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ghidra.pty.Pty;
|
||||
import ghidra.pty.PtyFactory;
|
||||
import ghidra.pty.linux.LinuxPty;
|
||||
|
||||
public class MacosPtyFactory implements PtyFactory {
|
||||
@Override
|
||||
public Pty openpty() throws IOException {
|
||||
return LinuxPty.openpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "local (MacOS)";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,246 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.ssh;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
|
||||
import com.jcraft.jsch.*;
|
||||
import com.jcraft.jsch.ConfigRepository.Config;
|
||||
|
||||
import docking.DockingWindowManager;
|
||||
import docking.widgets.PasswordDialog;
|
||||
import ghidra.pty.PtyFactory;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.StringUtilities;
|
||||
|
||||
public class GhidraSshPtyFactory implements PtyFactory {
|
||||
private static final String TITLE = "GDB via SSH";
|
||||
private static final int WRAP_LEN = 80;
|
||||
|
||||
public static final String DEFAULT_HOSTNAME = "localhost";
|
||||
public static final int DEFAULT_PORT = 22;
|
||||
public static final String DEFAULT_USERNAME = "user";
|
||||
public static final String DEFAULT_CONFIG_FILE = "~/.ssh/config";
|
||||
|
||||
private class RequireTTYAlwaysConfig implements Config {
|
||||
private final Config delegate;
|
||||
|
||||
public RequireTTYAlwaysConfig(Config delegate) {
|
||||
this.delegate = delegate;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHostname() {
|
||||
return delegate.getHostname();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUser() {
|
||||
return delegate.getUser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPort() {
|
||||
return delegate.getPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(String key) {
|
||||
if ("RequestTTY".equals(key)) {
|
||||
return "yes";
|
||||
}
|
||||
return delegate.getValue(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getValues(String key) {
|
||||
if ("RequestTTY".equals(key)) {
|
||||
return new String[] { "yes" };
|
||||
}
|
||||
return delegate.getValues(key);
|
||||
}
|
||||
}
|
||||
|
||||
private class RequireTTYAlwaysConfigRepo implements ConfigRepository {
|
||||
private final ConfigRepository delegate;
|
||||
|
||||
public RequireTTYAlwaysConfigRepo(ConfigRepository delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config getConfig(String host) {
|
||||
if (delegate == null) {
|
||||
return new RequireTTYAlwaysConfig(ConfigRepository.defaultConfig);
|
||||
}
|
||||
return new RequireTTYAlwaysConfig(delegate.getConfig(host));
|
||||
}
|
||||
}
|
||||
|
||||
private class GhidraUserInfo implements UserInfo {
|
||||
private String password;
|
||||
private String passphrase;
|
||||
|
||||
public String doPromptSecret(String prompt) {
|
||||
PasswordDialog dialog =
|
||||
new PasswordDialog(TITLE, "SSH", hostname, prompt, null, null);
|
||||
DockingWindowManager.showDialog(dialog);
|
||||
if (dialog.okWasPressed()) {
|
||||
return new String(dialog.getPassword());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String html(String message) {
|
||||
// TODO: I shouldn't have to do this. Why won't swing wrap?
|
||||
String wrapped = StringUtilities.wrapToWidth(message, WRAP_LEN);
|
||||
return "<html><pre>" + StringEscapeUtils.escapeHtml4(wrapped).replace("\n", "<br>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassphrase() {
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean promptPassword(String message) {
|
||||
password = doPromptSecret(message);
|
||||
return password != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean promptPassphrase(String message) {
|
||||
passphrase = doPromptSecret(message);
|
||||
return passphrase != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean promptYesNo(String message) {
|
||||
return JOptionPane.showConfirmDialog(null, html(message), TITLE,
|
||||
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) == JOptionPane.YES_OPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMessage(String message) {
|
||||
JOptionPane.showMessageDialog(null, html(message), TITLE,
|
||||
JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private String hostname = DEFAULT_HOSTNAME;
|
||||
private int port = DEFAULT_PORT;
|
||||
private String username = DEFAULT_USERNAME;
|
||||
private String configFile = DEFAULT_CONFIG_FILE;
|
||||
|
||||
private Session session;
|
||||
|
||||
public String getHostname() {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
public void setHostname(String hostname) {
|
||||
this.hostname = Objects.requireNonNull(hostname);
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = Objects.requireNonNull(username);
|
||||
}
|
||||
|
||||
public String getConfigFile() {
|
||||
return configFile;
|
||||
}
|
||||
|
||||
public void setConfigFile(String configFile) {
|
||||
this.configFile = configFile;
|
||||
}
|
||||
|
||||
protected Session connectAndAuthenticate() throws IOException {
|
||||
JSch jsch = new JSch();
|
||||
ConfigRepository configRepo = null;
|
||||
try {
|
||||
configRepo = OpenSSHConfig.parseFile(configFile);
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.warn(this, "ssh config file " + configFile + " could not be parsed.");
|
||||
// I guess the config file doesn't exist. Just go on
|
||||
}
|
||||
jsch.setConfigRepository(new RequireTTYAlwaysConfigRepo(configRepo));
|
||||
|
||||
try {
|
||||
Session session =
|
||||
jsch.getSession(username.length() == 0 ? null : username, hostname, port);
|
||||
session.setUserInfo(new GhidraUserInfo());
|
||||
session.connect();
|
||||
return session;
|
||||
}
|
||||
catch (JSchException e) {
|
||||
String message = e.getMessage();
|
||||
if (message.equals("Auth cancel")) {
|
||||
Msg.error(this, "SSH connection canceled");
|
||||
throw new CancellationException("SSH connection canceled");
|
||||
}
|
||||
else if (message.startsWith("reject HostKey")) {
|
||||
String cancelMessage = "SSH " + message;
|
||||
Msg.error(this, cancelMessage);
|
||||
throw new CancellationException(cancelMessage);
|
||||
}
|
||||
Msg.error(this, "SSH connection error");
|
||||
throw new IOException("SSH connection error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SshPty openpty() throws IOException {
|
||||
if (session == null) {
|
||||
session = connectAndAuthenticate();
|
||||
}
|
||||
try {
|
||||
return new SshPty((ChannelExec) session.openChannel("exec"));
|
||||
}
|
||||
catch (JSchException e) {
|
||||
throw new IOException("SSH connection error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "ssh:" + hostname + "(user=" + username + ",port=" + port + ")";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.ssh;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import com.jcraft.jsch.ChannelExec;
|
||||
import com.jcraft.jsch.JSchException;
|
||||
|
||||
import ghidra.pty.*;
|
||||
|
||||
public class SshPty implements Pty {
|
||||
private final ChannelExec channel;
|
||||
private final OutputStream out;
|
||||
private final InputStream in;
|
||||
|
||||
private final SshPtyParent parent;
|
||||
private final SshPtyChild child;
|
||||
|
||||
public SshPty(ChannelExec channel) throws JSchException, IOException {
|
||||
this.channel = channel;
|
||||
|
||||
out = channel.getOutputStream();
|
||||
in = channel.getInputStream();
|
||||
|
||||
parent = new SshPtyParent(channel, out, in);
|
||||
child = new SshPtyChild(channel, out, in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PtyParent getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PtyChild getChild() {
|
||||
return child;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
channel.disconnect();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.ssh;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.jcraft.jsch.ChannelExec;
|
||||
import com.jcraft.jsch.JSchException;
|
||||
|
||||
import ghidra.dbg.util.ShellUtils;
|
||||
import ghidra.pty.PtyChild;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
|
||||
private String name;
|
||||
|
||||
public SshPtyChild(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
|
||||
super(channel, outputStream, inputStream);
|
||||
}
|
||||
|
||||
private String sttyString(Collection<TermMode> mode) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (mode.contains(Echo.OFF)) {
|
||||
sb.append("-echo ");
|
||||
}
|
||||
else if (mode.contains(Echo.ON)) {
|
||||
sb.append("echo ");
|
||||
}
|
||||
if (sb.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return "stty " + sb + "&& ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public SshPtySession session(String[] args, Map<String, String> env, Collection<TermMode> mode)
|
||||
throws IOException {
|
||||
/**
|
||||
* TODO: This syntax assumes a UNIX-style shell, and even among them, this may not be
|
||||
* universal. This certainly works for my version of bash :)
|
||||
*/
|
||||
String envStr = env == null
|
||||
? ""
|
||||
: env.entrySet()
|
||||
.stream()
|
||||
.map(e -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining(" ")) +
|
||||
" ";
|
||||
String cmdStr = ShellUtils.generateLine(Arrays.asList(args));
|
||||
channel.setCommand(sttyString(mode) + envStr + cmdStr);
|
||||
try {
|
||||
channel.connect();
|
||||
}
|
||||
catch (JSchException e) {
|
||||
throw new IOException("SSH error", e);
|
||||
}
|
||||
return new SshPtySession(channel);
|
||||
}
|
||||
|
||||
private String getTtyNameAndStartNullSession(Collection<TermMode> mode) throws IOException {
|
||||
// NB. UNIX sleep is only required to support integer durations
|
||||
channel.setCommand(sttyString(mode) +
|
||||
"sh -c 'tty && ctrlc() { echo; } && trap ctrlc INT && while true; do sleep " +
|
||||
Integer.MAX_VALUE + "; done'");
|
||||
try {
|
||||
channel.connect();
|
||||
}
|
||||
catch (JSchException e) {
|
||||
throw new IOException("SSH error", e);
|
||||
}
|
||||
byte[] buf = new byte[1024]; // Should be plenty
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
int chr = inputStream.read();
|
||||
if (chr == '\n' || chr == -1) {
|
||||
return new String(buf, 0, i + 1, "UTF-8").trim();
|
||||
}
|
||||
buf[i] = (byte) chr;
|
||||
}
|
||||
throw new IOException("Expected pty name. Got " + new String(buf, 0, 1024, "UTF-8"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nullSession(Collection<TermMode> mode) throws IOException {
|
||||
if (name == null) {
|
||||
this.name = getTtyNameAndStartNullSession(mode);
|
||||
if ("".equals(name)) {
|
||||
throw new IOException("Could not determine child remote tty name");
|
||||
}
|
||||
}
|
||||
Msg.debug(this, "Remote SSH pty: " + name);
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
throw new UnsupportedOperationException("The child is not local");
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
throw new UnsupportedOperationException("The child is not local");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.ssh;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import com.jcraft.jsch.ChannelExec;
|
||||
|
||||
import ghidra.pty.PtyEndpoint;
|
||||
|
||||
public class SshPtyEndpoint implements PtyEndpoint {
|
||||
protected final ChannelExec channel;
|
||||
protected final OutputStream outputStream;
|
||||
protected final InputStream inputStream;
|
||||
|
||||
public SshPtyEndpoint(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
|
||||
this.channel = channel;
|
||||
this.outputStream = outputStream;
|
||||
this.inputStream = inputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return inputStream;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.ssh;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import com.jcraft.jsch.ChannelExec;
|
||||
|
||||
import ghidra.pty.PtyParent;
|
||||
|
||||
public class SshPtyParent extends SshPtyEndpoint implements PtyParent {
|
||||
public SshPtyParent(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
|
||||
super(channel, outputStream, inputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWindowSize(int cols, int rows) {
|
||||
channel.setPtySize(cols, rows, 0, 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.ssh;
|
||||
|
||||
import com.jcraft.jsch.Channel;
|
||||
|
||||
import ghidra.pty.PtySession;
|
||||
|
||||
public class SshPtySession implements PtySession {
|
||||
|
||||
private final Channel channel;
|
||||
|
||||
public SshPtySession(Channel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int waitExited() throws InterruptedException {
|
||||
// Doesn't look like there's a clever way to wait. So do the spin sleep :(
|
||||
while (!channel.isEOF()) {
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
// NB. May not be available
|
||||
return channel.getExitStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyForcibly() {
|
||||
channel.disconnect();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,476 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
// TODO: I shouldn't have to do any of this.
|
||||
public class AnsiBufferedInputStream extends InputStream {
|
||||
private static final Charset WINDOWS_1252 = Charset.forName("windows-1252");
|
||||
|
||||
private enum Mode {
|
||||
CHARS,
|
||||
ESC,
|
||||
CSI,
|
||||
CSI_p,
|
||||
CSI_Q,
|
||||
OSC,
|
||||
WINDOW_TITLE,
|
||||
WINDOW_TITLE_ESC;
|
||||
}
|
||||
|
||||
private final InputStream in;
|
||||
|
||||
private int countIn = 0;
|
||||
|
||||
private ByteBuffer lineBaked = ByteBuffer.allocate(Short.MAX_VALUE);
|
||||
private ByteBuffer lineBuf = ByteBuffer.allocate(Short.MAX_VALUE);
|
||||
private ByteBuffer escBuf = ByteBuffer.allocate(1024);
|
||||
private ByteBuffer titleBuf = ByteBuffer.allocate(255);
|
||||
|
||||
private Mode mode = Mode.CHARS;
|
||||
|
||||
public AnsiBufferedInputStream(InputStream in) {
|
||||
if (in instanceof HandleInputStream) {
|
||||
// Spare myself the 1-by-1 native calls
|
||||
in = new BufferedInputStream(in);
|
||||
}
|
||||
this.in = in;
|
||||
|
||||
lineBuf.limit(0);
|
||||
lineBaked.limit(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (lineBaked.hasRemaining()) {
|
||||
return lineBaked.get();
|
||||
}
|
||||
if (readUntilBaked() < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (lineBaked.hasRemaining()) {
|
||||
return lineBaked.get();
|
||||
}
|
||||
return -1; // EOF
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (!lineBaked.hasRemaining()) {
|
||||
if (readUntilBaked() < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
int read = Math.min(lineBaked.remaining(), len);
|
||||
lineBaked.get(b, off, read);
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
in.close();
|
||||
super.close();
|
||||
}
|
||||
|
||||
protected int readUntilBaked() throws IOException {
|
||||
while (!lineBaked.hasRemaining()) {
|
||||
if (processNext() < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!lineBaked.hasRemaining()) {
|
||||
return -1;
|
||||
}
|
||||
return lineBaked.remaining();
|
||||
}
|
||||
|
||||
protected void printDebugChar(byte c) {
|
||||
if (0x20 <= c && c <= 0x7f) {
|
||||
System.err.print(new String(new byte[] { c }));
|
||||
}
|
||||
else {
|
||||
System.err.print(String.format("<%02x>", c & 0xff));
|
||||
}
|
||||
}
|
||||
|
||||
protected int processNext() throws IOException {
|
||||
int ci = in.read();
|
||||
if (ci == -1) {
|
||||
return -1;
|
||||
}
|
||||
byte c = (byte) ci;
|
||||
// printDebugChar(c);
|
||||
switch (mode) {
|
||||
case CHARS:
|
||||
processChars(c);
|
||||
break;
|
||||
case ESC:
|
||||
processEsc(c);
|
||||
break;
|
||||
case CSI:
|
||||
processCsi(c);
|
||||
break;
|
||||
case CSI_p:
|
||||
processCsiParamOrCommand(c);
|
||||
break;
|
||||
case CSI_Q:
|
||||
processCsiQ(c);
|
||||
break;
|
||||
case OSC:
|
||||
processOsc(c);
|
||||
break;
|
||||
case WINDOW_TITLE:
|
||||
processWindowTitle(c);
|
||||
break;
|
||||
case WINDOW_TITLE_ESC:
|
||||
processWindowTitleEsc(c);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
countIn++;
|
||||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
* There's not really a good way to know if any trailing space was intentional. For GDB/MI, that
|
||||
* doesn't really matter.
|
||||
*/
|
||||
protected int guessEnd() {
|
||||
for (int i = lineBuf.limit() - 1; i >= 0; i--) {
|
||||
byte c = lineBuf.get(i);
|
||||
if (c != 0x20 && c != 0) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected void bakeLine() {
|
||||
lineBuf.position(0);
|
||||
lineBuf.limit(guessEnd() + 1);
|
||||
lineBuf.put(lineBuf.limit() - 1, (byte) '\n');
|
||||
ByteBuffer temp = lineBaked;
|
||||
lineBaked = lineBuf;
|
||||
lineBuf = temp;
|
||||
lineBuf.clear();
|
||||
Arrays.fill(lineBuf.array(), (byte) 0);
|
||||
lineBuf.limit(0);
|
||||
}
|
||||
|
||||
protected void appendChar(byte c) {
|
||||
int limit = lineBuf.limit();
|
||||
if (lineBuf.position() == limit) {
|
||||
lineBuf.limit(limit + 1);
|
||||
}
|
||||
lineBuf.put(c);
|
||||
}
|
||||
|
||||
protected void processChars(byte c) {
|
||||
switch (c) {
|
||||
case 0x08:
|
||||
if (lineBuf.get(lineBuf.position() - 1) == ' ') {
|
||||
lineBuf.position(lineBuf.position() - 1);
|
||||
}
|
||||
break;
|
||||
case '\n':
|
||||
//appendChar(c);
|
||||
bakeLine();
|
||||
break;
|
||||
case 0x1b:
|
||||
mode = Mode.ESC;
|
||||
break;
|
||||
default:
|
||||
appendChar(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected void processEsc(byte c) {
|
||||
switch (c) {
|
||||
case '[':
|
||||
mode = Mode.CSI;
|
||||
break;
|
||||
case ']':
|
||||
mode = Mode.OSC;
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Saw 'ESC " + c + "' at " + countIn);
|
||||
}
|
||||
}
|
||||
|
||||
protected void processCsi(byte c) {
|
||||
switch (c) {
|
||||
default:
|
||||
processCsiParamOrCommand(c);
|
||||
break;
|
||||
case '?':
|
||||
mode = Mode.CSI_Q;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected void processCsiParamOrCommand(byte c) {
|
||||
switch (c) {
|
||||
default:
|
||||
escBuf.put(c);
|
||||
break;
|
||||
case 'A':
|
||||
execCursorUp();
|
||||
mode = Mode.CHARS;
|
||||
break;
|
||||
case 'B':
|
||||
execCursorDown();
|
||||
mode = Mode.CHARS;
|
||||
break;
|
||||
case 'C':
|
||||
execCursorForward();
|
||||
mode = Mode.CHARS;
|
||||
break;
|
||||
case 'D':
|
||||
execCursorBackward();
|
||||
mode = Mode.CHARS;
|
||||
break;
|
||||
case 'H':
|
||||
execCursorPosition();
|
||||
mode = Mode.CHARS;
|
||||
break;
|
||||
case 'J':
|
||||
execEraseInDisplay();
|
||||
mode = Mode.CHARS;
|
||||
break;
|
||||
case 'K':
|
||||
execEraseInLine();
|
||||
mode = Mode.CHARS;
|
||||
break;
|
||||
case 'X':
|
||||
execEraseCharacter();
|
||||
mode = Mode.CHARS;
|
||||
break;
|
||||
case 'm':
|
||||
execSetGraphicsRendition();
|
||||
mode = Mode.CHARS;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected void processCsiQ(byte c) {
|
||||
String buf;
|
||||
switch (c) {
|
||||
default:
|
||||
escBuf.put(c);
|
||||
break;
|
||||
case 'h':
|
||||
buf = readAndClearEscBuf();
|
||||
if ("12".equals(buf)) {
|
||||
execTextCursorEnableBlinking();
|
||||
escBuf.clear();
|
||||
mode = Mode.CHARS;
|
||||
}
|
||||
else if ("25".equals(buf)) {
|
||||
execTextCursorEnableModeShow();
|
||||
escBuf.clear();
|
||||
mode = Mode.CHARS;
|
||||
}
|
||||
else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
break;
|
||||
case 'l':
|
||||
buf = readAndClearEscBuf();
|
||||
if ("12".equals(buf)) {
|
||||
execTextCursorDisableBlinking();
|
||||
escBuf.clear();
|
||||
mode = Mode.CHARS;
|
||||
}
|
||||
else if ("25".equals(buf)) {
|
||||
execTextCursorDisableModeShow();
|
||||
escBuf.clear();
|
||||
mode = Mode.CHARS;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected void processOsc(byte c) {
|
||||
switch (c) {
|
||||
default:
|
||||
escBuf.put(c);
|
||||
break;
|
||||
case ';':
|
||||
if (Set.of("0", "2").contains(readAndClearEscBuf())) {
|
||||
mode = Mode.WINDOW_TITLE;
|
||||
escBuf.clear();
|
||||
break;
|
||||
}
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
protected void processWindowTitle(byte c) {
|
||||
switch (c) {
|
||||
default:
|
||||
titleBuf.put(c);
|
||||
break;
|
||||
case 0x07: // bell, even though MSDN says longer form preferred
|
||||
execSetWindowTitle();
|
||||
mode = Mode.CHARS;
|
||||
break;
|
||||
case 0x1b:
|
||||
mode = Mode.WINDOW_TITLE_ESC;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected void processWindowTitleEsc(byte c) {
|
||||
switch (c) {
|
||||
case '\\':
|
||||
execSetWindowTitle();
|
||||
mode = Mode.CHARS;
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Saw <ST> ... ESC " + c + " at " + countIn);
|
||||
}
|
||||
}
|
||||
|
||||
protected String readAndClear(ByteBuffer buf) {
|
||||
buf.flip();
|
||||
String result = new String(buf.array(), buf.position(), buf.remaining(), WINDOWS_1252);
|
||||
buf.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
protected String readAndClearEscBuf() {
|
||||
return readAndClear(escBuf);
|
||||
}
|
||||
|
||||
protected int parseNumericBuffer() {
|
||||
String numeric = readAndClearEscBuf();
|
||||
if (numeric.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int result = Integer.parseInt(numeric);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected int[] parseNumericListBuffer() {
|
||||
String numericList = readAndClearEscBuf();
|
||||
if (numericList.isEmpty()) {
|
||||
return new int[] {};
|
||||
}
|
||||
return Stream.of(numericList.split(";"))
|
||||
.mapToInt(Integer::parseInt)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
protected void execCursorUp() {
|
||||
throw new UnsupportedOperationException("Cursor Up");
|
||||
}
|
||||
|
||||
protected void execCursorDown() {
|
||||
throw new UnsupportedOperationException("Cursor Down");
|
||||
}
|
||||
|
||||
protected void setPosition(int newPosition) {
|
||||
if (lineBuf.limit() < newPosition) {
|
||||
lineBuf.limit(newPosition);
|
||||
}
|
||||
lineBuf.position(newPosition);
|
||||
}
|
||||
|
||||
protected void execCursorForward() {
|
||||
int delta = parseNumericBuffer();
|
||||
setPosition(lineBuf.position() + delta);
|
||||
}
|
||||
|
||||
protected void execCursorBackward() {
|
||||
int delta = parseNumericBuffer();
|
||||
lineBuf.position(lineBuf.position() - delta);
|
||||
}
|
||||
|
||||
protected void execCursorPosition() {
|
||||
int[] yx = parseNumericListBuffer();
|
||||
if (yx.length == 0) {
|
||||
lineBuf.position(0);
|
||||
return;
|
||||
}
|
||||
if (yx.length != 2) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
if (yx[0] != 1) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
lineBuf.position(yx[1] - 1);
|
||||
}
|
||||
|
||||
protected void execTextCursorEnableBlinking() {
|
||||
// Don't care
|
||||
}
|
||||
|
||||
protected void execTextCursorDisableBlinking() {
|
||||
// Don't care
|
||||
}
|
||||
|
||||
protected void execTextCursorEnableModeShow() {
|
||||
// Don't care
|
||||
}
|
||||
|
||||
protected void execTextCursorDisableModeShow() {
|
||||
// Don't care
|
||||
}
|
||||
|
||||
protected void execEraseInDisplay() {
|
||||
// Because I have only one line, right?
|
||||
execEraseInLine();
|
||||
}
|
||||
|
||||
protected void execEraseInLine() {
|
||||
switch (parseNumericBuffer()) {
|
||||
case 0:
|
||||
Arrays.fill(lineBuf.array(), lineBuf.position(), lineBuf.capacity(), (byte) 0);
|
||||
break;
|
||||
case 1:
|
||||
Arrays.fill(lineBuf.array(), 0, lineBuf.position() + 1, (byte) 0);
|
||||
break;
|
||||
case 2:
|
||||
Arrays.fill(lineBuf.array(), (byte) 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected void execEraseCharacter() {
|
||||
int count = parseNumericBuffer();
|
||||
Arrays.fill(lineBuf.array(), lineBuf.position(), lineBuf.position() + count, (byte) ' ');
|
||||
}
|
||||
|
||||
protected void execSetGraphicsRendition() {
|
||||
// TODO: Maybe echo these or provide callbacks
|
||||
// Otherwise, don't care
|
||||
escBuf.clear();
|
||||
}
|
||||
|
||||
protected void execSetWindowTitle() {
|
||||
// Msg.info(this, "Title: " + readAndClear(titleBuf));
|
||||
// TODO: Maybe a callback. Otherwise, don't care
|
||||
titleBuf.clear();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.sun.jna.platform.win32.Kernel32;
|
||||
import com.sun.jna.platform.win32.WinDef.DWORD;
|
||||
import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
|
||||
|
||||
import ghidra.pty.*;
|
||||
import ghidra.pty.windows.jna.ConsoleApiNative;
|
||||
import ghidra.pty.windows.jna.ConsoleApiNative.COORD;
|
||||
|
||||
import com.sun.jna.platform.win32.COM.COMUtils;
|
||||
|
||||
public class ConPty implements Pty {
|
||||
static final DWORD DW_ZERO = new DWORD(0);
|
||||
static final DWORD DW_ONE = new DWORD(1);
|
||||
static final DWORD PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = new DWORD(0x20016);
|
||||
static final DWORD EXTENDED_STARTUPINFO_PRESENT =
|
||||
new DWORD(Kernel32.EXTENDED_STARTUPINFO_PRESENT);
|
||||
private static final COORD SIZE = new COORD();
|
||||
static {
|
||||
SIZE.X = Short.MAX_VALUE;
|
||||
SIZE.Y = 1;
|
||||
}
|
||||
|
||||
private final Pipe pipeToChild;
|
||||
private final Pipe pipeFromChild;
|
||||
private final PseudoConsoleHandle pseudoConsoleHandle;
|
||||
private boolean closed = false;
|
||||
|
||||
private final ConPtyParent parent;
|
||||
private final ConPtyChild child;
|
||||
|
||||
public static ConPty openpty() {
|
||||
// Create communication channels
|
||||
|
||||
Pipe pipeToChild = Pipe.createPipe();
|
||||
Pipe pipeFromChild = Pipe.createPipe();
|
||||
|
||||
// Close the child-connected ends after creating the pseudoconsole
|
||||
// Keep the parent-connected ends, because we're the parent
|
||||
|
||||
HANDLEByReference lphPC = new HANDLEByReference();
|
||||
|
||||
COMUtils.checkRC(ConsoleApiNative.INSTANCE.CreatePseudoConsole(
|
||||
SIZE,
|
||||
pipeToChild.getReadHandle().getNative(),
|
||||
pipeFromChild.getWriteHandle().getNative(),
|
||||
DW_ZERO,
|
||||
lphPC));
|
||||
|
||||
return new ConPty(pipeToChild, pipeFromChild, new PseudoConsoleHandle(lphPC.getValue()));
|
||||
}
|
||||
|
||||
public ConPty(Pipe pipeToChild, Pipe pipeFromChild, PseudoConsoleHandle pseudoConsoleHandle) {
|
||||
this.pipeToChild = pipeToChild;
|
||||
this.pipeFromChild = pipeFromChild;
|
||||
this.pseudoConsoleHandle = pseudoConsoleHandle;
|
||||
|
||||
// TODO: See if this can all be combined with named pipes.
|
||||
// Would be nice if that's sufficient to support new-ui
|
||||
|
||||
this.parent = new ConPtyParent(pipeToChild.getWriteHandle(), pipeFromChild.getReadHandle());
|
||||
this.child = new ConPtyChild(pipeFromChild.getWriteHandle(), pipeToChild.getReadHandle(),
|
||||
pseudoConsoleHandle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PtyParent getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PtyChild getChild() {
|
||||
return child;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
pseudoConsoleHandle.close();
|
||||
pipeToChild.close();
|
||||
pipeFromChild.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
closed = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
import com.sun.jna.*;
|
||||
import com.sun.jna.platform.win32.Kernel32;
|
||||
import com.sun.jna.platform.win32.WinBase;
|
||||
import com.sun.jna.platform.win32.WinBase.PROCESS_INFORMATION;
|
||||
import com.sun.jna.platform.win32.WinDef.*;
|
||||
import com.sun.jna.platform.win32.WinNT.HANDLE;
|
||||
|
||||
import ghidra.dbg.util.ShellUtils;
|
||||
import ghidra.pty.PtyChild;
|
||||
import ghidra.pty.local.LocalWindowsNativeProcessPtySession;
|
||||
import ghidra.pty.windows.jna.ConsoleApiNative;
|
||||
import ghidra.pty.windows.jna.ConsoleApiNative.STARTUPINFOEX;
|
||||
|
||||
public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
|
||||
private final Handle pseudoConsoleHandle;
|
||||
|
||||
public ConPtyChild(Handle writeHandle, Handle readHandle, Handle pseudoConsoleHandle) {
|
||||
super(writeHandle, readHandle);
|
||||
this.pseudoConsoleHandle = pseudoConsoleHandle;
|
||||
}
|
||||
|
||||
protected STARTUPINFOEX prepareStartupInfo() {
|
||||
STARTUPINFOEX si = new STARTUPINFOEX();
|
||||
si.StartupInfo.cb = new DWORD(si.size());
|
||||
si.StartupInfo.hStdOutput = new HANDLE();
|
||||
si.StartupInfo.hStdError = new HANDLE();
|
||||
si.StartupInfo.hStdInput = new HANDLE();
|
||||
si.StartupInfo.dwFlags = WinBase.STARTF_USESTDHANDLES;
|
||||
|
||||
// Discover the size required for the thread attrs list and allocate
|
||||
UINTByReference bytesRequired = new UINTByReference();
|
||||
// NB. This will "fail." See Remarks on MSDN.
|
||||
ConsoleApiNative.INSTANCE.InitializeProcThreadAttributeList(
|
||||
null, ConPty.DW_ONE, ConPty.DW_ZERO, bytesRequired);
|
||||
// NB. Memory frees itself in .finalize()
|
||||
si.lpAttributeList = new Memory(bytesRequired.getValue().intValue());
|
||||
// Initialize it
|
||||
if (!ConsoleApiNative.INSTANCE.InitializeProcThreadAttributeList(
|
||||
si.lpAttributeList, ConPty.DW_ONE, ConPty.DW_ZERO, bytesRequired)
|
||||
.booleanValue()) {
|
||||
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
|
||||
}
|
||||
|
||||
// Set the pseudoconsole information into the list
|
||||
if (!ConsoleApiNative.INSTANCE.UpdateProcThreadAttribute(
|
||||
si.lpAttributeList, ConPty.DW_ZERO,
|
||||
ConPty.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
|
||||
new PVOID(pseudoConsoleHandle.getNative().getPointer()),
|
||||
new DWORD(Native.POINTER_SIZE),
|
||||
null, null).booleanValue()) {
|
||||
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
|
||||
}
|
||||
|
||||
return si;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalWindowsNativeProcessPtySession session(String[] args, Map<String, String> env,
|
||||
Collection<TermMode> mode) throws IOException {
|
||||
/**
|
||||
* TODO: How to incorporate environment into CreateProcess?
|
||||
*
|
||||
* TODO: How to control local echo?
|
||||
*/
|
||||
|
||||
STARTUPINFOEX si = prepareStartupInfo();
|
||||
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
||||
|
||||
if (!ConsoleApiNative.INSTANCE.CreateProcessW(
|
||||
null /*lpApplicationName*/,
|
||||
new WString(ShellUtils.generateLine(Arrays.asList(args))),
|
||||
null /*lpProcessAttributes*/,
|
||||
null /*lpThreadAttributes*/,
|
||||
false /*bInheritHandles*/,
|
||||
ConPty.EXTENDED_STARTUPINFO_PRESENT /*dwCreationFlags*/,
|
||||
null /*lpEnvironment*/,
|
||||
null /*lpCurrentDirectory*/,
|
||||
si /*lpStartupInfo*/,
|
||||
pi /*lpProcessInformation*/).booleanValue()) {
|
||||
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
|
||||
}
|
||||
|
||||
return new LocalWindowsNativeProcessPtySession(pi.dwProcessId.intValue(),
|
||||
pi.dwThreadId.intValue(),
|
||||
new Handle(pi.hProcess), new Handle(pi.hThread));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nullSession(Collection<TermMode> mode) throws IOException {
|
||||
throw new UnsupportedOperationException("ConPTY does not have a name");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import ghidra.pty.PtyEndpoint;
|
||||
|
||||
public class ConPtyEndpoint implements PtyEndpoint {
|
||||
protected InputStream inputStream;
|
||||
protected OutputStream outputStream;
|
||||
|
||||
public ConPtyEndpoint(Handle writeHandle, Handle readHandle) {
|
||||
this.inputStream = new HandleInputStream(readHandle);
|
||||
this.outputStream = new HandleOutputStream(writeHandle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return inputStream;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ghidra.pty.Pty;
|
||||
import ghidra.pty.PtyFactory;
|
||||
|
||||
public class ConPtyFactory implements PtyFactory {
|
||||
@Override
|
||||
public Pty openpty() throws IOException {
|
||||
return ConPty.openpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "local (Windows)";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import ghidra.pty.PtyParent;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public class ConPtyParent extends ConPtyEndpoint implements PtyParent {
|
||||
public ConPtyParent(Handle writeHandle, Handle readHandle) {
|
||||
super(writeHandle, readHandle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWindowSize(int rows, int cols) {
|
||||
Msg.error(this, "Pty window size not implemented on Windows");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import java.lang.ref.Cleaner;
|
||||
|
||||
import com.sun.jna.LastErrorException;
|
||||
import com.sun.jna.platform.win32.Kernel32;
|
||||
import com.sun.jna.platform.win32.WinNT.HANDLE;
|
||||
|
||||
public class Handle implements AutoCloseable {
|
||||
private static final Cleaner CLEANER = Cleaner.create();
|
||||
|
||||
protected static class State implements Runnable {
|
||||
protected final HANDLE handle;
|
||||
|
||||
protected State(HANDLE handle) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!Kernel32.INSTANCE.CloseHandle(handle)) {
|
||||
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final State state;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public Handle(HANDLE handle) {
|
||||
this.state = newState(handle);
|
||||
this.cleanable = CLEANER.register(this, state);
|
||||
}
|
||||
|
||||
protected State newState(HANDLE handle) {
|
||||
return new State(handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
cleanable.clean();
|
||||
}
|
||||
|
||||
public HANDLE getNative() {
|
||||
HANDLE handle = state.handle;
|
||||
if (handle == null) {
|
||||
throw new IllegalStateException("This handle is no longer valid");
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import com.sun.jna.LastErrorException;
|
||||
import com.sun.jna.platform.win32.Kernel32;
|
||||
import com.sun.jna.ptr.IntByReference;
|
||||
|
||||
public class HandleInputStream extends InputStream {
|
||||
private final Handle handle;
|
||||
private boolean closed = false;
|
||||
|
||||
HandleInputStream(Handle handle) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int read() throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException("Stream closed");
|
||||
}
|
||||
byte[] buf = new byte[1];
|
||||
if (0 == read(buf)) {
|
||||
return -1;
|
||||
}
|
||||
return buf[0] & 0x0FF;
|
||||
}
|
||||
|
||||
protected void waitPipeConnected() {
|
||||
if (Kernel32.INSTANCE.ConnectNamedPipe(handle.getNative(), null)) {
|
||||
return; // We waited, and now we're connected
|
||||
}
|
||||
int error = Kernel32.INSTANCE.GetLastError();
|
||||
if (error == Kernel32.ERROR_PIPE_CONNECTED) {
|
||||
return; // We got the connection before we waited. OK
|
||||
}
|
||||
throw new LastErrorException(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int read(byte[] b) throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException("Stream closed");
|
||||
}
|
||||
IntByReference dwRead = new IntByReference();
|
||||
|
||||
while (!Kernel32.INSTANCE.ReadFile(handle.getNative(), b, b.length, dwRead, null)) {
|
||||
int error = Kernel32.INSTANCE.GetLastError();
|
||||
switch (error) {
|
||||
case Kernel32.ERROR_BROKEN_PIPE:
|
||||
return -1;
|
||||
case Kernel32.ERROR_PIPE_LISTENING:
|
||||
/**
|
||||
* Well, we know we're dealing with a listening pipe, now. Wait for a client,
|
||||
* then try reading again.
|
||||
*/
|
||||
waitPipeConnected();
|
||||
continue;
|
||||
}
|
||||
throw new IOException("Could not read",
|
||||
new LastErrorException(error));
|
||||
}
|
||||
return dwRead.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int read(byte[] b, int off, int len) throws IOException {
|
||||
byte[] temp = new byte[len];
|
||||
int read = read(temp);
|
||||
System.arraycopy(temp, 0, b, off, read);
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
closed = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.sun.jna.LastErrorException;
|
||||
import com.sun.jna.platform.win32.Kernel32;
|
||||
import com.sun.jna.ptr.IntByReference;
|
||||
|
||||
public class HandleOutputStream extends OutputStream {
|
||||
private final Handle handle;
|
||||
private boolean closed = false;
|
||||
|
||||
public HandleOutputStream(Handle handle) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(int b) throws IOException {
|
||||
write(new byte[] { (byte) b });
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] b) throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException("Stream closed");
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
do {
|
||||
IntByReference dwWritten = new IntByReference();
|
||||
if (!Kernel32.INSTANCE.WriteFile(handle.getNative(), b, b.length, dwWritten, null)) {
|
||||
throw new IOException("Could not write",
|
||||
new LastErrorException(Kernel32.INSTANCE.GetLastError()));
|
||||
}
|
||||
total += dwWritten.getValue();
|
||||
}
|
||||
while (total < b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] b, int off, int len) throws IOException {
|
||||
// Abstraction of Windows API given by JNA doesn't have offset
|
||||
// In C, could add offset to lpBuffer, but not here :(
|
||||
write(Arrays.copyOfRange(b, off, off + len));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
closed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this handle has buffered output
|
||||
*
|
||||
* <p>
|
||||
* Windows can get touchy when trying to flush handles that are not actually buffered. If the
|
||||
* wrapped handle is not buffered, then this method must return false, otherwise, any attempt to
|
||||
* flush this stream will result in {@code ERROR_INVALID_HANDLE}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
protected boolean isBuffered() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
if (!isBuffered()) {
|
||||
return;
|
||||
}
|
||||
if (!(Kernel32.INSTANCE.FlushFileBuffers(handle.getNative()))) {
|
||||
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import com.sun.jna.LastErrorException;
|
||||
import com.sun.jna.platform.win32.Kernel32;
|
||||
import com.sun.jna.platform.win32.WinBase.SECURITY_ATTRIBUTES;
|
||||
import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
|
||||
|
||||
public class Pipe {
|
||||
public static Pipe createPipe() {
|
||||
HANDLEByReference pRead = new HANDLEByReference();
|
||||
HANDLEByReference pWrite = new HANDLEByReference();
|
||||
|
||||
if (!Kernel32.INSTANCE.CreatePipe(pRead, pWrite, new SECURITY_ATTRIBUTES(), 0)) {
|
||||
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
|
||||
}
|
||||
return new Pipe(new Handle(pRead.getValue()), new Handle(pWrite.getValue()));
|
||||
}
|
||||
|
||||
private final Handle readHandle;
|
||||
private final Handle writeHandle;
|
||||
|
||||
private Pipe(Handle read, Handle write) {
|
||||
this.readHandle = read;
|
||||
this.writeHandle = write;
|
||||
}
|
||||
|
||||
public Handle getReadHandle() {
|
||||
return readHandle;
|
||||
}
|
||||
|
||||
public Handle getWriteHandle() {
|
||||
return writeHandle;
|
||||
}
|
||||
|
||||
public void close() throws Exception {
|
||||
writeHandle.close();
|
||||
readHandle.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import com.sun.jna.platform.win32.WinNT.HANDLE;
|
||||
|
||||
import ghidra.pty.windows.jna.ConsoleApiNative;
|
||||
|
||||
public class PseudoConsoleHandle extends Handle {
|
||||
|
||||
protected static class PseudoConsoleState extends State {
|
||||
public PseudoConsoleState(HANDLE handle) {
|
||||
super(handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
ConsoleApiNative.INSTANCE.ClosePseudoConsole(handle);
|
||||
}
|
||||
}
|
||||
|
||||
public PseudoConsoleHandle(HANDLE handle) {
|
||||
super(handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected State newState(HANDLE handle) {
|
||||
return new PseudoConsoleState(handle);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows.jna;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.sun.jna.*;
|
||||
import com.sun.jna.platform.win32.WinBase;
|
||||
import com.sun.jna.platform.win32.WinDef.*;
|
||||
import com.sun.jna.platform.win32.WinNT.*;
|
||||
import com.sun.jna.win32.StdCallLibrary;
|
||||
|
||||
public interface ConsoleApiNative extends StdCallLibrary {
|
||||
ConsoleApiNative INSTANCE = Native.load("Kernel32.dll", ConsoleApiNative.class);
|
||||
BOOL FAIL = new BOOL(false);
|
||||
|
||||
BOOL CreatePipe(HANDLEByReference hReadPipe, HANDLEByReference hWritePipe,
|
||||
SECURITY_ATTRIBUTES.ByReference lpPipeAttributes, DWORD nSize);
|
||||
|
||||
HRESULT CreatePseudoConsole(COORD.ByValue size, HANDLE hInput, HANDLE hOutput,
|
||||
DWORD dwFlags,
|
||||
HANDLEByReference phPC);
|
||||
|
||||
void ClosePseudoConsole(HANDLE hPC);
|
||||
|
||||
BOOL InitializeProcThreadAttributeList(Pointer lpAttributeList,
|
||||
DWORD dwAttributeCount, DWORD dwFlags, UINTByReference lpSize);
|
||||
|
||||
BOOL UpdateProcThreadAttribute(
|
||||
Pointer lpAttributeList,
|
||||
DWORD dwFlags,
|
||||
DWORD Attribute,
|
||||
PVOID lpValue,
|
||||
DWORD cbSize,
|
||||
PVOID lpPreviousValue,
|
||||
ULONGLONGByReference lpReturnSize);
|
||||
|
||||
BOOL CreateProcessW(
|
||||
WString lpApplicationName,
|
||||
WString lpCommandLine,
|
||||
WinBase.SECURITY_ATTRIBUTES lpProcessAttributes,
|
||||
WinBase.SECURITY_ATTRIBUTES lpThreadAttributes,
|
||||
boolean bInheritHandles,
|
||||
DWORD dwCreationFlags,
|
||||
Pointer lpEnvironment,
|
||||
WString lpCurrentDirectory,
|
||||
STARTUPINFOEX lpStartupInfo,
|
||||
WinBase.PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
/*
|
||||
BOOL GetConsoleMode(
|
||||
HANDLE hConsoleMode,
|
||||
DWORDByReference dwMode);
|
||||
|
||||
BOOL CreateProcessWithTokenW(
|
||||
HANDLE hToken,
|
||||
DWORD dwLogonFlags,
|
||||
WString lpApplicationName,
|
||||
WString lpCommandLine,
|
||||
DWORD dwCreationFlags,
|
||||
Pointer lpEnvironment,
|
||||
WString lpCurrentDirectory,
|
||||
STARTUPINFOEX lpStartupInfo,
|
||||
WinBase.PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
BOOL LogonUserW(
|
||||
WString lpUsername,
|
||||
WString lpDomain,
|
||||
WString lpPassword,
|
||||
DWORD dwLogonType,
|
||||
DWORD dwLogonProvider,
|
||||
HANDLEByReference phToken);
|
||||
*/
|
||||
|
||||
public static class COORD extends Structure implements Structure.ByValue {
|
||||
public static class ByReference extends COORD
|
||||
implements Structure.ByReference {
|
||||
}
|
||||
|
||||
public static final List<String> FIELDS = createFieldsOrder("X", "Y");
|
||||
|
||||
public short X;
|
||||
public short Y;
|
||||
|
||||
@Override
|
||||
protected List<String> getFieldOrder() {
|
||||
return FIELDS;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SECURITY_ATTRIBUTES extends Structure {
|
||||
public static class ByReference extends SECURITY_ATTRIBUTES
|
||||
implements Structure.ByReference {
|
||||
}
|
||||
|
||||
public static final List<String> FIELDS = createFieldsOrder(
|
||||
"nLength", "lpSecurityDescriptor", "bInheritedHandle");
|
||||
|
||||
public DWORD nLength;
|
||||
public ULONGLONG lpSecurityDescriptor;
|
||||
public BOOL bInheritedHandle;
|
||||
|
||||
@Override
|
||||
protected List<String> getFieldOrder() {
|
||||
return FIELDS;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PROC_THREAD_ATTRIBUTE_LIST extends Structure {
|
||||
public static class ByReference extends PROC_THREAD_ATTRIBUTE_LIST
|
||||
implements Structure.ByReference {
|
||||
}
|
||||
|
||||
public static final List<String> FIELDS = createFieldsOrder(
|
||||
"dwFlags", "Size", "Count", "Reserved", "Unknown");
|
||||
|
||||
public DWORD dwFlags;
|
||||
public ULONG Size;
|
||||
public ULONG Count;
|
||||
public ULONG Reserved;
|
||||
public ULONGLONG Unknown;
|
||||
//public PROC_THREAD_ATTRIBUTE_ENTRY Entries[0];
|
||||
|
||||
@Override
|
||||
protected List<String> getFieldOrder() {
|
||||
return FIELDS;
|
||||
}
|
||||
}
|
||||
|
||||
public static class STARTUPINFOEX extends Structure {
|
||||
public static class ByReference extends STARTUPINFOEX
|
||||
implements Structure.ByReference {
|
||||
}
|
||||
|
||||
public static final List<String> FIELDS = createFieldsOrder(
|
||||
"StartupInfo", "lpAttributeList");
|
||||
|
||||
public WinBase.STARTUPINFO StartupInfo;
|
||||
public Pointer lpAttributeList;
|
||||
|
||||
@Override
|
||||
protected List<String> getFieldOrder() {
|
||||
return FIELDS;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class AbstractPtyTest {
|
||||
public Thread pump(InputStream is, OutputStream os) {
|
||||
Thread t = new Thread(() -> {
|
||||
byte[] buf = new byte[1];
|
||||
while (true) {
|
||||
int len;
|
||||
try {
|
||||
len = is.read(buf);
|
||||
if (len == -1) {
|
||||
return;
|
||||
}
|
||||
os.write(buf, 0, len);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
return t;
|
||||
}
|
||||
|
||||
public BufferedReader loggingReader(InputStream is) {
|
||||
return new BufferedReader(new InputStreamReader(is)) {
|
||||
@Override
|
||||
public String readLine() throws IOException {
|
||||
String line = super.readLine();
|
||||
System.out.println("log: " + line);
|
||||
return line;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public Thread runExitCheck(int expected, PtySession session) {
|
||||
Thread exitCheck = new Thread(() -> {
|
||||
while (true) {
|
||||
try {
|
||||
assertEquals("Early exit with wrong code", expected,
|
||||
session.waitExited());
|
||||
return;
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
System.err.println("Exit check interrupted");
|
||||
}
|
||||
}
|
||||
});
|
||||
exitCheck.setDaemon(true);
|
||||
exitCheck.start();
|
||||
return exitCheck;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.linux;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import ghidra.dbg.testutil.DummyProc;
|
||||
import ghidra.framework.OperatingSystem;
|
||||
import ghidra.pty.AbstractPtyTest;
|
||||
import ghidra.pty.PtyChild.Echo;
|
||||
import ghidra.pty.PtySession;
|
||||
|
||||
public class LinuxPtyTest extends AbstractPtyTest {
|
||||
@Before
|
||||
public void checkLinux() {
|
||||
assumeTrue(OperatingSystem.LINUX == OperatingSystem.CURRENT_OPERATING_SYSTEM);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenClosePty() throws IOException {
|
||||
LinuxPty pty = LinuxPty.openpty();
|
||||
pty.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParentToChild() throws IOException {
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
|
||||
BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(pty.getChild().getInputStream()));
|
||||
|
||||
writer.println("Hello, World!");
|
||||
writer.flush();
|
||||
assertEquals("Hello, World!", reader.readLine());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChildToParent() throws IOException {
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
PrintWriter writer = new PrintWriter(pty.getChild().getOutputStream());
|
||||
BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
|
||||
|
||||
writer.println("Hello, World!");
|
||||
writer.flush();
|
||||
assertEquals("Hello, World!", reader.readLine());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSessionBash() throws IOException, InterruptedException {
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
PtySession bash =
|
||||
pty.getChild().session(new String[] { DummyProc.which("bash") }, null);
|
||||
pty.getParent().getOutputStream().write("exit\n".getBytes());
|
||||
assertEquals(0, bash.waitExited());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForkIntoNonExistent() throws IOException, InterruptedException {
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
PtySession dies =
|
||||
pty.getChild().session(new String[] { "thisHadBetterNotExist" }, null);
|
||||
/**
|
||||
* Choice of 127 is based on bash setting "exit code" to 127 for "command not found"
|
||||
*/
|
||||
assertEquals(127, dies.waitExited());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSessionBashEchoTest() throws IOException, InterruptedException {
|
||||
Map<String, String> env = new HashMap<>();
|
||||
env.put("PS1", "BASH:");
|
||||
env.put("PROMPT_COMMAND", "");
|
||||
env.put("TERM", "");
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
LinuxPtyParent parent = pty.getParent();
|
||||
PrintWriter writer = new PrintWriter(parent.getOutputStream());
|
||||
BufferedReader reader = loggingReader(parent.getInputStream());
|
||||
PtySession bash =
|
||||
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
|
||||
runExitCheck(3, bash);
|
||||
|
||||
writer.println("echo test");
|
||||
writer.flush();
|
||||
String line;
|
||||
do {
|
||||
line = reader.readLine();
|
||||
}
|
||||
while (!"test".equals(line));
|
||||
|
||||
writer.println("exit 3");
|
||||
writer.flush();
|
||||
|
||||
line = reader.readLine();
|
||||
assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + line + "'",
|
||||
Set.of("BASH:exit 3", "exit 3").contains(line));
|
||||
|
||||
assertEquals(3, bash.waitExited());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSessionBashInterruptCat() throws IOException, InterruptedException {
|
||||
Map<String, String> env = new HashMap<>();
|
||||
env.put("PS1", "BASH:");
|
||||
env.put("PROMPT_COMMAND", "");
|
||||
env.put("TERM", "");
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
LinuxPtyParent parent = pty.getParent();
|
||||
PrintWriter writer = new PrintWriter(parent.getOutputStream());
|
||||
BufferedReader reader = loggingReader(parent.getInputStream());
|
||||
PtySession bash =
|
||||
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
|
||||
runExitCheck(3, bash);
|
||||
|
||||
writer.println("echo test");
|
||||
writer.flush();
|
||||
String line;
|
||||
do {
|
||||
line = reader.readLine();
|
||||
}
|
||||
while (!"test".equals(line));
|
||||
|
||||
writer.println("cat");
|
||||
writer.flush();
|
||||
line = reader.readLine();
|
||||
assertTrue("Not 'cat' or 'BASH:cat': '" + line + "'",
|
||||
Set.of("BASH:cat", "cat").contains(line));
|
||||
|
||||
writer.println("Hello, cat!");
|
||||
writer.flush();
|
||||
assertEquals("Hello, cat!", reader.readLine()); // echo back
|
||||
assertEquals("Hello, cat!", reader.readLine()); // cat back
|
||||
|
||||
writer.write(3); // should interrupt
|
||||
writer.flush();
|
||||
do {
|
||||
line = reader.readLine();
|
||||
}
|
||||
while (!"^C".equals(line));
|
||||
writer.println("echo test");
|
||||
writer.flush();
|
||||
|
||||
do {
|
||||
line = reader.readLine();
|
||||
}
|
||||
while (!"test".equals(line));
|
||||
|
||||
writer.println("exit 3");
|
||||
writer.flush();
|
||||
assertTrue(Set.of("BASH:exit 3", "exit 3").contains(reader.readLine()));
|
||||
|
||||
assertEquals(3, bash.waitExited());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocalEchoOn() throws IOException {
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
pty.getChild().nullSession();
|
||||
|
||||
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
|
||||
BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
|
||||
|
||||
writer.println("Hello, World!");
|
||||
writer.flush();
|
||||
assertEquals("Hello, World!", reader.readLine());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocalEchoOff() throws IOException {
|
||||
try (LinuxPty pty = LinuxPty.openpty()) {
|
||||
pty.getChild().nullSession(Echo.OFF);
|
||||
|
||||
PrintWriter writerP = new PrintWriter(pty.getParent().getOutputStream());
|
||||
PrintWriter writerC = new PrintWriter(pty.getChild().getOutputStream());
|
||||
BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
|
||||
|
||||
writerP.println("Hello, World!");
|
||||
writerP.flush();
|
||||
writerC.println("Good bye!");
|
||||
writerC.flush();
|
||||
|
||||
assertEquals("Good bye!", reader.readLine());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.ssh;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assume.assumeFalse;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import ghidra.app.script.AskDialog;
|
||||
import ghidra.pty.PtyChild.Echo;
|
||||
import ghidra.pty.PtySession;
|
||||
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||
import ghidra.util.SystemUtilities;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
|
||||
public class SshPtyTest extends AbstractGhidraHeadedIntegrationTest {
|
||||
protected GhidraSshPtyFactory factory;
|
||||
|
||||
@Before
|
||||
public void setupSshPtyTest() throws CancelledException {
|
||||
assumeFalse(SystemUtilities.isInTestingBatchMode());
|
||||
factory = new GhidraSshPtyFactory();
|
||||
factory.setUsername(promptUser());
|
||||
}
|
||||
|
||||
public static String promptUser() throws CancelledException {
|
||||
AskDialog<String> dialog = new AskDialog<>("SSH", "Username:", AskDialog.STRING, "");
|
||||
if (dialog.isCanceled()) {
|
||||
throw new CancelledException();
|
||||
}
|
||||
return dialog.getValueAsString();
|
||||
}
|
||||
|
||||
public static class StreamPumper extends Thread {
|
||||
private final InputStream in;
|
||||
private final OutputStream out;
|
||||
|
||||
public StreamPumper(InputStream in, OutputStream out) {
|
||||
setDaemon(true);
|
||||
this.in = in;
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
byte[] buf = new byte[1024];
|
||||
try {
|
||||
while (true) {
|
||||
int len = in.read(buf);
|
||||
if (len <= 0) {
|
||||
break;
|
||||
}
|
||||
out.write(buf, 0, len);
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSessionBash() throws IOException, InterruptedException {
|
||||
try (SshPty pty = factory.openpty()) {
|
||||
PtySession bash = pty.getChild().session(new String[] { "bash" }, null);
|
||||
OutputStream out = pty.getParent().getOutputStream();
|
||||
out.write("exit\n".getBytes("UTF-8"));
|
||||
out.flush();
|
||||
new StreamPumper(pty.getParent().getInputStream(), System.out).start();
|
||||
assertEquals(0, bash.waitExited());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisableEcho() throws IOException, InterruptedException {
|
||||
try (SshPty pty = factory.openpty()) {
|
||||
PtySession bash =
|
||||
pty.getChild().session(new String[] { "bash" }, null, Echo.OFF);
|
||||
OutputStream out = pty.getParent().getOutputStream();
|
||||
out.write("exit\n".getBytes("UTF-8"));
|
||||
out.flush();
|
||||
new StreamPumper(pty.getParent().getInputStream(), System.out).start();
|
||||
assertEquals(0, bash.waitExited());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.ProcessBuilder.Redirect;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.sun.jna.LastErrorException;
|
||||
|
||||
import ghidra.dbg.testutil.DummyProc;
|
||||
import ghidra.framework.OperatingSystem;
|
||||
import ghidra.pty.*;
|
||||
|
||||
public class ConPtyTest extends AbstractPtyTest {
|
||||
|
||||
@Before
|
||||
public void checkWindows() {
|
||||
assumeTrue(OperatingSystem.WINDOWS == OperatingSystem.CURRENT_OPERATING_SYSTEM);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSessionCmd() throws IOException, InterruptedException {
|
||||
try (Pty pty = ConPty.openpty()) {
|
||||
PtySession cmd = pty.getChild().session(new String[] { DummyProc.which("cmd") }, null);
|
||||
pty.getParent().getOutputStream().write("exit\r\n".getBytes());
|
||||
assertEquals(0, cmd.waitExited());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSessionNonExistent() throws IOException, InterruptedException {
|
||||
try (Pty pty = ConPty.openpty()) {
|
||||
pty.getChild().session(new String[] { "thisHadBetterNoExist" }, null);
|
||||
fail();
|
||||
}
|
||||
catch (LastErrorException e) {
|
||||
assertEquals(2, e.getErrorCode());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSessionCmdEchoTest() throws IOException, InterruptedException {
|
||||
try (Pty pty = ConPty.openpty()) {
|
||||
PtyParent parent = pty.getParent();
|
||||
PrintWriter writer = new PrintWriter(parent.getOutputStream());
|
||||
BufferedReader reader = loggingReader(parent.getInputStream());
|
||||
PtySession cmd = pty.getChild().session(new String[] { DummyProc.which("cmd") }, null);
|
||||
runExitCheck(3, cmd);
|
||||
|
||||
writer.println("echo test");
|
||||
writer.flush();
|
||||
String line;
|
||||
do {
|
||||
line = reader.readLine();
|
||||
}
|
||||
while (!"test".equals(line));
|
||||
|
||||
writer.println("exit 3");
|
||||
writer.flush();
|
||||
|
||||
assertEquals(3, cmd.waitExited());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSessionGdbLineLength() throws IOException, InterruptedException {
|
||||
try (Pty pty = ConPty.openpty()) {
|
||||
PtyParent parent = pty.getParent();
|
||||
PrintWriter writer = new PrintWriter(parent.getOutputStream());
|
||||
BufferedReader reader = loggingReader(parent.getInputStream());
|
||||
PtySession gdb =
|
||||
pty.getChild().session(new String[] { "C:\\msys64\\mingw64\\bin\\gdb.exe" }, null);
|
||||
|
||||
writer.println(
|
||||
"echo This line is cleary much, much, much, much, much, much, much, much, much " +
|
||||
" longer than 80 characters");
|
||||
writer.flush();
|
||||
String line;
|
||||
do {
|
||||
line = reader.readLine();
|
||||
}
|
||||
while (!"test".equals(line));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGdbInterruptPlain() throws Exception {
|
||||
ProcessBuilder builder = new ProcessBuilder("C:\\msys64\\mingw64\\bin\\gdb.exe");
|
||||
builder.redirectOutput(Redirect.PIPE);
|
||||
builder.redirectInput(Redirect.PIPE);
|
||||
builder.redirectErrorStream(true);
|
||||
|
||||
Process gdb = builder.start();
|
||||
|
||||
PrintWriter writer = new PrintWriter(gdb.getOutputStream());
|
||||
pump(gdb.getInputStream(), System.err);
|
||||
|
||||
System.out.println("Testing");
|
||||
writer.println("echo test");
|
||||
writer.println("set new-console on");
|
||||
System.out.println("Launching notepad");
|
||||
writer.println("file C:\\\\Windows\\\\notepad.exe");
|
||||
writer.println("run");
|
||||
writer.flush();
|
||||
System.out.println("Waiting");
|
||||
Thread.sleep(3000);
|
||||
System.out.println("Interrupting");
|
||||
writer.write(3);
|
||||
writer.println();
|
||||
writer.flush();
|
||||
System.out.println("Killing");
|
||||
writer.println("kill");
|
||||
writer.flush();
|
||||
writer.println("y");
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGdbInterruptConPty() throws Exception {
|
||||
try (Pty pty = ConPty.openpty()) {
|
||||
PtyParent parent = pty.getParent();
|
||||
PrintWriter writer = new PrintWriter(parent.getOutputStream());
|
||||
//BufferedReader reader = loggingReader(parent.getInputStream());
|
||||
PtySession gdb =
|
||||
pty.getChild().session(new String[] { "C:\\msys64\\mingw64\\bin\\gdb.exe" }, null);
|
||||
|
||||
pump(parent.getInputStream(), System.err);
|
||||
|
||||
System.out.println("Testing");
|
||||
writer.println("echo test");
|
||||
writer.println("set new-console on");
|
||||
System.out.println("Launching notepad");
|
||||
writer.println("file C:\\\\Windows\\\\notepad.exe");
|
||||
writer.println("run");
|
||||
writer.flush();
|
||||
System.out.println("Waiting");
|
||||
Thread.sleep(3000);
|
||||
System.out.println("Interrupting");
|
||||
writer.write(3);
|
||||
writer.println();
|
||||
writer.flush();
|
||||
System.out.println("Killing");
|
||||
writer.println("kill");
|
||||
writer.flush();
|
||||
writer.println("y");
|
||||
writer.flush();
|
||||
|
||||
Thread.sleep(100000);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGdbMiConPty() throws Exception {
|
||||
try (Pty pty = ConPty.openpty()) {
|
||||
PtyParent parent = pty.getParent();
|
||||
PrintWriter writer = new PrintWriter(parent.getOutputStream());
|
||||
//BufferedReader reader = loggingReader(parent.getInputStream());
|
||||
PtySession gdb = pty.getChild()
|
||||
.session(new String[] { "C:\\msys64\\mingw64\\bin\\gdb.exe", "-i", "mi2" },
|
||||
null);
|
||||
|
||||
InputStream inputStream = parent.getInputStream();
|
||||
inputStream = new AnsiBufferedInputStream(inputStream);
|
||||
pump(inputStream, System.out);
|
||||
|
||||
writer.println("-interpreter-exec console \"echo test\"");
|
||||
writer.println("-interpreter-exec console \"quit\"");
|
||||
writer.flush();
|
||||
|
||||
gdb.waitExited();
|
||||
//System.out.println("Exited");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.pty.windows;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import com.sun.jna.LastErrorException;
|
||||
import com.sun.jna.platform.win32.Kernel32;
|
||||
import com.sun.jna.platform.win32.WinNT.HANDLE;
|
||||
|
||||
public class NamedPipeTest {
|
||||
|
||||
protected Handle checkHandle(HANDLE handle) {
|
||||
if (Kernel32.INVALID_HANDLE_VALUE.equals(handle)) {
|
||||
throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
|
||||
}
|
||||
return new Handle(handle);
|
||||
}
|
||||
|
||||
// @Test
|
||||
/**
|
||||
* Experiment with MinGW GDB, named pipes, and {@code new-ui}.
|
||||
*
|
||||
* <p>
|
||||
* Run this test, start GDB in a command shell, then issue
|
||||
* "{@code new-ui mi2 \\\\.\\pipe\\GhidraGDB}". With GDB 11.1, GDB will print "New UI allocated"
|
||||
* to the console, and I'll receive {@code =thread-group-added,id="i1"} on the pipe. However,
|
||||
* GDB never seems to process my {@code -add-inferior} command. Furthermore, GDB freezes and no
|
||||
* longer accepts input on the console, either.
|
||||
*/
|
||||
public void testExpNamedPipes() throws Exception {
|
||||
Handle hPipe = checkHandle(Kernel32.INSTANCE.CreateNamedPipe(
|
||||
"\\\\.\\pipe\\GhidraGDB" /*lpName*/,
|
||||
Kernel32.PIPE_ACCESS_DUPLEX /*dwOpenMode*/,
|
||||
Kernel32.PIPE_TYPE_BYTE | Kernel32.PIPE_WAIT /*dwPipeMode*/,
|
||||
Kernel32.PIPE_UNLIMITED_INSTANCES /*nMaxInstances*/,
|
||||
1024 /*nOutBufferSize*/,
|
||||
1024 /*nInBufferSize*/,
|
||||
0 /*nDefaultTimeOut*/,
|
||||
null /*lpSecurityAttributes*/));
|
||||
|
||||
try (PrintWriter writer = new PrintWriter(new HandleOutputStream(hPipe));
|
||||
BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(new HandleInputStream(hPipe)))) {
|
||||
|
||||
writer.println("-add-inferior");
|
||||
writer.flush();
|
||||
|
||||
String line;
|
||||
while (null != (line = reader.readLine())) {
|
||||
System.out.println(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue