From 83f90d6b3de4dd4dbee0208d2c8308f9011758b5 Mon Sep 17 00:00:00 2001 From: ghidraffe <108089404+ghidraffe@users.noreply.github.com> Date: Fri, 1 Dec 2023 22:49:09 +0000 Subject: [PATCH] GP-2774 Implementation of GhidraGo for Linux, Windows, and Mac --- Ghidra/Features/GhidraGo/Module.manifest | 0 Ghidra/Features/GhidraGo/build.gradle | 30 +++ .../Features/GhidraGo/certification.manifest | 4 + .../src/main/help/help/TOC_Source.xml | 60 +++++ .../help/help/topics/GhidraGo/GhidraGo.html | 233 ++++++++++++++++++ .../src/main/java/ghidra/GhidraGo.java | 168 +++++++++++++ .../app/plugin/core/go/GhidraGoPlugin.java | 106 ++++++++ .../app/plugin/core/go/GhidraGoSender.java | 151 ++++++++++++ .../core/go/dialog/GhidraGoWaitDialog.java | 105 ++++++++ .../dialog/GhidraGoWaitForListenerDialog.java | 29 +++ .../FailedToStartGhidraException.java | 20 ++ .../StartedGhidraProcessExitedException.java | 24 ++ .../go/exception/StopWaitingException.java | 20 ++ .../exception/UnableToGetLockException.java | 20 ++ .../go/ipc/CheckForFileProcessedRunnable.java | 131 ++++++++++ .../core/go/ipc/CheckForListenerRunnable.java | 156 ++++++++++++ .../go/ipc/CheckPeriodicallyRunnable.java | 71 ++++++ .../app/plugin/core/go/ipc/GhidraGoIPC.java | 109 ++++++++ .../plugin/core/go/ipc/GhidraGoListener.java | 182 ++++++++++++++ .../plugin/core/go/GhidraGoPluginTest.java | 135 ++++++++++ .../plugin/core/go/ipc/GhidraGoIPCTest.java | 160 ++++++++++++ .../support/GhidraGo/ghidraGoREADME.html | 229 +++++++++++++++++ .../Linux/support/GhidraGo/ghidraGo | 24 ++ .../Windows/support/GhidraGo/ghidraGo.bat | 19 ++ Ghidra/RuntimeScripts/certification.manifest | 3 + 25 files changed, 2189 insertions(+) create mode 100644 Ghidra/Features/GhidraGo/Module.manifest create mode 100644 Ghidra/Features/GhidraGo/build.gradle create mode 100644 Ghidra/Features/GhidraGo/certification.manifest create mode 100755 Ghidra/Features/GhidraGo/src/main/help/help/TOC_Source.xml create mode 100644 Ghidra/Features/GhidraGo/src/main/help/help/topics/GhidraGo/GhidraGo.html create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/GhidraGo.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoSender.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitDialog.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitForListenerDialog.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/FailedToStartGhidraException.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StartedGhidraProcessExitedException.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StopWaitingException.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/UnableToGetLockException.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForFileProcessedRunnable.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForListenerRunnable.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckPeriodicallyRunnable.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPC.java create mode 100644 Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoListener.java create mode 100644 Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/GhidraGoPluginTest.java create mode 100644 Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPCTest.java create mode 100644 Ghidra/RuntimeScripts/Common/support/GhidraGo/ghidraGoREADME.html create mode 100755 Ghidra/RuntimeScripts/Linux/support/GhidraGo/ghidraGo create mode 100644 Ghidra/RuntimeScripts/Windows/support/GhidraGo/ghidraGo.bat diff --git a/Ghidra/Features/GhidraGo/Module.manifest b/Ghidra/Features/GhidraGo/Module.manifest new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Ghidra/Features/GhidraGo/build.gradle b/Ghidra/Features/GhidraGo/build.gradle new file mode 100644 index 0000000000..4a9f530a93 --- /dev/null +++ b/Ghidra/Features/GhidraGo/build.gradle @@ -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') +} + diff --git a/Ghidra/Features/GhidraGo/certification.manifest b/Ghidra/Features/GhidraGo/certification.manifest new file mode 100644 index 0000000000..69125eb9f8 --- /dev/null +++ b/Ghidra/Features/GhidraGo/certification.manifest @@ -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| diff --git a/Ghidra/Features/GhidraGo/src/main/help/help/TOC_Source.xml b/Ghidra/Features/GhidraGo/src/main/help/help/TOC_Source.xml new file mode 100755 index 0000000000..3c42358bb2 --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/help/help/TOC_Source.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + diff --git a/Ghidra/Features/GhidraGo/src/main/help/help/topics/GhidraGo/GhidraGo.html b/Ghidra/Features/GhidraGo/src/main/help/help/topics/GhidraGo/GhidraGo.html new file mode 100644 index 0000000000..0194d6907f --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/help/help/topics/GhidraGo/GhidraGo.html @@ -0,0 +1,233 @@ + + + + + + + + +GhidraGo README + + + + + + +

