Merge branch 'GP-2774_ghidraffe_GhidraGo_final'

This commit is contained in:
ghidra1 2023-12-01 18:39:51 -05:00
commit 32d3fc4c70
25 changed files with 2172 additions and 0 deletions

View file

View 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.
*/
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/helpProject.gradle"
apply plugin: 'eclipse'
eclipse.project.name = 'Features GhidraGo'
dependencies {
api project(':Base')
api project(':Generic')
api project(':Project')
}

View file

@ -0,0 +1,4 @@
##VERSION: 2.0
Module.manifest||GHIDRA||||END|
src/main/help/help/TOC_Source.xml||GHIDRA||||END|
src/main/help/help/topics/GhidraGo/GhidraGo.html||GHIDRA||||END|

View file

@ -0,0 +1,60 @@
<?xml version='1.0' encoding='ISO-8859-1' ?>
<!--
This is an XML file intended to be parsed by the Ghidra help system. It is loosely based
upon the JavaHelp table of contents document format. The Ghidra help system uses a
TOC_Source.xml file to allow a module with help to define how its contents appear in the
Ghidra help viewer's table of contents. The main document (in the Base module)
defines a basic structure for the
Ghidra table of contents system. Other TOC_Source.xml files may use this structure to insert
their files directly into this structure (and optionally define a substructure).
In this document, a tag can be either a <tocdef> or a <tocref>. The former is a definition
of an XML item that may have a link and may contain other <tocdef> and <tocref> children.
<tocdef> items may be referred to in other documents by using a <tocref> tag with the
appropriate id attribute value. Using these two tags allows any module to define a place
in the table of contents system (<tocdef>), which also provides a place for
other TOC_Source.xml files to insert content (<tocref>).
During the help build time, all TOC_Source.xml files will be parsed and validated to ensure
that all <tocref> tags point to valid <tocdef> tags. From these files will be generated
<module name>_TOC.xml files, which are table of contents files written in the format
desired by the JavaHelp system. Additionally, the genated files will be merged together
as they are loaded by the JavaHelp system. In the end, when displaying help in the Ghidra
help GUI, there will be on table of contents that has been created from the definitions in
all of the modules' TOC_Source.xml files.
Tags and Attributes
<tocdef>
-id - the name of the definition (this must be unique across all TOC_Source.xml files)
-text - the display text of the node, as seen in the help GUI
-target** - the file to display when the node is clicked in the GUI
-sortgroup - this is a string that defines where a given node should appear under a given
parent. The string values will be sorted by the JavaHelp system using
a javax.text.RulesBasedCollator. If this attribute is not specified, then
the text of attribute will be used.
<tocref>
-id - The id of the <tocdef> that this reference points to
**The URL for the target is relative and should start with 'help/topics'. This text is
used by the Ghidra help system to provide a universal starting point for all links so that
they can be resolved at runtime, across modules.
-->
<tocroot>
<tocref id="Ghidra Support">
<tocdef id="GhidraGo"
text="GhidraGo"
target="help/topics/GhidraGo/GhidraGo.html" />
</tocref> <!-- End Ghidra Support -->
</tocroot>

View file

@ -0,0 +1,233 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<!-- Keep pre text from wrapping so that it is formatted exactly as we have it -->
<style>
pre {
white-space: no-wrap;
font-family: 'Courier New', 'Courier';
}
typewriter {
font-family: 'Courier New', 'Courier';
}
/* Make the general text a bit more readable */
body {
font-size: 20px;
}
</style>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>GhidraGo README</title>
<link rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
<link rel="stylesheet" type="text/css" href="../../shared/languages.css">
<meta name="generator" content="DocBook XSL Stylesheets V1.79.1">
</head>
<body>
<h1 align="center"><a name="top">GhidraGo README</a></h1>
<h2>Table of Contents</h2>
<UL>
<LI><a href="#general">Introduction</a></LI>
<ul><li><a href="#example">Example</a></li></ul>
<LI><a href="#plugin">Configure GhidraGo Plugin</a></LI>
<li>
<a href="#configure">Configure Protocol Handler (Platform Specific)</a>
<ul>
<LI><a href="#windows">Windows</a></LI>
<LI><a href="#linux">Linux</a></LI>
<LI><a href="#mac">Mac</a></LI>
</ul>
</li>
</UL>
<div style="border-top: 4px double; margin-top: 1em; padding-top: 1em;"> </div>
<h2><a name="general">GhidraGo Introduction</a></h2>
<div>
<p>
GhidraGo is a mechanism to cause Ghidra to display a previously imported program within a
local or multi-user project using a ghidraURL hyperlink similar to an http reference. In
practice ghidraURL's work very similarly to selecting a URL reference which displays a PDF.
Once setup correctly, GhidraURL links can be placed in web pages, external project
documentation files, or any other place a URL hyperlink can be placed.
</p>
<p>
When a GhidraURL is selected, GhidraGo will startup Ghidra if it isn't already running as
well as prompt to login to the multi-user project if necessary. The program is displayed in
the default tool, usually the codebrowser, and can be configured to re-use an open default
tool or to use a new default tool. The GhidraURL must currently be locating a DomainFile
that is either in a Remote, Shared project, or a local project.
</p>
<p>
GhidraGo is a combination of a command line program to send a link, a plugin running within
the Ghidra project manager, and the configuration of the default handling for the ghidraURL
within the user environment. The ghidraURL is sent as the first and only parameter to the
ghidraGo command line interface.
</p>
<p>
GhidraGo passes information through a simple filesystem mechanism vice an open port for
security and simplicity. GhidraGo works on Windows, Linux, and MacOS.
</p>
<p>
<p>GhidraURL's have the format:</p>
<div style="margin-left: 25px">
<p>
Remote Ghidra Server File:
ghidra://&lt;host&gt;[:&lt;port&gt;]/&lt;repository-name&gt;/&lt;program-path&gt;
[#&lt;address-or-symbol-ref&gt;]
</p>
<p style="margin-left: 25px">Example: ghidra://hostname/Repo/notepad.exe#main</p>
<p>
Local Ghidra Project File:
ghidra:/[&lt;project-path&gt;/]&lt;project-name&gt;?/&lt;program-path&gt;
[#&lt;address-or-symbol-ref&gt;]
</p>
<p style="margin-left: 25px">Example: ghidra:/share/MyProject?/notepad.exe#main</p>
</div>
</p>
</div>
<h3><a name="example">Example of Using ghidraGo CLI</a></h3>
<div>
<p><code>ghidraGo ghidra://ghidra-server/project/myProgram#symbol</code></p>
<p>Executing this command will result in the program called <code>myProgram</code> being
opened in Ghidra's default tool with the cursor at <code>symbol</code>.</p>
</div>
<h2><a name="plugin">Configure GhidraGo Plugin</a></h2>
<div>
<ol>
<li>Start Ghidra</li>
<li>Choose File &gt; Configuration in the Project Window (not the Codebrowser Window)</li>
<li>Click the Plug Icon in the upper right to display all plugins</li>
<li>Search for GhidraGoPlugin and select it</li>
<li>Press OK</li>
</ol>
<p>Ghidra is now configured to listen to GhidraGo Requests. You can execute a GhidraGo request
using the "ghidraGo" shell/batch script in
<code>/path/to/ghidra/support/GhidraGo/ghidraGo</code></p>
</div>
<div>
<h2><a name="configure">Configure Protocol Handler (Platform Specific)</a></h2>
<div>
<p>
Configuring your platform to handle the <script>ghidra</script> protocol is what
enables the ghidraGo command line interface to be associated with a ghidraURL. Once
configured, clicking hyperlinks that start with the <script>ghidra</script> protocol
will execute the ghidraGo CLI with that hyperlink as the first argument. The
configuration is platform specific.
</p>
<p>
*NOTE: changes to your path to ghidra, such as upgrading ghidra to a new version,
will require the path you set in this configuration to be updated.
</p>
</div>
<h3><a name="windows">Windows Protocol Handler Configuration</a></h3>
<div>
<ol>
<li>Go to Start &gt; Find and Type <code>regedit</code></li>
<li>Right click HKEY_CLASSES_ROOT then New &gt; Key</li>
<li>Name the key "ghidra"</li>
<li>Right Click ghidra &gt; New &gt; String Value and add "URL Protocol" without a value</li>
<li>Right Click ghidra &gt; New &gt; Key and create the heiarchy ghidra/shell/open/command</li>
<li>Inside command change (Default) to the path where ghidraGo is located followed by
a "%1". For Example:</li>
<br />
<code>C:\Path\To\Ghidra\support\GhidraGo\ghidraGo "%1"</code>
</ol>
</div>
<h3><a name="linux">Linux Protocol Handler Configuration</a></h3>
<div>
<p>In Linux, when you click a browser link with an <code>href</code> value to a GhidraURL,
you'll be prompted to use xdg-open.</p>
<ol>
<li>Edit the file <code>ghidra.desktop</code> in <code>~/.local/share/applications</code></li>
<br />
<code>
[Desktop Entry]<br />
Name=ghidra Client<br />
Exec=/path/to/ghidra/support/GhidraGo/ghidraGo "%u"<br />
Type=Application<br />
Terminal=false<br />
MimeType=x-scheme-handler/ghidra;<br />
</code>
<br />
<li>Edit the file mimeapps.list in <code>~/.local/share/applications</code></li>
<br />
<code>
[Default Applications]<br />
x-scheme-handler/ghidra=ghidra.desktop<br />
...<br />
</code>
</ol>
<p>After the steps above, you should be able to click a GhidraURL href, get the same
xdg-open prompt, and upon clicking "Open xdg-open" GhidraGo should execute and open
Ghidra to the given GhidraURL.</p>
</div>
<h3><a name="mac">Mac Protocol Handler Configuration</a></h3>
<div>
<ol>
<li>Open <code>Script Editor</code> and past the following into the editor.</li>
<br />
<code>
on open location schemeUrl<br />
&emsp;set ghidraUrl to quoted form of schemeUrl<br />
&emsp;do shell script "/path/to/ghidraGo " &amp; ghidraUrl<br />
end open location<br />
</code>
<br />
<li>Save the script as an Application named GhidraGo in either
<code>/Applications</code> or <code>~/Applications</code></li>
<li>Right click on the saved Application and click Show Package Contents</li>
<li>Open Contents &gt; Info.plist and under
<code>&lt;string&gt;com.apple.ScriptEditor.id.GhidraGo&lt;/string&gt;</code>
paste the following:</li>
<br />
<code>
&lt;key&gt;CFBundleURLTypes&lt;/key&gt;<br />
&lt;array&gt;<br />
&emsp;&lt;dict&gt;<br />
&emsp;&emsp;&lt;key&gt;CFBundleURLName&lt;/key&gt;<br />
&emsp;&emsp;&lt;string&gt;Ghidra Scheme&lt;/string&gt;<br />
&emsp;&emsp;&lt;key&gt;CFBundleURLSchemes&lt;/key&gt;<br />
&emsp;&emsp;&lt;array&gt;<br />
&emsp;&emsp;&emsp;&lt;string&gt;ghidra&lt;/string&gt;<br />
&emsp;&emsp;&lt;/array&gt;<br />
&emsp;&lt;/dict&gt;<br />
&lt;/array&gt;
</code>
<br />
<li>Go to the Applications folder where you saved the GhidraGo, and Open
GhidraGo (run it once).</li>
</ol>
</div>
</div>
(<a href="#top">Back to Top</a>)
<div style="border-top: 4px double; margin-top: 1em; padding-top: 1em;"> </div>
<address></address>
Last modified: Oct 26 2023
</body> </html>
<style>
table, td {
border: 1px solid black;
}
td {
padding: 15px;
text-align: center;
}
</style>

View file

@ -0,0 +1,168 @@
/* ###
* 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;
import java.io.IOException;
import java.nio.file.Path;
import docking.framework.DockingApplicationConfiguration;
import generic.jar.ResourceFile;
import ghidra.app.plugin.core.go.GhidraGoSender;
import ghidra.app.plugin.core.go.exception.*;
import ghidra.framework.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.util.*;
/**
* <h1>GhidraGo Client</h1>
* <p>The first argument is expected to be non-null and a valid {@link GhidraURL}</p>
* <p>If the {@link GhidraURL} is valid, the URL is processed in an existing Ghidra, or
* a new Ghidra is started and used to process the URL.</p>
* <p>A valid {@link GhidraURL} in this case must be pointing to a remote (shared project)
* Program.</p>
* <p>In the event that a Ghidra is running and does not have an active project, the URL cannot be
* processed.</p>
*/
public class GhidraGo implements GhidraLaunchable {
private GhidraGoSender sender;
/**
* Initializes a new GhidraGoSender and processes the {@link GhidraURL}
* @param layout the layout passed from main.Ghidra
* @param args the CLI args passed to GhidraGo. args should contain a single {@link GhidraURL}.
* @throws Exception in the event of an error
*/
@Override
public void launch(GhidraApplicationLayout layout, String[] args) throws Exception {
try {
ApplicationConfiguration configuration = null;
if (!Application.isInitialized()) {
System.setProperty(ApplicationProperties.APPLICATION_NAME_PROPERTY, "GhidraGo");
configuration = new DockingApplicationConfiguration();
Application.initializeApplication(layout, configuration);
}
if (args != null && args.length > 0) {
ghidra.framework.protocol.ghidra.Handler.registerHandler();
sender = new GhidraGoSender();
startGhidraIfNeeded(layout);
sender.send(args[0]);
// if configuration is null, probably running inside a test
if (configuration != null) {
// calling System.exit explicitly is necessary, otherwise the Loading... screen
// persists instead of closing when complete.
System.exit(0);
}
}
else {
throw new IllegalArgumentException(
"A valid GhidraURL locating a program, program name, or path to a program name " +
"must be specified as the first command line argument.");
}
}
catch (FailedToStartGhidraException e) {
logOrShowError("GhidraGo Start Ghidra Exception",
"Failed to start Ghidra from GhidraGo", e);
System.exit(-1);
}
catch (StopWaitingException e) {
System.exit(-1);
}
catch (Exception e) {
logOrShowError("GhidraGo Exception", "An unexpected exception occurred in GhidraGo", e);
// calling System.exit explicitly is necessary, otherwise the Loading... screen
// persists instead of closing when complete.
System.exit(-1);
}
}
private void logOrShowError(String errorTitle, String errorMessage, Exception e) {
if (SystemUtilities.isInHeadlessMode()) {
Msg.error(this, errorMessage, e);
}
else {
Swing.runNow(() -> Msg.showError(this, null, errorTitle, errorMessage, e));
}
}
private void startGhidraIfNeeded(GhidraApplicationLayout layout)
throws StopWaitingException, FailedToStartGhidraException {
// if there is no listening Ghidra
if (!sender.isGhidraListening()) {
// attempt to start a Ghidra within a locked action
// do not wait for the lock if another GhidraGo has been started.
try {
boolean success = sender.doLockedAction(false, () -> {
try {
Process ghidraProcess = startGhidra(layout);
sender.waitForListener(ghidraProcess);
return true;
}
catch (StopWaitingException e) {
return true;
}
catch (StartedGhidraProcessExitedException | IOException e) {
return false;
}
});
if (!success) {
// GhidraGo attempted to start ghidra and failed
throw new FailedToStartGhidraException();
}
}
catch (UnableToGetLockException e) {
// When another GhidraGo has the lock,
// wait for there to be a listener without starting the process
sender.waitForListener();
}
}
}
/**
* Determines the execution platform and executes the appropriate shell/bash script to start
* Ghidra.
* @throws IOException in the event that the execution failed
*/
private Process startGhidra(GhidraApplicationLayout layout) throws IOException {
ResourceFile file = layout.getApplicationInstallationDir();
Path ghidraRunPath;
if (SystemUtilities.isInDevelopmentMode()) {
if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.WINDOWS) {
ghidraRunPath = Path.of(file.getAbsolutePath(),
"/ghidra/Ghidra/RuntimeScripts/Windows/ghidraRun.bat");
}
else {
ghidraRunPath = Path.of(file.getAbsolutePath(),
"/ghidra/Ghidra/RuntimeScripts/Linux/ghidraRun");
}
}
else {
if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.WINDOWS) {
ghidraRunPath = Path.of(file.getAbsolutePath(), "/ghidraRun.bat");
}
else {
ghidraRunPath = Path.of(file.getAbsolutePath(), "/ghidraRun");
}
}
Msg.info(this, "Starting new Ghidra using ghidraRun script at " + ghidraRunPath);
return Runtime.getRuntime().exec(ghidraRunPath.toString());
}
}