GhidraGo README

+ +

Table of Contents

+ + +
+ +

GhidraGo Introduction

+
+

+ 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. +

+

+ 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. +

+

+ 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. +

+

+ GhidraGo passes information through a simple filesystem mechanism vice an open port for + security and simplicity. GhidraGo works on Windows, Linux, and MacOS. +

+

+

GhidraURL's have the format:

+
+

+ Remote Ghidra Server File: + ghidra://<host>[:<port>]/<repository-name>/<program-path> + [#<address-or-symbol-ref>] +

+

Example: ghidra://hostname/Repo/notepad.exe#main

+ +

+ Local Ghidra Project File: + ghidra:/[<project-path>/]<project-name>?/<program-path> + [#<address-or-symbol-ref>] +

+

Example: ghidra:/share/MyProject?/notepad.exe#main

+
+

+
+ +

Example of Using ghidraGo CLI

+
+

ghidraGo ghidra://ghidra-server/project/myProgram#symbol

+

Executing this command will result in the program called myProgram being + opened in Ghidra's default tool with the cursor at symbol.

+
+ +

Configure GhidraGo Plugin

+
+
    +
  1. Start Ghidra
  2. +
  3. Choose File > Configuration in the Project Window (not the Codebrowser Window)
  4. +
  5. Click the Plug Icon in the upper right to display all plugins
  6. +
  7. Search for GhidraGoPlugin and select it
  8. +
  9. Press OK
  10. +
+

Ghidra is now configured to listen to GhidraGo Requests. You can execute a GhidraGo request + using the "ghidraGo" shell/batch script in + /path/to/ghidra/support/GhidraGo/ghidraGo

+
+ +
+

Configure Protocol Handler (Platform Specific)

+
+

+ Configuring your platform to handle the protocol is what + enables the ghidraGo command line interface to be associated with a ghidraURL. Once + configured, clicking hyperlinks that start with the protocol + will execute the ghidraGo CLI with that hyperlink as the first argument. The + configuration is platform specific. +

+

+ *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. +

+
+ +

Windows Protocol Handler Configuration

+
+
    +
  1. Go to Start > Find and Type regedit
  2. +
  3. Right click HKEY_CLASSES_ROOT then New > Key
  4. +
  5. Name the key "ghidra"
  6. +
  7. Right Click ghidra > New > String Value and add "URL Protocol" without a value
  8. +
  9. Right Click ghidra > New > Key and create the heiarchy ghidra/shell/open/command
  10. +
  11. Inside command change (Default) to the path where ghidraGo is located followed by + a "%1". For Example:
  12. +
    + C:\Path\To\Ghidra\support\GhidraGo\ghidraGo "%1" +
+
+ +

Linux Protocol Handler Configuration

+
+

In Linux, when you click a browser link with an href value to a GhidraURL, + you'll be prompted to use xdg-open.

+
    +
  1. Edit the file ghidra.desktop in ~/.local/share/applications
  2. +
    + + [Desktop Entry]
    + Name=ghidra Client
    + Exec=/path/to/ghidra/support/GhidraGo/ghidraGo "%u"
    + Type=Application
    + Terminal=false
    + MimeType=x-scheme-handler/ghidra;
    +
    +
    +
  3. Edit the file mimeapps.list in ~/.local/share/applications
  4. +
    + + [Default Applications]
    + x-scheme-handler/ghidra=ghidra.desktop
    + ...
    +
    +
+

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.

+
+ +

Mac Protocol Handler Configuration

+
+
    +
  1. Open Script Editor and past the following into the editor.
  2. +
    + + on open location schemeUrl
    +  set ghidraUrl to quoted form of schemeUrl
    +  do shell script "/path/to/ghidraGo " & ghidraUrl
    + end open location
    +
    +
    +
  3. Save the script as an Application named GhidraGo in either + /Applications or ~/Applications
  4. +
  5. Right click on the saved Application and click Show Package Contents
  6. +
  7. Open Contents > Info.plist and under + <string>com.apple.ScriptEditor.id.GhidraGo</string> + paste the following:
  8. +
    + + <key>CFBundleURLTypes</key>
    + <array>
    +  <dict>
    +   <key>CFBundleURLName</key>
    +   <string>Ghidra Scheme</string>
    +   <key>CFBundleURLSchemes</key>
    +   <array>
    +    <string>ghidra</string>
    +   </array>
    +  </dict>
    + </array> +
    +
    +
  9. Go to the Applications folder where you saved the GhidraGo, and Open + GhidraGo (run it once).
  10. +
+
+
+ +(Back to Top) + +
+
+Last modified: Oct 26 2023 + + + diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/GhidraGo.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/GhidraGo.java new file mode 100644 index 0000000000..e37ad4b395 --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/GhidraGo.java @@ -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.*; + +/** + *

GhidraGo Client

+ *

The first argument is expected to be non-null and a valid {@link GhidraURL}

+ *

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.

+ *

A valid {@link GhidraURL} in this case must be pointing to a remote (shared project) + * Program.

+ *

In the event that a Ghidra is running and does not have an active project, the URL cannot be + * processed.

+ */ +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()); + } +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java new file mode 100644 index 0000000000..70d8647115 --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoPlugin.java @@ -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); + } + } +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoSender.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoSender.java new file mode 100644 index 0000000000..c06550d83d --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/GhidraGoSender.java @@ -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 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(); + } + } +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitDialog.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitDialog.java new file mode 100644 index 0000000000..a473ee9f38 --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitDialog.java @@ -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; + } +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitForListenerDialog.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitForListenerDialog.java new file mode 100644 index 0000000000..45d279ad02 --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/dialog/GhidraGoWaitForListenerDialog.java @@ -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); + } + +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/FailedToStartGhidraException.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/FailedToStartGhidraException.java new file mode 100644 index 0000000000..c8dd6eb1c3 --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/FailedToStartGhidraException.java @@ -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 +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StartedGhidraProcessExitedException.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StartedGhidraProcessExitedException.java new file mode 100644 index 0000000000..a1318238ee --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StartedGhidraProcessExitedException.java @@ -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); + } + +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StopWaitingException.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StopWaitingException.java new file mode 100644 index 0000000000..2f8e2b91ce --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/StopWaitingException.java @@ -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 +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/UnableToGetLockException.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/UnableToGetLockException.java new file mode 100644 index 0000000000..1d4313dd5d --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/exception/UnableToGetLockException.java @@ -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 +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForFileProcessedRunnable.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForFileProcessedRunnable.java new file mode 100644 index 0000000000..01d39e44e1 --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForFileProcessedRunnable.java @@ -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 + } + } +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForListenerRunnable.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForListenerRunnable.java new file mode 100644 index 0000000000..4421ff8a6b --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckForListenerRunnable.java @@ -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 checkCondition) { + super(false, period, timeUnit, checkCondition); + this.process = p; + } + + private CheckForListenerRunnable(ScheduledExecutorService executor, Process p, int period, + TimeUnit timeUnit, Callable 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 + } + } + +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckPeriodicallyRunnable.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckPeriodicallyRunnable.java new file mode 100644 index 0000000000..5d223965ce --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/CheckPeriodicallyRunnable.java @@ -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 checkCondition; + + public CheckPeriodicallyRunnable(boolean showDialog, + int period, TimeUnit timeUnit, Callable 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 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); + } +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPC.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPC.java new file mode 100644 index 0000000000..62c6db1083 --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPC.java @@ -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 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; + } + +} diff --git a/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoListener.java b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoListener.java new file mode 100644 index 0000000000..a2257d78e6 --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/main/java/ghidra/app/plugin/core/go/ipc/GhidraGoListener.java @@ -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 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 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(); + } + } + +} diff --git a/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/GhidraGoPluginTest.java b/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/GhidraGoPluginTest.java new file mode 100644 index 0000000000..1e78a4c725 --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/GhidraGoPluginTest.java @@ -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 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()); + } + +} diff --git a/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPCTest.java b/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPCTest.java new file mode 100644 index 0000000000..0f213304c2 --- /dev/null +++ b/Ghidra/Features/GhidraGo/src/test.slow/java/ghidra/app/plugin/core/go/ipc/GhidraGoIPCTest.java @@ -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(); + } +} diff --git a/Ghidra/RuntimeScripts/Common/support/GhidraGo/ghidraGoREADME.html b/Ghidra/RuntimeScripts/Common/support/GhidraGo/ghidraGoREADME.html new file mode 100644 index 0000000000..2c6fc3e372 --- /dev/null +++ b/Ghidra/RuntimeScripts/Common/support/GhidraGo/ghidraGoREADME.html @@ -0,0 +1,229 @@ + + + + + + + +GhidraGo README + + + +