View file

@ -0,0 +1,106 @@
/* ###
* 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.app.plugin.core.go;
import java.io.IOException;
import java.net.URL;
import ghidra.app.CorePluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.go.ipc.GhidraGoListener;
import ghidra.framework.main.AppInfo;
import ghidra.framework.main.ApplicationLevelOnlyPlugin;
import ghidra.framework.model.ToolServices;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
//@formatter:off
@PluginInfo(
category = PluginCategoryNames.COMMON,
status = PluginStatus.UNSTABLE,
packageName = CorePluginPackage.NAME,
shortDescription = "Listens for new GhidraURL's to launch using ToolServices",
description = "Polls the ghidraGo directory for any url files written by the GhidraGoClient and " +
"processes them in Ghidra",
eventsConsumed = {ProjectPluginEvent.class})
//@formatter:on
public class GhidraGoPlugin extends Plugin implements ApplicationLevelOnlyPlugin {
private GhidraGoListener listener;
public GhidraGoPlugin(PluginTool tool) {
super(tool);
}
@Override
protected void init() {
super.init();
}
@Override
protected void dispose() {
if (this.listener != null) {
listener.dispose();
listener = null;
}
super.dispose();
}
@Override
public void processEvent(PluginEvent event) {
if (event instanceof ProjectPluginEvent) {
if (((ProjectPluginEvent) event).getProject() == null) {
dispose();
}
else {
try {
listener = new GhidraGoListener((url) -> {
processGhidraURL(url);
});
}
catch (IOException e) {
Msg.showError(this, null, "GhidraGoPlugin Exception",
"Unable to create Listener", e);
}
}
}
}
/**
* If the active project is null, do nothing.
* Otherwise, try and open the url using {@link ToolServices} launchDefaultToolWithURL function.
* @param ghidraURL the GhidraURL to open.
*/
private void processGhidraURL(URL ghidraURL) {
Msg.info(GhidraGoPlugin.class, "GhidraGo processing " + ghidraURL);
try {
Msg.info(GhidraGoPlugin.class,
"Accepting the resource at " + GhidraURL.getProjectURL(ghidraURL));
SystemUtilities.runSwingNow(() -> {
AppInfo.getFrontEndTool().toFront();
AppInfo.getFrontEndTool().getToolServices().launchDefaultToolWithURL(ghidraURL);
});
}
catch (IllegalArgumentException e) {
Msg.showError(GhidraGoPlugin.class, null, "GhidraGo Unable to process GhidraURL",
"GhidraGo could not process " + ghidraURL, e);
}
}
}

View file

@ -0,0 +1,151 @@
/* ###
* 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.app.plugin.core.go;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.apache.commons.lang3.StringUtils;
import ghidra.app.plugin.core.go.exception.*;
import ghidra.app.plugin.core.go.ipc.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.util.Msg;
import ghidra.util.Swing;
public class GhidraGoSender extends GhidraGoIPC {
public GhidraGoSender() throws IOException {
super();
}
@Override
public void dispose() {
// empty
}
/**
* performs the given action once the sender lock has been acquired. Using this method ensures
* only one sender will perform the given action.
* @param waitForLock whether to block until the lock is available
* @param action the action to be performed once a lock is acquired. Returns true if successful.
* @return true if action was successfully performed; false otherwise.
* @throws UnableToGetLockException if the lock was unobtainable
*/
public boolean doLockedAction(boolean waitForLock, Supplier<Boolean> action)
throws UnableToGetLockException {
return GhidraGoIPC.doLockedAction(senderLockPath, waitForLock, action);
}
/**
* Send the url to an existing, listening Ghidra
* @param url a valid {@link GhidraURL} in string form for a remote Ghidra program. An error is
* displayed if the url is null.
* @throws StopWaitingException in the event the stop waiting dialog is shown and answered No.
*/
public void send(String url) throws StopWaitingException {
if (StringUtils.isEmpty(url)) {
Swing.runNow(() -> Msg.showError(this, null, "GhidraGo Empty URL Error",
"An empty GhidraURL cannot be sent."));
return;
}
// create a random file and write the url in it
String fileName = UUID.randomUUID().toString();
Path randomFilePath = channelPath.resolve(fileName);
Path writtenFilePath = urlFilesPath.resolve(fileName);
try (FileOutputStream fos = new FileOutputStream(randomFilePath.toFile());) {
fos.write(url.getBytes());
// need to close the file so that it can be moved on window's host
fos.close();
Files.move(randomFilePath, writtenFilePath);
}
catch (IOException e) {
randomFilePath.toFile().delete();
Swing.runNow(() -> Msg.showError(this, null, "GhidraGo Error Sending URL",
"There was a file system error preventing the url from being sent.", e));
}
Msg.info(this, "Wrote " + url + " to random file " + writtenFilePath);
if (writtenFilePath.toFile().exists()) {
waitForFileToBeProcessed(writtenFilePath);
}
}
/**
* waits for the file located at the given file path to be deleted.
* @param filePath the path to the file to wait for deletion of
* @throws StopWaitingException in the event the stop waiting dialog is shown and answered No.
*/
private void waitForFileToBeProcessed(Path filePath) throws StopWaitingException {
// check without dialogs every 100 milliseconds
if (filePath.toFile().exists()) {
// set up periodic check for file
CheckForFileProcessedRunnable checkForFile =
new CheckForFileProcessedRunnable(filePath, 100, TimeUnit.MILLISECONDS);
// start checking for file
checkForFile.startChecking(100, TimeUnit.MILLISECONDS);
// block until file has been processed or user answers dialog with No.
checkForFile.awaitTermination();
}
}
/**
* wait for a Ghidra to be listening and ready.
* @throws StopWaitingException in the event waiting for a listener was stopped
*/
public void waitForListener() throws StopWaitingException {
try {
waitForListener(null);
}
catch (StartedGhidraProcessExitedException e) {
// this will never happen when the process sent is null
}
}
/**
* wait for a Ghidra to be listening and ready.
* @param p ghidraRun process that is being waited for in the event that GhidraGo
* started Ghidra
* @throws StopWaitingException in the event waiting for a listener was stopped
* @throws StartedGhidraProcessExitedException in the event a Ghidra was started and exited
* unexpectedly.
*/
public void waitForListener(Process p)
throws StopWaitingException, StartedGhidraProcessExitedException {
if (!isGhidraListening()) {
// set up periodic check for listener
CheckForListenerRunnable checkForListener = new CheckForListenerRunnable(p, 100,
TimeUnit.MILLISECONDS,
() -> !isGhidraListening());
// start checking for listener
checkForListener.startChecking(100, TimeUnit.MILLISECONDS);
// block until listener has been processed or user answers dialog with No.
checkForListener.awaitTermination();
}
}
}

View file

@ -0,0 +1,105 @@
/* ###
* 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.app.plugin.core.go.dialog;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
import docking.DialogComponentProvider;
import docking.DockingWindowManager;
import docking.widgets.MultiLineLabel;
import docking.widgets.OptionDialog;
import docking.widgets.label.GIconLabel;
import ghidra.app.plugin.core.go.exception.StopWaitingException;
public abstract class GhidraGoWaitDialog extends DialogComponentProvider {
public static final int WAIT = 0;
public static final int DO_NOT_WAIT = 1;
protected int actionID = DO_NOT_WAIT;
protected boolean answered = false;
public GhidraGoWaitDialog(String title, String msgText, boolean modal) {
super(title, modal);
addWorkPanel(buildMainPanel(msgText));
JButton waitButton = new JButton("Wait");
waitButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
actionID = WAIT;
answered = true;
close();
}
});
addButton(waitButton);
JButton noWaitButton = new JButton("No");
noWaitButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
actionID = DO_NOT_WAIT;
answered = true;
close();
}
});
addButton(noWaitButton);
}
public void showDialog() throws StopWaitingException {
answered = false;
if (!isShowing()) {
DockingWindowManager.showDialog(null, this);
}
if (answered && actionID == DO_NOT_WAIT) {
throw new StopWaitingException();
}
}
public boolean isAnsweredNo() {
return answered && actionID == DO_NOT_WAIT;
}
public void reset() {
answered = false;
actionID = WAIT;
close();
}
protected JPanel buildMainPanel(String msgTextString) {
JPanel innerPanel = new JPanel();
innerPanel.setLayout(new BorderLayout());
innerPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
JPanel msgPanel = new JPanel(new BorderLayout());
msgPanel.add(
new GIconLabel(OptionDialog.getIconForMessageType(OptionDialog.WARNING_MESSAGE)),
BorderLayout.WEST);
MultiLineLabel msgText = new MultiLineLabel(msgTextString);
msgText.setMaximumSize(msgText.getPreferredSize());
msgPanel.add(msgText, BorderLayout.CENTER);
innerPanel.add(msgPanel, BorderLayout.CENTER);
return innerPanel;
}
}

View file

@ -0,0 +1,29 @@
/* ###
* 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.app.plugin.core.go.dialog;
public class GhidraGoWaitForListenerDialog extends GhidraGoWaitDialog {
public GhidraGoWaitForListenerDialog() {
super("GhidraGo Taking Longer Than Expected to Listen",
"If Ghidra has started, please confirm the GhidraGoPlugin has been added in " +
"File->Configure in the Ghidra project manager.\n" +
"If GhidraGoPlugin has been configured, make sure Ghidra has an active project.\n" +
"Would you like to keep waiting?",
true);
}
}

View file

@ -0,0 +1,20 @@
/* ###
* 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.app.plugin.core.go.exception;
public class FailedToStartGhidraException extends Exception {
// empty
}

View file

@ -0,0 +1,24 @@
/* ###
* 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.app.plugin.core.go.exception;
public class StartedGhidraProcessExitedException extends Exception {
public StartedGhidraProcessExitedException(int exitValue) {
super("Started Ghidra process exited early with exit-value: " + exitValue);
}
}

View file

@ -0,0 +1,20 @@
/* ###
* 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.app.plugin.core.go.exception;
public class StopWaitingException extends Exception {
// empty
}

View file

@ -0,0 +1,20 @@
/* ###
* 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.app.plugin.core.go.exception;
public class UnableToGetLockException extends Exception {
// empty
}

View file

@ -0,0 +1,131 @@
/* ###
* 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.app.plugin.core.go.ipc;
import java.nio.file.Path;
import java.util.concurrent.*;
import ghidra.app.plugin.core.go.exception.StopWaitingException;
import ghidra.util.Msg;
import ghidra.util.Swing;
public class CheckForFileProcessedRunnable extends CheckPeriodicallyRunnable {
/**
* How long to wait before asking to continue waiting after the url has been sent to a listening
* Ghidra
*/
public static int WAIT_FOR_PROCESSING_DELAY_MS = 500;
/**
* How frequently to ask to continue waiting after Wait is selected
*/
public static int WAIT_FOR_PROCESSING_PERIOD_MS = 60_000;
/**
* Maximum amount of time to wait for the file to be processed
*/
public static int MAX_WAIT_FOR_PROCESSING_MIN = 1;
private Path filePath;
private StopWaitingException stopWaitingException;
public CheckForFileProcessedRunnable(Path filePath, int period, TimeUnit timeUnit) {
super(false, period, timeUnit, () -> filePath.toFile().exists());
this.filePath = filePath;
}
/**
* This constructor is used to create the thread that will show the wait dialog
* @param executor the internal executor that should have 2 threads
* @param filePath the path to the file to check
* @param period the interval to show the dialog
* @param timeUnit the units for the period
*/
private CheckForFileProcessedRunnable(ScheduledExecutorService executor, Path filePath,
int period, TimeUnit timeUnit) {
super(executor, true, period, timeUnit, () -> filePath.toFile().exists());
this.filePath = filePath;
}
public void run() {
try {
if (checkCondition.call()) {
try {
if (showDialog) {
// show the dialog in a blocking action. This will throw a StopWaitingException
// if they answer No. Otherwise, they want to keep waiting.
dialog.showDialog();
}
// if their response was WAIT, reset the timer
executor.schedule(this, period, timeUnit);
}
catch (StopWaitingException e) {
this.stopWaitingException = e;
dispose();
}
catch (RejectedExecutionException e) {
// this is okay, executor has been shutdown
dispose();
}
}
else {
dispose();
}
}
catch (Exception e) {
Swing.runNow(() -> Msg.showError(this, null, "GhidraGo Unable to Check File",
"GhidraGo could not check existence of file at " + filePath, e));
dispose();
}
}
@Override
public void startChecking(int delay, TimeUnit delayTimeUnit) throws StopWaitingException {
dialog.reset();
try {
// start thread to check frequently
super.startChecking(delay, delayTimeUnit);
// start thread for showing dialog
executor.schedule(new CheckForFileProcessedRunnable(executor, filePath,
WAIT_FOR_PROCESSING_PERIOD_MS, TimeUnit.MILLISECONDS), WAIT_FOR_PROCESSING_DELAY_MS,
TimeUnit.MILLISECONDS);
}
catch (RejectedExecutionException e) {
if (dialog.isAnsweredNo()) {
throw new StopWaitingException();
}
}
}
public void awaitTermination() throws StopWaitingException {
try {
executor.awaitTermination(MAX_WAIT_FOR_PROCESSING_MIN, TimeUnit.MINUTES);
if (dialog.isAnsweredNo()) {
throw new StopWaitingException();
}
if (this.stopWaitingException != null) {
throw this.stopWaitingException;
}
}
catch (InterruptedException e) {
// this is okay
}
}
}

View file