GhidraGo README

+ +

Table of Contents

+ + +
+ +

GhidraGo Introduction

+
+

+ 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. +

+

+ 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. +

+

+ 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. +

+

+ GhidraGo passes information through a simple filesystem mechanism vice an open port for + security and simplicity. GhidraGo works on Windows, Linux, and MacOS. +

+

+

GhidraURL's have the format:

+
+

+ Remote Ghidra Server File: + ghidra://<host>[:<port>]/<repository-name>/<program-path> + [#<address-or-symbol-ref>] +

+

Example: ghidra://hostname/Repo/notepad.exe#main

+ +

+ Local Ghidra Project File: + ghidra:/[<project-path>/]<project-name>?/<program-path> + [#<address-or-symbol-ref>] +

+

Example: ghidra:/share/MyProject?/notepad.exe#main

+
+

+
+ +

Example of Using ghidraGo CLI

+
+

ghidraGo ghidra://ghidra-server/project/myProgram#symbol

+

Executing this command will result in the program called myProgram being + opened in Ghidra's default tool with the cursor at symbol.

+
+ +

Configure GhidraGo Plugin

+
+
    +
  1. Start Ghidra
  2. +
  3. Choose File > Configuration in the Project Window (not the Codebrowser Window)
  4. +
  5. Click the Plug Icon in the upper right to display all plugins
  6. +
  7. Search for GhidraGoPlugin and select it
  8. +
  9. Press OK
  10. +
+

Ghidra is now configured to listen to GhidraGo Requests. You can execute a GhidraGo request + using the "ghidraGo" shell/batch script in + /path/to/ghidra/support/GhidraGo/ghidraGo

+
+ +
+

Configure Protocol Handler (Platform Specific)

+
+

+ Configuring your platform to handle the protocol is what + enables the ghidraGo command line interface to be associated with a ghidraURL. Once + configured, clicking hyperlinks that start with the protocol + will execute the ghidraGo CLI with that hyperlink as the first argument. The + configuration is platform specific. +

+

+ *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. +

+
+ +

Windows Protocol Handler Configuration

+
+
    +
  1. Go to Start > Find and Type regedit
  2. +
  3. Right click HKEY_CLASSES_ROOT then New > Key
  4. +
  5. Name the key "ghidra"
  6. +
  7. Right Click ghidra > New > String Value and add "URL Protocol" without a value
  8. +
  9. Right Click ghidra > New > Key and create the heiarchy ghidra/shell/open/command
  10. +
  11. Inside command change (Default) to the path where ghidraGo is located followed by + a "%1". For Example:
  12. +
    + C:\Path\To\Ghidra\support\GhidraGo\ghidraGo "%1" +
+
+ +

Linux Protocol Handler Configuration

+
+

In Linux, when you click a browser link with an href value to a GhidraURL, + you'll be prompted to use xdg-open.

+
    +
  1. Edit the file ghidra.desktop in ~/.local/share/applications
  2. +
    + + [Desktop Entry]
    + Name=ghidra Client
    + Exec=/path/to/ghidra/support/GhidraGo/ghidraGo "%u"
    + Type=Application
    + Terminal=false
    + MimeType=x-scheme-handler/ghidra;
    +
    +
    +
  3. Edit the file mimeapps.list in ~/.local/share/applications
  4. +
    + + [Default Applications]
    + x-scheme-handler/ghidra=ghidra.desktop
    + ...
    +
    +
+

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.

+
+ +

Mac Protocol Handler Configuration

+
+
    +
  1. Open Script Editor and past the following into the editor.
  2. +
    + + on open location schemeUrl
    +  set ghidraUrl to quoted form of schemeUrl
    +  do shell script "/path/to/ghidraGo " & ghidraUrl
    + end open location
    +
    +
    +
  3. Save the script as an Application named GhidraGo in either + /Applications or ~/Applications
  4. +
  5. Right click on the saved Application and click Show Package Contents
  6. +
  7. Open Contents > Info.plist and under + <string>com.apple.ScriptEditor.id.GhidraGo</string> + paste the following:
  8. +
    + + <key>CFBundleURLTypes</key>
    + <array>
    +  <dict>
    +   <key>CFBundleURLName</key>
    +   <string>Ghidra Scheme</string>
    +   <key>CFBundleURLSchemes</key>
    +   <array>
    +    <string>ghidra</string>
    +   </array>
    +  </dict>
    + </array> +
    +
    +
  9. Go to the Applications folder where you saved the GhidraGo, and Open + GhidraGo (run it once).
  10. +
+
+
+ +(Back to Top) + +
+
+Last modified: Oct 26 2023 + + + diff --git a/Ghidra/RuntimeScripts/Linux/support/GhidraGo/ghidraGo b/Ghidra/RuntimeScripts/Linux/support/GhidraGo/ghidraGo new file mode 100755 index 0000000000..ecbe275fac --- /dev/null +++ b/Ghidra/RuntimeScripts/Linux/support/GhidraGo/ghidraGo @@ -0,0 +1,24 @@ +#!/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")" + +# in dev mode, SCRIPT_FILE is Ghidra/RuntimeScripts_U/Linux/support/GhidraGo/ghidraGo +# in release mode, SCRIPT_FILE is support/GhidraGo/ghidraGo +# BASE_DIR is the base directory of ext-u. +# Initally assume to be in release mode. +BASE_DIR="${SCRIPT_FILE%/*}/../.." + +if [ ! -d "$BASE_DIR/Ghidra" ]; then + BASE_DIR="${BASE_DIR}/../../../../ghidra/Ghidra/RuntimeScripts/Linux" +fi + +"${BASE_DIR}"/support/launch.sh $LAUNCH_MODE jdk GhidraGo "" "" ghidra.GhidraGo "$@" diff --git a/Ghidra/RuntimeScripts/Windows/support/GhidraGo/ghidraGo.bat b/Ghidra/RuntimeScripts/Windows/support/GhidraGo/ghidraGo.bat new file mode 100644 index 0000000000..091f8bfb8b --- /dev/null +++ b/Ghidra/RuntimeScripts/Windows/support/GhidraGo/ghidraGo.bat @@ -0,0 +1,19 @@ +:: GhidraGo launch + +@echo off +setlocal + +set SCRIPT_FILE=%~dp0% +:: in dev mode, SCRIPT_FILE is Ghidra/RuntimeScripts_U/Windows/support/GhidraGo/ghidraGo +:: in release mode, SCRIPT_FILE is support/GhidraGo/ghidraGo +:: BASE_DIR is the base directory of ext-u +:: Initially assume to be in release mode. +set BASE_DIR=%SCRIPT_FILE:~0,-1%\..\.. + +if not exist %BASE_DIR%\Ghidra ( + :: set base dir to location of windows base script dir + set BASE_DIR=%BASE_DIR%\..\..\..\..\ghidra\Ghidra\RuntimeScripts\Windows +) + +call "%BASE_DIR%\support\launch.bat" fg jdk GhidraGo "" "" ghidra.GhidraGo "%*" + diff --git a/Ghidra/RuntimeScripts/certification.manifest b/Ghidra/RuntimeScripts/certification.manifest index 58661dbd10..52d9b254d8 100644 --- a/Ghidra/RuntimeScripts/certification.manifest +++ b/Ghidra/RuntimeScripts/certification.manifest @@ -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|