@ -0,0 +1,156 @@
/* ###
* 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.app.plugin.core.go.ipc;
import java.util.concurrent.*;
import ghidra.app.plugin.core.go.exception.StartedGhidraProcessExitedException;
import ghidra.app.plugin.core.go.exception.StopWaitingException;
import ghidra.util.Msg;
import ghidra.util.Swing;
public class CheckForListenerRunnable extends CheckPeriodicallyRunnable {
/**
* If a listening Ghidra needs to be started, how long to wait before asking to continue
* waiting
*/
public static int WAIT_FOR_LISTENER_DELAY_MS = 30_000;
/**
* How frequently to ask to continue waiting after Wait is selected
*/
public static int WAIT_FOR_LISTENER_PERIOD_MS = 60_000;
/**
* Maximum amount of time to wait for a listening Ghidra
*/
public static int MAX_WAIT_FOR_LISTENER_MIN = 5;
private Process process;
private StopWaitingException stopWaitingException;
private StartedGhidraProcessExitedException startedGhidraProcessExitedException;
public CheckForListenerRunnable(Process p, int period, TimeUnit timeUnit,
Callable<Boolean> checkCondition) {
super(false, period, timeUnit, checkCondition);
this.process = p;
}
private CheckForListenerRunnable(ScheduledExecutorService executor, Process p, int period,
TimeUnit timeUnit, Callable<Boolean> checkCondition) {
super(executor, true, period, timeUnit, checkCondition);
this.process = p;
}
public void run() {
try {
if (checkCondition.call()) {
try {
checkProcessDidNotExit(process);
if (showDialog) {
Msg.info(this, "Waiting for GhidraGo to listen for new files...");
// show the dialog in a blocking action. This will throw a StopWaitingException
// if they answer No. Otherwise, they want to keep waiting.
dialog.showDialog();
}
executor.schedule(this, period, timeUnit);
}
catch (StopWaitingException e) {
this.stopWaitingException = e;
dispose();
}
catch (StartedGhidraProcessExitedException e) {
this.startedGhidraProcessExitedException = e;
dispose();
}
catch (RejectedExecutionException e) {
// this is okay, executor has been shutdown
dispose();
}
}
else {
dispose();
}
}
catch (Exception e) {
Swing.runNow(() -> Msg.showError(this, null, "GhidraGo Unable to Check For Listener",
"GhidraGo could not check for a listener.", e));
dispose();
}
}
/**
* checks to see if Ghidra process exited early, In the event Ghidra exits early,
* a runtime exception is thrown
* @param p Ghidra process
* @throws StartedGhidraProcessExitedException if the Ghidra process has an exit value
*/
private void checkProcessDidNotExit(Process p) throws StartedGhidraProcessExitedException {
if (p != null) {
try {
int exitValue = p.exitValue();
if (exitValue != 0)
throw new StartedGhidraProcessExitedException(exitValue);
}
catch (IllegalThreadStateException e) {
// this is okay, ghidraRun hasn't exited
}
}
}
@Override
public void startChecking(int delay, TimeUnit delayTimeUnit) throws StopWaitingException {
dialog.reset();
try {
// start thread to check frequently
super.startChecking(delay, delayTimeUnit);
// start thread for showing dialog
executor.schedule(
new CheckForListenerRunnable(executor, process, WAIT_FOR_LISTENER_PERIOD_MS,
TimeUnit.MILLISECONDS, checkCondition),
WAIT_FOR_LISTENER_DELAY_MS, TimeUnit.MILLISECONDS);
}
catch (RejectedExecutionException e) {
if (dialog.isAnsweredNo()) {
throw new StopWaitingException();
}
}
}
@Override
public void awaitTermination()
throws StopWaitingException, StartedGhidraProcessExitedException {
try {
executor.awaitTermination(MAX_WAIT_FOR_LISTENER_MIN, TimeUnit.MINUTES);
if (dialog.isAnsweredNo()) {
throw new StopWaitingException();
}
if (this.stopWaitingException != null) {
throw this.stopWaitingException;
}
if (this.startedGhidraProcessExitedException != null) {
throw this.startedGhidraProcessExitedException;
}
}
catch (InterruptedException e) {
// this is okay
}
}
}

View file

@ -0,0 +1,71 @@
/* ###
* 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.app.plugin.core.go.ipc;
import java.util.concurrent.*;
import ghidra.app.plugin.core.go.dialog.GhidraGoWaitForListenerDialog;
import ghidra.app.plugin.core.go.exception.StartedGhidraProcessExitedException;
import ghidra.app.plugin.core.go.exception.StopWaitingException;
import ghidra.util.Swing;
public abstract class CheckPeriodicallyRunnable implements Runnable {
protected static GhidraGoWaitForListenerDialog dialog = new GhidraGoWaitForListenerDialog();
protected boolean showDialog;
protected int period;
protected TimeUnit timeUnit;
protected ScheduledExecutorService executor;
protected Callable<Boolean> checkCondition;
public CheckPeriodicallyRunnable(boolean showDialog,
int period, TimeUnit timeUnit, Callable<Boolean> checkCondition) {
this.showDialog = showDialog;
this.period = period;
this.timeUnit = timeUnit;
this.checkCondition = checkCondition;
// two threads; one for checking quickly without showing dialog, another for showing dialog
this.executor = Executors.newScheduledThreadPool(2);
}
protected CheckPeriodicallyRunnable(ScheduledExecutorService executor, boolean showDialog,
int period,
TimeUnit timeUnit, Callable<Boolean> checkCondition) {
this(showDialog, period, timeUnit, checkCondition);
this.executor = executor;
}
/**
* Begins checking the check condition in a thread
* @param delay the amount of time to wait to being checking
* @param delayTimeUnit the units for the amount of time
* @throws StopWaitingException in the event a dialog is answered to stop waiting
*/
public void startChecking(int delay,
TimeUnit delayTimeUnit) throws StopWaitingException {
executor.schedule(this, delay, delayTimeUnit);
}
public abstract void awaitTermination()
throws StopWaitingException, StartedGhidraProcessExitedException;
public void dispose() {
executor.shutdownNow();
Swing.runNow(dialog::close);
}
}

View file

@ -0,0 +1,109 @@
/* ###
* 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.app.plugin.core.go.ipc;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.*;
import java.nio.file.Path;
import java.util.function.Supplier;
import ghidra.app.plugin.core.go.exception.UnableToGetLockException;
import ghidra.framework.Application;
import ghidra.util.Msg;
import ghidra.util.Swing;
import utilities.util.FileUtilities;
/**
* Ghidra Go Inter-Process Communication
*/
public abstract class GhidraGoIPC {
protected final Path channelPath =
Path.of(Application.getUserTempDirectory().getPath(), "ghidraGo");
protected final Path urlFilesPath = channelPath.resolve("urls");
protected final Path listenerLockPath = channelPath.resolve("listenerLock");
protected final Path listenerReadyLockPath = channelPath.resolve("listenerReadyLock");
protected final Path senderLockPath = channelPath.resolve("senderLock");
protected GhidraGoIPC() throws IOException {
// make the directories that will be needed
try {
FileUtilities.checkedMkdir(channelPath.toFile());
FileUtilities.checkedMkdir(urlFilesPath.toFile());
}
catch (IOException e) {
Msg.error(this, "Unable to create IPC directories.");
throw e;
}
}
public abstract void dispose();
/**
* @return true if a Ghidra is listening and ready. false otherwise
*/
public boolean isGhidraListening() {
if (listenerLockPath.toFile().exists() && listenerReadyLockPath.toFile().exists()) {
return isFileLocked(listenerLockPath) && isFileLocked(listenerReadyLockPath);
}
return false;
}
private boolean isFileLocked(Path lockPath) {
try {
return !doLockedAction(lockPath, false, () -> true);
}
catch (OverlappingFileLockException | UnableToGetLockException e) {
return true;
}
}
/**
* perform the given action after acquiring the client lock successfully. This method is used
* to ensure that only one actor for the given lock path is performing the action.
* @param lockPath the path of the file to lock
* @param action the action taken after acquiring the lock.
* @param waitForLock if true blocks until the lock is acquired. otherwise, if the lock can't be
* acquired, the method returns false and does not do any blocking actions
* @return true if the action succeeded. false otherwise.
* @throws OverlappingFileLockException if another process currently controls the lock
* @throws UnableToGetLockException if the lock was unobtainable
*/
public static boolean doLockedAction(Path lockPath, boolean waitForLock,
Supplier<Boolean> action)
throws OverlappingFileLockException, UnableToGetLockException {
try (FileOutputStream fos = new FileOutputStream(lockPath.toFile());
FileChannel channel = fos.getChannel();
FileLock lock = waitForLock ? channel.lock() : channel.tryLock();) {
if (lock == null) {
throw new UnableToGetLockException();
}
return action.get();
}
catch (FileLockInterruptionException e) {
// this is okay, user interrupted locking action
}
catch (IOException e) {
Swing.runNow(
() -> Msg.showError(GhidraGoIPC.class, null, "Could not perform exclusive action",
"Another process is currently holding the lock at " + lockPath, e));
}
return false;
}
}

View file

@ -0,0 +1,182 @@
/* ###
* 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.app.plugin.core.go.ipc;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.URL;
import java.nio.channels.FileLockInterruptionException;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.*;
import java.util.function.Consumer;
import org.apache.commons.lang3.StringUtils;
import ghidra.app.plugin.core.go.exception.UnableToGetLockException;
import ghidra.framework.main.AppInfo;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.util.Msg;
import ghidra.util.Swing;
public class GhidraGoListener extends GhidraGoIPC implements Runnable {
public static int WAIT_FOR_ACTIVE_PROJECT_TIMEOUT_S = 30;
private Thread t;
private Consumer<URL> onNewUrl;
/**
* Begin listening for urls in a non-blocking thread. If a listener already exists, the thread
* will wait until no listener exists and attempt to get the lock. Once the lock has been acquired
* the listener will start watching for new urls and create a ready lock. Upon a new url being found,
* the onNewUrl Consumer will be executed.
* @param onNewUrl consumer method to execute upon finding a new url
* @throws IOException if the Runnable cannot be created
*/
public GhidraGoListener(Consumer<URL> onNewUrl) throws IOException {
super();
this.onNewUrl = onNewUrl;
t = new Thread(this, "GhidraGo Handler");
t.start();
}
@Override
public void run() {
try {
doLockedAction(listenerLockPath, true, () -> {
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
urlFilesPath.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
Msg.info(this, "Listening for GhidraGo Requests.");
doLockedAction(listenerReadyLockPath, true, () -> {
try {
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
// only process events that are not null. null events could happen
// when event.kind() is OVERFLOW.
if (event.context() != null) {
Msg.trace(this, event.context() + " is a new file!");
// get the url from the new url
Path urlFilePath =
urlFilesPath.resolve(event.context().toString());
URL url = getGhidraURL(urlFilePath);
urlFilePath.toFile().delete();
if (url != null) {
onNewUrl.accept(url);
}
}
}
key.reset();
}
}
catch (InterruptedException e) {
// watch service interrupted
}
return true;
});
}
catch (FileLockInterruptionException | InterruptedIOException e) {
return false;
}
catch (IOException | UnableToGetLockException e) {
Swing.runNow(() -> Msg.showError(this, null,
"GhidraGo Unable to Watch for New GhidraURL's", e));
return false;
}
catch (ClosedWatchServiceException e) {
// do nothing
}
finally {
Msg.info(this, "No longer listening for GhidraGo Requests.");
}
return true;
});
}
catch (OverlappingFileLockException | UnableToGetLockException e) {
Swing.runNow(
() -> Msg.showError(this, null, "GhidraGo Unable to Watch for New GhidraURL's", e));
}
}
/**
* Returns a URL given the first argument from GhidraGo.
* @param ghidraGoArgument could be a GhidraURL or a projectFilePath.
* @return the GhidraURL to a program
* @throws IllegalArgumentException in the event the given GhidraGo argument is invalid
*/
private URL toURL(String ghidraGoArgument) throws IllegalArgumentException {
try {
if (ghidraGoArgument.startsWith(GhidraURL.PROTOCOL + ":?")) {
String projectFilePath =
ghidraGoArgument.substring(ghidraGoArgument.indexOf("?") + 1);
if (!projectFilePath.startsWith("/")) {
projectFilePath = "/" + projectFilePath;
}
return GhidraURL.makeURL(AppInfo.getActiveProject().getProjectLocator(),
projectFilePath, null);
}
return GhidraURL.toURL(ghidraGoArgument);
}
catch (IllegalArgumentException e) {
if (ghidraGoArgument.startsWith(GhidraURL.PROTOCOL + "://") ||
AppInfo.getActiveProject() == null)
throw e;
if (!ghidraGoArgument.startsWith("/")) {
ghidraGoArgument = "/" + ghidraGoArgument;
}
return GhidraURL.makeURL(AppInfo.getActiveProject().getProjectLocator(),
ghidraGoArgument, null);
}
}
/**
* Reads the url file for the url string and returns it.
* @param urlFilePath the path for the url file
* @return the url string, or null if the file cannot be read.
*/
private URL getGhidraURL(Path urlFilePath) {
try {
String urlContents = new String(Files.readAllBytes(urlFilePath));
if (StringUtils.isEmpty(urlContents)) {
Swing.runNow(() -> Msg.showError(GhidraGoIPC.class, null,
"GhidraGo Empty GhidraURL Read",
"The GhidraURL read from url file was null or empty. This should not happen, " +
"ensure ghidraGo is being used properly."));
return null;
}
return toURL(urlContents);
}
catch (IOException e) {
Swing.runNow(() -> Msg.showError(GhidraGoIPC.class, null, "GhidraGo Error",
"Failed to read the url from " + urlFilePath, e));
}
catch (IllegalArgumentException e) {
Swing.runNow(
() -> Msg.showError(GhidraGoIPC.class, null, "GhidraGo Illegal Argument Given", e));
}
return null;
}
@Override
public void dispose() {
if (t != null) {
t.interrupt();
}
}
}

View file

@ -0,0 +1,135 @@
/* ###
* 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.app.plugin.core.go;
import static org.junit.Assert.*;
import java.net.URL;
import java.util.Arrays;
import java.util.Optional;
import java.util.function.Predicate;
import org.junit.*;
import docking.AbstractErrDialog;
import ghidra.GhidraApplicationLayout;
import ghidra.GhidraGo;
import ghidra.app.plugin.core.go.ipc.CheckForFileProcessedRunnable;
import ghidra.app.plugin.core.go.ipc.CheckForListenerRunnable;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.program.model.listing.Program;
import ghidra.test.*;
import ghidra.util.Swing;
import ghidra.util.task.TaskMonitor;
public class GhidraGoPluginTest extends AbstractGhidraHeadedIntegrationTest {
private TestEnv env;
private PluginTool tool;
private GhidraGo ghidraGo;
private URL url;
private GhidraApplicationLayout layout;
@Before
public void setUp() throws Exception {
env = new TestEnv();
tool = env.getFrontEndTool();
tool.addPlugin(GhidraGoPlugin.class.getName());
showTool(tool);
DomainFolder rootFolder = env.getProject().getProjectData().getRootFolder();
Program p = createNotepadProgram();
rootFolder.createFile("notepad", p, TaskMonitor.DUMMY);
env.release(p);
url = GhidraURL.makeURL(env.getProjectManager().getActiveProject().getProjectLocator(),
"/notepad", null);
layout = (GhidraApplicationLayout) createApplicationLayout();
ghidraGo = new GhidraGo();
CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_DELAY_MS = 1000;
CheckForFileProcessedRunnable.MAX_WAIT_FOR_PROCESSING_MIN = 1;
CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_PERIOD_MS = 10;
CheckForListenerRunnable.WAIT_FOR_LISTENER_DELAY_MS = 1000;
CheckForListenerRunnable.MAX_WAIT_FOR_LISTENER_MIN = 1;
CheckForListenerRunnable.WAIT_FOR_LISTENER_PERIOD_MS = 10;
}
private Program createNotepadProgram() throws Exception {
ClassicSampleX86ProgramBuilder builder =
new ClassicSampleX86ProgramBuilder("notepad", false, this);
return builder.getProgram();
}
@After
public void tearDown() {
env.dispose();
}
@Test
public void testProcessingUrl() throws Exception {
Swing.runLater(() -> {
try {
ghidraGo.launch(layout, new String[] { url.toString() });
}
catch (Exception e) {
// empty
}
});
waitForSwing();
waitFor(() -> Arrays.asList(tool.getToolServices().getRunningTools())
.stream()
.map(PluginTool::getName)
.anyMatch(Predicate.isEqual("CodeBrowser")));
Optional<PluginTool> cb = Arrays.asList(tool.getToolServices().getRunningTools())
.stream()
.filter(p -> p.getName().equals("CodeBrowser"))
.findFirst();
assertTrue(cb.isPresent());
assertTrue(Arrays.asList(cb.get().getDomainFiles())
.stream()
.map(DomainFile::getName)
.anyMatch(Predicate.isEqual("notepad")));
}
@Test
public void testLaunchingWithInvalidUrl() throws Exception {
Swing.runLater(() -> {
try {
ghidraGo.launch(layout, new String[] { "ghidra:/test" });
}
catch (Exception e) {
// empty
}
});
AbstractErrDialog err = waitForErrorDialog();
assertEquals("Unsupported Content", err.getTitle());
}
}

View file

@ -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.app.plugin.core.go.ipc;
import static org.junit.Assert.*;
import java.io.IOException;
import org.junit.*;
import docking.DialogComponentProvider;
import ghidra.app.plugin.core.go.GhidraGoSender;
import ghidra.app.plugin.core.go.dialog.GhidraGoWaitForListenerDialog;
import ghidra.app.plugin.core.go.exception.StopWaitingException;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
import ghidra.util.Msg;
public class GhidraGoIPCTest extends AbstractGhidraHeadedIntegrationTest {
private GhidraGoSender sender;
private GhidraGoListener listener;
private String url = "ghidra://testing/testProject";
private TestEnv env;
public GhidraGoIPCTest() throws IOException {
sender = new GhidraGoSender();
}
@Before
public void setUp() throws IOException {
env = new TestEnv(); // need this so that Application is initialized
CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_DELAY_MS = 1000;
CheckForFileProcessedRunnable.MAX_WAIT_FOR_PROCESSING_MIN = 1;
CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_PERIOD_MS = 10;
CheckForListenerRunnable.WAIT_FOR_LISTENER_DELAY_MS = 1000;
CheckForListenerRunnable.MAX_WAIT_FOR_LISTENER_MIN = 1;
CheckForListenerRunnable.WAIT_FOR_LISTENER_PERIOD_MS = 10;
}
@After
public void tearDown() {
if (env != null) {
env.dispose();
}
sender.dispose();
if (listener != null) {
listener.dispose();
}
waitFor(() -> !sender.isGhidraListening());
}
public Thread sendExpectingStopWaitingException() {
Thread t = new Thread(() -> {
try {
sender.send(url);
assertFalse(true); // fail
}
catch (StopWaitingException e) {
// passed
}
});
t.start();
return t;
}
public Thread sendExpectingSuccess() {
Thread t = new Thread(() -> {
try {
sender.send(url);
// passed
}
catch (StopWaitingException e) {
assertFalse(true); // fail
}
});
t.start();
return t;
}
@Test
public void testSendingWithNoListener() throws InterruptedException {
// given no listener is listening and the timeout is 0
waitFor(() -> !sender.isGhidraListening());
CheckForListenerRunnable.WAIT_FOR_LISTENER_DELAY_MS = 0;
CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_DELAY_MS = 0;
// then a the wait for listener dialog should appear on send
Thread t = sendExpectingStopWaitingException();
DialogComponentProvider dialog =
waitForDialogComponent(GhidraGoWaitForListenerDialog.class);
// when Wait is pressed, the dialog should reappear with no timeout
pressButtonByText(dialog, "Wait");
// then pressing No when the dialog appears again should stop waiting
dialog = waitForDialogComponent(GhidraGoWaitForListenerDialog.class);
pressButtonByText(dialog, "No");
t.join();
}
@Test
public void testSendingWithListener() throws IOException, InterruptedException {
// given a listener is listening and processing new urls
listener = new GhidraGoListener((passedURL) -> {
Msg.info(this, "Found " + passedURL + " in test");
});
waitFor(sender::isGhidraListening);
// then the sender should not throw an exception when sending a url
Thread t = sendExpectingSuccess();
t.join();
}
@Test
public void testInterruptingListener() throws IOException, InterruptedException {
// given a listener is listening and processing new urls
listener = new GhidraGoListener((passedURL) -> {
Msg.info(this, "Found " + passedURL + " in test");
});
waitFor(sender::isGhidraListening);
// then sending a url before disposing the listener should succeed
Thread t = sendExpectingSuccess();
t.join();
// when the listener is disposed
listener.dispose();
// given no listener is listening and the timeout is 0
waitFor(() -> !sender.isGhidraListening());
CheckForListenerRunnable.WAIT_FOR_LISTENER_DELAY_MS = 0;
CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_DELAY_MS = 0;
// then sending a url should fail
t = sendExpectingStopWaitingException();
DialogComponentProvider dialog =
waitForDialogComponent(GhidraGoWaitForListenerDialog.class);
pressButtonByText(dialog, "No");
t.join();
}
}

View file

@ -0,0 +1,229 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<!-- Keep pre text from wrapping so that it is formatted exactly as we have it -->
<style>
pre {
white-space: no-wrap;
font-family: 'Courier New', 'Courier';
}
typewriter {
font-family: 'Courier New', 'Courier';
}
/* Make the general text a bit more readable */
body {
font-size: 20px;
}
</style>
<html>
<head>
<title>GhidraGo README</title>
</head>
<body>
<h1 align="center"><a name="top">GhidraGo README</a></h1>
<h2>Table of Contents</h2>
<UL>
<LI><a href="#general">Introduction</a></LI>
<ul><li><a href="#example">Example</a></li></ul>
<LI><a href="#plugin">Configure GhidraGo Plugin</a></LI>
<li>
<a href="#configure">Configure Protocol Handler (Platform Specific)</a>
<ul>
<LI><a href="#windows">Windows</a></LI>
<LI><a href="#linux">Linux</a></LI>
<LI><a href="#mac">Mac</a></LI>
</ul>
</li>
</UL>
<div style="border-top: 4px double; margin-top: 1em; padding-top: 1em;"> </div>
<h2><a name="general">GhidraGo Introduction</a></h2>
<div>
<p>
GhidraGo is a mechanism to cause Ghidra to display a previously imported program within a
local or multi-user project using a ghidraURL hyperlink similar to an http reference. In
practice ghidraURL's work very similarly to selecting a URL reference which displays a PDF.
Once setup correctly, GhidraURL links can be placed in web pages, external project
documentation files, or any other place a URL hyperlink can be placed.
</p>
<p>
When a GhidraURL is selected, GhidraGo will startup Ghidra if it isn't already running as
well as prompt to login to the multi-user project if necessary. The program is displayed in
the default tool, usually the codebrowser, and can be configured to re-use an open default
tool or to use a new default tool. The GhidraURL must currently be locating a DomainFile
that is either in a Remote, Shared project, or a local project.
</p>
<p>
GhidraGo is a combination of a command line program to send a link, a plugin running within
the Ghidra project manager, and the configuration of the default handling for the ghidraURL
within the user environment. The ghidraURL is sent as the first and only parameter to the
ghidraGo command line interface.
</p>
<p>
GhidraGo passes information through a simple filesystem mechanism vice an open port for
security and simplicity. GhidraGo works on Windows, Linux, and MacOS.
</p>
<p>
<p>GhidraURL's have the format:</p>
<div style="margin-left: 25px">
<p>
Remote Ghidra Server File:
ghidra://&lt;host&gt;[:&lt;port&gt;]/&lt;repository-name&gt;/&lt;program-path&gt;
[#&lt;address-or-symbol-ref&gt;]
</p>
<p style="margin-left: 25px">Example: ghidra://hostname/Repo/notepad.exe#main</p>
<p>
Local Ghidra Project File:
ghidra:/[&lt;project-path&gt;/]&lt;project-name&gt;?/&lt;program-path&gt;
[#&lt;address-or-symbol-ref&gt;]
</p>
<p style="margin-left: 25px">Example: ghidra:/share/MyProject?/notepad.exe#main</p>
</div>
</p>
</div>
<h3><a name="example">Example of Using ghidraGo CLI</a></h3>
<div>
<p><code>ghidraGo ghidra://ghidra-server/project/myProgram#symbol</code></p>
<p>Executing this command will result in the program called <code>myProgram</code> being
opened in Ghidra's default tool with the cursor at <code>symbol</code>.</p>
</div>
<h2><a name="plugin">Configure GhidraGo Plugin</a></h2>
<div>
<ol>
<li>Start Ghidra</li>
<li>Choose File &gt; Configuration in the Project Window (not the Codebrowser Window)</li>
<li>Click the Plug Icon in the upper right to display all plugins</li>
<li>Search for GhidraGoPlugin and select it</li>
<li>Press OK</li>
</ol>
<p>Ghidra is now configured to listen to GhidraGo Requests. You can execute a GhidraGo request
using the "ghidraGo" shell/batch script in
<code>/path/to/ghidra/support/GhidraGo/ghidraGo</code></p>
</div>
<div>
<h2><a name="configure">Configure Protocol Handler (Platform Specific)</a></h2>
<div>
<p>
Configuring your platform to handle the <script>ghidra</script> protocol is what
enables the ghidraGo command line interface to be associated with a ghidraURL. Once
configured, clicking hyperlinks that start with the <script>ghidra</script> protocol
will execute the ghidraGo CLI with that hyperlink as the first argument. The
configuration is platform specific.
</p>
<p>
*NOTE: changes to your path to ghidra, such as upgrading ghidra to a new version,
will require the path you set in this configuration to be updated.
</p>
</div>
<h3><a name="windows">Windows Protocol Handler Configuration</a></h3>
<div>
<ol>
<li>Go to Start &gt; Find and Type <code>regedit</code></li>
<li>Right click HKEY_CLASSES_ROOT then New &gt; Key</li>
<li>Name the key "ghidra"</li>
<li>Right Click ghidra &gt; New &gt; String Value and add "URL Protocol" without a value</li>
<li>Right Click ghidra &gt; New &gt; Key and create the heiarchy ghidra/shell/open/command</li>
<li>Inside command change (Default) to the path where ghidraGo is located followed by
a "%1". For Example:</li>
<br />
<code>C:\Path\To\Ghidra\support\GhidraGo\ghidraGo "%1"</code>
</ol>
</div>
<h3><a name="linux">Linux Protocol Handler Configuration</a></h3>
<div>
<p>In Linux, when you click a browser link with an <code>href</code> value to a GhidraURL,
you'll be prompted to use xdg-open.</p>
<ol>
<li>Edit the file <code>ghidra.desktop</code> in <code>~/.local/share/applications</code></li>
<br />
<code>
[Desktop Entry]<br />
Name=ghidra Client<br />
Exec=/path/to/ghidra/support/GhidraGo/ghidraGo "%u"<br />
Type=Application<br />
Terminal=false<br />
MimeType=x-scheme-handler/ghidra;<br />
</code>
<br />
<li>Edit the file mimeapps.list in <code>~/.local/share/applications</code></li>
<br />
<code>
[Default Applications]<br />
x-scheme-handler/ghidra=ghidra.desktop<br />
...<br />
</code>
</ol>
<p>After the steps above, you should be able to click a GhidraURL href, get the same
xdg-open prompt, and upon clicking "Open xdg-open" GhidraGo should execute and open
Ghidra to the given GhidraURL.</p>
</div>
<h3><a name="mac">Mac Protocol Handler Configuration</a></h3>
<div>
<ol>
<li>Open <code>Script Editor</code> and past the following into the editor.</li>
<br />
<code>
on open location schemeUrl<br />
&emsp;set ghidraUrl to quoted form of schemeUrl<br />
&emsp;do shell script "/path/to/ghidraGo " &amp; ghidraUrl<br />
end open location<br />
</code>
<br />
<li>Save the script as an Application named GhidraGo in either
<code>/Applications</code> or <code>~/Applications</code></li>
<li>Right click on the saved Application and click Show Package Contents</li>
<li>Open Contents &gt; Info.plist and under
<code>&lt;string&gt;com.apple.ScriptEditor.id.GhidraGo&lt;/string&gt;</code>
paste the following:</li>
<br />
<code>
&lt;key&gt;CFBundleURLTypes&lt;/key&gt;<br />
&lt;array&gt;<br />
&emsp;&lt;dict&gt;<br />
&emsp;&emsp;&lt;key&gt;CFBundleURLName&lt;/key&gt;<br />
&emsp;&emsp;&lt;string&gt;Ghidra Scheme&lt;/string&gt;<br />
&emsp;&emsp;&lt;key&gt;CFBundleURLSchemes&lt;/key&gt;<br />
&emsp;&emsp;&lt;array&gt;<br />
&emsp;&emsp;&emsp;&lt;string&gt;ghidra&lt;/string&gt;<br />
&emsp;&emsp;&lt;/array&gt;<br />
&emsp;&lt;/dict&gt;<br />
&lt;/array&gt;
</code>
<br />
<li>Go to the Applications folder where you saved the GhidraGo, and Open
GhidraGo (run it once).</li>
</ol>
</div>
</div>
(<a href="#top">Back to Top</a>)
<div style="border-top: 4px double; margin-top: 1em; padding-top: 1em;"> </div>
<address></address>
Last modified: Oct 26 2023
</body> </html>
<style>
table, td {
border: 1px solid black;
}
td {
padding: 15px;
text-align: center;
}
</style>

View file

@ -0,0 +1,16 @@
#!/bin/bash
#
# Command-line script for starting GhidraGo
# launch mode (fg, bg, debug, debug-suspend, debug-suspend-launcher)
LAUNCH_MODE=fg
# Resolve symbolic link if present and get the directory this script lives in.
# NOTE: "readlink -f" is best but works on Linux only, "readlink" will only work if your PWD
# contains the link you are calling (which is the best we can do on macOS), and the "echo" is the
# fallback, which doesn't attempt to do anything with links.
SCRIPT_FILE="$(readlink -f "$0" 2>/dev/null || readlink "$0" 2>/dev/null || echo "$0")"
SCRIPT_DIR="${SCRIPT_FILE%/*}"
# Launch Filesystem Conversion
"${SCRIPT_DIR}"/../launch.sh $LAUNCH_MODE jdk GhidraGo "" "" ghidra.GhidraGo "$@"

View file

@ -0,0 +1,10 @@
:: GhidraGo launch
@echo off
setlocal
:: Launch mode can be changed to one of the following:
:: fg, debug, debug-suspend
set LAUNCH_MODE=fg
call "%~dp0..\launch.bat" %LAUNCH_MODE% jdk GhidraGo "" "" ghidra.GhidraGo "%*"

View file

@ -3,6 +3,7 @@
Common/server/jaas.conf||GHIDRA||||END|
Common/server/server.conf||GHIDRA||||END|
Common/server/svrREADME.html||GHIDRA||||END|
Common/support/GhidraGo/ghidraGoREADME.html||GHIDRA||||END|
Common/support/analyzeHeadlessREADME.html||GHIDRA||||END|
Common/support/buildGhidraJarREADME.txt||GHIDRA||||END|
Common/support/debug.log4j.xml||GHIDRA||||END|
@ -12,6 +13,7 @@ Linux/server/ghidraSvr||GHIDRA||||END|
Linux/server/svrAdmin||GHIDRA||||END|
Linux/server/svrInstall||GHIDRA||||END|
Linux/server/svrUninstall||GHIDRA||||END|
Linux/support/GhidraGo/ghidraGo||GHIDRA||||END|
Linux/support/analyzeHeadless||GHIDRA||||END|
Linux/support/buildGhidraJar||GHIDRA||||END|
Linux/support/buildNatives||GHIDRA||||END|
@ -25,6 +27,7 @@ Windows/server/ghidraSvr.bat||GHIDRA||||END|
Windows/server/svrAdmin.bat||GHIDRA||||END|
Windows/server/svrInstall.bat||GHIDRA||||END|
Windows/server/svrUninstall.bat||GHIDRA||||END|
Windows/support/GhidraGo/ghidraGo.bat||GHIDRA||||END|
Windows/support/README_createPdbXmlFiles.txt||GHIDRA||||END|
Windows/support/analyzeHeadless.bat||GHIDRA||||END|
Windows/support/buildGhidraJar.bat||GHIDRA||||END|