Merge remote-tracking branch 'origin/GP-4447_ghidra_AbstractGhidraURLQueryTask--SQUASHED'

This commit is contained in:
ghidra1
2024-03-25 14:54:57 -04:00
15 changed files with 682 additions and 368 deletions
@@ -1025,8 +1025,7 @@ public class BulkSignatures implements AutoCloseable {
}
@Override
protected void process(Program program, TaskMonitor monitor)
throws IOException, LSHException {
protected void process(Program program, TaskMonitor monitor) throws IOException {
// NOTE: task monitor not used by DescriptionManager
String md5string = program.getExecutableMD5();
if ((md5string == null) || (md5string.length() < 10)) {
@@ -1059,9 +1058,8 @@ public class BulkSignatures implements AutoCloseable {
manager.saveXml(fwrite);
}
}
catch (LSHException | IOException e) {
Msg.error(this, e.getMessage());
throw e;
catch (LSHException e) {
throw new IOException("Program signature generation failure: " + e.getMessage());
}
}
}
@@ -1084,8 +1082,7 @@ public class BulkSignatures implements AutoCloseable {
}
@Override
protected void process(Program program, TaskMonitor monitor)
throws IOException, LSHException, DecompileException {
protected void process(Program program, TaskMonitor monitor) throws IOException {
// NOTE: task monitor not used by DescriptionManager
String md5string = program.getExecutableMD5();
if ((md5string == null) || (md5string.length() < 10)) {
@@ -1121,9 +1118,8 @@ public class BulkSignatures implements AutoCloseable {
manager.saveXml(fwrite);
fwrite.close();
}
catch (DecompileException | LSHException | IOException e) {
Msg.error(this, e.getMessage());
throw e;
catch (DecompileException | LSHException e) {
throw new IOException("Program signature generation failure: " + e.getMessage());
}
}
}
@@ -15,13 +15,10 @@
*/
package ghidra.features.bsim.query.ingest;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import ghidra.features.bsim.query.LSHException;
import ghidra.framework.client.NotConnectedException;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.framework.protocol.ghidra.*;
@@ -38,11 +35,11 @@ public abstract class IterateRepository {
* Perform processing on program obtained from repository.
* @param program program obtained from repository
* @param monitor processing task monitor
* @throws Exception if an error occured during processing.
* @throws IOException if an error occured during processing.
* @throws CancelledException if processing was cancelled
*/
protected abstract void process(Program program, TaskMonitor monitor)
throws Exception, CancelledException;
throws IOException, CancelledException;
/**
* Process the specified repository URL
@@ -59,93 +56,32 @@ public abstract class IterateRepository {
throw new MalformedURLException("Unsupported repository URL: " + ghidraURL);
}
URL repoURL = GhidraURL.getProjectURL(ghidraURL);
String path = GhidraURL.getProjectPathname(ghidraURL);
GhidraURLQuery.queryUrl(ghidraURL, new GhidraURLResultHandlerAdapter(true) {
String finalelement = null;
path = path.trim();
if (!path.endsWith("/")) {
int pos = path.lastIndexOf('/');
if (pos >= 0) {
String tmp = path.substring(0, pos + 1);
if (tmp.length() != 0 && !tmp.equals("/")) {
finalelement = path.substring(pos + 1); // A possible file name at the end of the path
path = tmp;
@Override
public void processResult(DomainFolder domainFolder, URL url, TaskMonitor m)
throws IOException, CancelledException {
if (GhidraURL.isServerRepositoryURL(ghidraURL)) {
ghidraURL = new URL(repoURL + path);
}
else {
ghidraURL = new URL(repoURL + "?" + path);
}
}
}
}
try {
GhidraURLConnection c = (GhidraURLConnection) ghidraURL.openConnection();
Msg.debug(IterateRepository.class, "Opening ghidra repository: " + ghidraURL);
Object obj = c.getContent();
if (!(obj instanceof GhidraURLWrappedContent)) {
throw new IOException("Connect to repository folder failed");
}
Object consumer = new Object();
GhidraURLWrappedContent wrappedContent = (GhidraURLWrappedContent) obj;
Object content = null;
try {
content = wrappedContent.getContent(consumer);
if (!(content instanceof DomainFolder)) {
throw new IOException("Connect to repository folder failed");
}
DomainFolder folder = (DomainFolder) content;
int totalFiles = getTotalFileCount(folder);
int totalFiles = getTotalFileCount(domainFolder);
monitor.setMaximum(totalFiles);
monitor.setShowProgressValue(true);
if (finalelement != null) {
DomainFolder subfolder = folder.getFolder(finalelement);
if (subfolder != null) {
folder = subfolder;
// fall thru to the DomainFile and DomainFolder loop
}
else {
DomainFile file = folder.getFile(finalelement);
if (file == null) {
throw new IOException("Bad folder/file element: " + finalelement);
}
process(file, monitor);
return;
}
}
process(folder, monitor);
process(domainFolder, monitor);
}
finally {
if (content != null) {
wrappedContent.release(content, consumer);
}
@Override
public void processResult(DomainFile domainFile, URL url, TaskMonitor m)
throws IOException, CancelledException {
process(domainFile, monitor);
}
}
catch (NotConnectedException e) {
throw new IOException(
"Ghidra repository connection failed (" + repoURL + "): " + e.getMessage());
}
catch (FileNotFoundException e) {
throw new IOException("Repository path not found: " + path);
}
}, monitor);
}
private void process(DomainFolder folder, TaskMonitor monitor)
throws Exception, CancelledException {
throws IOException, CancelledException {
for (DomainFile file : folder.getFiles()) {
monitor.checkCancelled();
@@ -177,7 +113,7 @@ public abstract class IterateRepository {
}
private void process(DomainFile file, TaskMonitor monitor)
throws Exception, CancelledException {
throws IOException, CancelledException {
// Do not follow folder-links or consider program links. Using content type
// to filter is best way to control this. If program links should be considered
@@ -189,12 +125,11 @@ public abstract class IterateRepository {
}
Program program = null;
Object consumer = new Object();
try {
Msg.debug(IterateRepository.class, "Processing " + file.getPathname() + "...");
monitor.setMessage("Processing: " + file.getName());
monitor.incrementProgress(1);
program = (Program) file.getReadOnlyDomainObject(consumer, -1, monitor);
program = (Program) file.getReadOnlyDomainObject(this, -1, monitor);
process(program, monitor);
}
catch (VersionException e) {
@@ -203,7 +138,7 @@ public abstract class IterateRepository {
}
finally {
if (program != null) {
program.release(consumer);
program.release(this);
}
}
}
@@ -48,7 +48,6 @@ import ghidra.framework.remote.User;
import ghidra.framework.store.LockException;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.program.database.ProgramContentHandler;
import ghidra.program.database.ProgramDB;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.listing.Program;
import ghidra.program.util.GhidraProgramUtilities;
@@ -310,52 +309,55 @@ public class HeadlessAnalyzer {
Msg.info(HeadlessAnalyzer.class, "HEADLESS: execution starts");
GhidraURLConnection c = (GhidraURLConnection) ghidraURL.openConnection();
c.setReadOnly(options.readOnly); // writable repository connection
// force explicit folder access since file may have same name as folder
ghidraURL = GhidraURL.getFolderURL(ghidraURL);
if (c.getRepositoryName() == null) {
throw new MalformedURLException("Unsupported repository URL: " + ghidraURL);
}
Msg.info(this, "Opening ghidra repository folder: " + ghidraURL);
Msg.info(this, "Opening ghidra repository project: " + ghidraURL);
Object obj = c.getContent();
if (!(obj instanceof GhidraURLWrappedContent)) {
throw new IOException(
"Connect to repository folder failed. Response code: " + c.getStatusCode());
}
GhidraURLWrappedContent wrappedContent = (GhidraURLWrappedContent) obj;
Object content = null;
try {
content = wrappedContent.getContent(this);
if (!(content instanceof DomainFolder)) {
throw new IOException("Connect to repository folder failed");
}
GhidraURLQuery.queryRepositoryUrl(ghidraURL, options.readOnly,
new GhidraURLResultHandlerAdapter() {
DomainFolder folder = (DomainFolder) content;
project = new HeadlessProject(getProjectManager(), c);
@Override
public void processResult(DomainFolder domainFolder, URL url,
TaskMonitor monitor) throws IOException, CancelledException {
try {
project = new HeadlessProject(getProjectManager(),
domainFolder.getProjectData());
if (!checkUpdateOptions()) {
return; // TODO: Should an exception be thrown?
}
if (!checkUpdateOptions()) {
return; // TODO: Should an exception be thrown?
}
if (options.runScriptsNoImport) {
processNoImport(folder.getPathname());
}
else {
processWithImport(folder.getPathname(), filesToImport);
}
}
catch (FileNotFoundException e) {
throw new IOException("Connect to repository folder failed");
}
finally {
if (content != null) {
wrappedContent.release(content, this);
}
if (project != null) {
project.close();
}
}
if (options.runScriptsNoImport) {
processNoImport(domainFolder.getPathname());
}
else {
processWithImport(domainFolder.getPathname(), filesToImport);
}
}
finally {
if (project != null) {
project.close();
}
}
}
@Override
public void handleError(String title, String message, URL url,
IOException cause) throws IOException {
if (cause instanceof FileNotFoundException) {
throw new IOException("Connect to repository folder failed");
}
if (cause != null) {
throw cause;
}
throw new IOException(title + ": " + message);
}
}, TaskMonitor.DUMMY);
}
catch (CancelledException e) {
throw new IOException(e); // unexpected
}
finally {
GhidraScriptUtil.dispose();
@@ -1835,17 +1837,16 @@ public class HeadlessAnalyzer {
*/
private static class HeadlessProject extends DefaultProject {
HeadlessProject(HeadlessGhidraProjectManager projectManager, GhidraURLConnection connection)
throws IOException {
super(projectManager, connection);
AppInfo.setActiveProject(this);
}
HeadlessProject(HeadlessGhidraProjectManager projectManager, ProjectLocator projectLocator)
throws NotOwnerException, LockException, IOException {
super(projectManager, projectLocator, false);
AppInfo.setActiveProject(this);
}
HeadlessProject(HeadlessGhidraProjectManager projectManager, ProjectData projectData) {
super(projectManager, (DefaultProjectData) projectData);
AppInfo.setActiveProject(this);
}
}
private static class HeadlessGhidraProjectManager extends DefaultProjectManager {
@@ -17,6 +17,7 @@ package ghidra.app.util.task;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.atomic.AtomicReference;
import docking.widgets.OptionDialog;
import ghidra.app.plugin.core.progmgr.ProgramLocator;
@@ -25,9 +26,8 @@ import ghidra.framework.client.ClientUtil;
import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.main.AppInfo;
import ghidra.framework.model.DomainFile;
import ghidra.framework.protocol.ghidra.GhidraURLConnection;
import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
import ghidra.framework.protocol.ghidra.GhidraURLWrappedContent;
import ghidra.framework.protocol.ghidra.GhidraURLQuery;
import ghidra.framework.protocol.ghidra.GhidraURLResultHandlerAdapter;
import ghidra.framework.remote.User;
import ghidra.framework.store.ExclusiveCheckoutException;
import ghidra.program.model.lang.LanguageNotFoundException;
@@ -35,6 +35,7 @@ import ghidra.program.model.listing.Program;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
/**
@@ -84,74 +85,42 @@ public class ProgramOpener {
}
/**
* Opens the program for the given location
* Opens the program for the given location.
* This method is intended to be invoked from within a {@link Task} or for headless operations.
* @param locator the program location to open
* @param monitor the TaskMonitor used for status and cancelling
* @return the opened program or null if the operation failed or was cancelled
*/
public Program openProgram(ProgramLocator locator, TaskMonitor monitor) {
if (locator.isURL()) {
return openURL(locator, monitor);
try {
return openURL(locator, monitor);
}
catch (CancelledException e) {
return null;
}
catch (IOException e) {
Msg.showError(this, null, "Program Open Failed",
"Failed to open Ghidra URL: " + locator.getURL(), e);
}
}
return openProgram(locator, locator.getDomainFile(), monitor);
}
private Program openURL(ProgramLocator locator, TaskMonitor monitor) {
URL url = locator.getURL();
GhidraURLWrappedContent wrappedContent = getWrappedContent(url);
if (wrappedContent == null) {
return null;
}
DomainFile remoteDomainFile = getDomainFile(url, wrappedContent);
if (remoteDomainFile == null) {
return null;
}
private Program openURL(ProgramLocator locator, TaskMonitor monitor)
throws CancelledException, IOException {
URL ghidraUrl = locator.getURL();
try {
return openProgram(locator, remoteDomainFile, monitor);
}
finally {
wrappedContent.release(remoteDomainFile, this);
}
}
private DomainFile getDomainFile(URL url, GhidraURLWrappedContent wrappedContent) {
try {
Object content = wrappedContent.getContent(this);
if (content instanceof DomainFile domainFile) {
return domainFile;
AtomicReference<Program> openedProgram = new AtomicReference<>();
GhidraURLQuery.queryUrl(ghidraUrl, new GhidraURLResultHandlerAdapter() {
@Override
public void processResult(DomainFile domainFile, URL url, TaskMonitor m) {
Program p = openProgram(locator, domainFile, m); // may return null
openedProgram.set(p);
}
messageBadProgramURL(url);
if (content != null) {
wrappedContent.release(content, this);
}
}
catch (IOException e) {
Msg.showError(this, null, "Program Open Failed", "Failed to open Ghidra URL: " + url,
e);
}
return null;
}
}, monitor);
private GhidraURLWrappedContent getWrappedContent(URL url) {
try {
GhidraURLConnection c = (GhidraURLConnection) url.openConnection();
Object obj = c.getContent(); // read-only access
if (c.getStatusCode() == StatusCode.UNAUTHORIZED) {
return null; // assume user already notified
}
if (obj instanceof GhidraURLWrappedContent wrappedContent) {
return wrappedContent;
}
return null;
}
catch (IOException e) {
Msg.showError(this, null, "Program Open Failed", "Failed to open Ghidra URL: " + url,
e);
}
return null;
return openedProgram.get();
}
private Program openProgram(ProgramLocator locator, DomainFile domainFile,
@@ -253,9 +222,8 @@ public class ProgramOpener {
if (domainFile.checkout(dialog.exclusiveCheckout(), monitor)) {
return;
}
Msg.showError(this, null, "Checkout Failed",
"Exclusive checkout failed for: " + domainFile.getName() +
"\nOne or more users have file checked out!");
Msg.showError(this, null, "Checkout Failed", "Exclusive checkout failed for: " +
domainFile.getName() + "\nOne or more users have file checked out!");
}
catch (CancelledException e) {
// we don't care, the task has been cancelled
@@ -298,8 +266,4 @@ public class ProgramOpener {
return option == OptionDialog.OPTION_ONE;
}
private void messageBadProgramURL(URL url) {
Msg.error("Invalid Ghidra URL", "Ghidra URL does not reference a Ghidra Program: " + url);
}
}
@@ -69,7 +69,6 @@ public class GhidraGoPluginTest extends AbstractGhidraHeadedIntegrationTest {
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;
@@ -129,7 +128,7 @@ public class GhidraGoPluginTest extends AbstractGhidraHeadedIntegrationTest {
}
});
AbstractErrDialog err = waitForErrorDialog();
assertEquals("Unsupported Content", err.getTitle());
assertEquals("Content Not Found", err.getTitle());
}
}
@@ -19,13 +19,13 @@ import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.Icon;
import generic.theme.GIcon;
import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.*;
import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
import ghidra.framework.store.FileSystem;
import ghidra.framework.store.FolderItem;
import ghidra.framework.store.local.LocalFileSystem;
@@ -91,47 +91,56 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> extends DBCon
return getObject(item, version, consumer, monitor, true);
}
@SuppressWarnings("unchecked")
private T getObject(FolderItem item, int version, Object consumer, TaskMonitor monitor,
boolean immutable) throws IOException, VersionException, CancelledException {
URL url = getURL(item);
URL ghidraUrl = getURL(item);
Class<?> domainObjectClass = getDomainObjectClass();
if (domainObjectClass == null) {
throw new UnsupportedOperationException("");
}
GhidraURLWrappedContent wrappedContent = null;
Object content = null;
final Object transientConsumer = new Object();
try {
GhidraURLConnection c = (GhidraURLConnection) url.openConnection();
Object obj = c.getContent(); // read-only access
if (c.getStatusCode() == StatusCode.UNAUTHORIZED) {
AtomicReference<VersionException> verExcRef = new AtomicReference<>();
AtomicReference<T> domainObjectRef = new AtomicReference<>();
GhidraURLQuery.queryUrl(ghidraUrl, new GhidraURLResultHandlerAdapter(true) {
@Override
public void processResult(DomainFile domainFile, URL url, TaskMonitor m)
throws IOException, CancelledException {
if (!getDomainObjectClass().isAssignableFrom(domainFile.getDomainObjectClass())) {
throw new BadLinkException("Expected " + getDomainObjectClass() +
" but linked to " + domainFile.getDomainObjectClass());
}
try {
@SuppressWarnings("unchecked")
T linkedObject = immutable
? (T) domainFile.getImmutableDomainObject(consumer, version, monitor)
: (T) domainFile.getReadOnlyDomainObject(consumer, version, monitor);
domainObjectRef.set(linkedObject);
}
catch (VersionException e) {
verExcRef.set(e);
}
}
@Override
public void handleUnauthorizedAccess(URL url) throws IOException {
throw new IOException("Authorization failure");
}
if (!(obj instanceof GhidraURLWrappedContent)) {
throw new IOException("Unsupported linked content");
}
wrappedContent = (GhidraURLWrappedContent) obj;
content = wrappedContent.getContent(transientConsumer);
if (!(content instanceof DomainFile)) {
throw new IOException("Unsupported linked content: " + content.getClass());
}
DomainFile linkedFile = (DomainFile) content;
if (!getDomainObjectClass().isAssignableFrom(linkedFile.getDomainObjectClass())) {
throw new BadLinkException("Expected " + getDomainObjectClass() +
" but linked to " + linkedFile.getDomainObjectClass());
}
return immutable ? (T) linkedFile.getImmutableDomainObject(consumer, version, monitor)
: (T) linkedFile.getReadOnlyDomainObject(consumer, version, monitor);
}, monitor);
VersionException versionException = verExcRef.get();
if (versionException != null) {
throw versionException;
}
finally {
if (content != null) {
wrappedContent.release(content, transientConsumer);
}
T domainObj = domainObjectRef.get();
if (domainObj == null) {
throw new IOException(
"Failed to obtain linked object for unknown reason: " + item.getPathName());
}
return domainObj;
}
@Override
@@ -120,22 +120,15 @@ public class DefaultProject implements Project {
}
/**
* Constructor for opening a URL-based project
* Construct a project with specific project manager and data
*
* @param projectManager the manager of this project
* @param connection project URL connection (not previously used)
* @throws IOException if I/O error occurs.
* @param projectData the project data
*/
protected DefaultProject(DefaultProjectManager projectManager, GhidraURLConnection connection)
throws IOException {
protected DefaultProject(DefaultProjectManager projectManager, DefaultProjectData projectData) {
this.projectManager = projectManager;
Msg.info(this, "Opening project/repository: " + connection.getURL());
projectData = (DefaultProjectData) connection.getProjectData();
if (projectData == null) {
throw new IOException("Failed to open project/repository: " + connection.getURL());
}
this.projectData = projectData;
projectLocator = projectData.getProjectLocator();
if (!SystemUtilities.isInHeadlessMode()) {
@@ -32,7 +32,7 @@ import ghidra.framework.main.FrontEndTool;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.preferences.Preferences;
import ghidra.framework.protocol.ghidra.GetUrlContentTypeTask;
import ghidra.framework.protocol.ghidra.ContentTypeQueryTask;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.util.Msg;
import ghidra.util.filechooser.GhidraFileChooserModel;
@@ -272,7 +272,7 @@ class ToolServicesImpl implements ToolServices {
}
private String getContentType(URL url) throws IllegalArgumentException {
GetUrlContentTypeTask task = new GetUrlContentTypeTask(url);
ContentTypeQueryTask task = new ContentTypeQueryTask(url);
TaskLauncher.launch(task); // blocking task
return task.getContentType();
}
@@ -0,0 +1,56 @@
/* ###
* 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.framework.protocol.ghidra;
import java.net.URL;
import ghidra.framework.model.DomainFile;
import ghidra.util.task.TaskMonitor;
/**
* A blocking/modal Ghidra URL content type discovery task
*/
public class ContentTypeQueryTask extends GhidraURLQueryTask {
private String contentType = "Unknown";
/**
* Construct a Ghidra URL content type query task
* @param ghidraUrl Ghidra URL (local or remote)
* @throws IllegalArgumentException if specified URL is not a Ghidra URL
* (see {@link GhidraURL}).
*/
public ContentTypeQueryTask(URL ghidraUrl) {
super("Query URL Content Type", ghidraUrl);
}
/**
* Get the discovered content type (e.g., "Program")
* @return content type or null if error occured or unsupported URL content
* @throws IllegalStateException if task has not completed execution
*/
public String getContentType() {
if (!isDone()) {
throw new IllegalStateException("task has not completed");
}
return contentType;
}
@Override
public void processResult(DomainFile domainFile, URL url, TaskMonitor monitor) {
contentType = domainFile.getContentType();
}
}
@@ -1,113 +0,0 @@
/* ###
* 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.framework.protocol.ghidra;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import ghidra.framework.model.DomainFile;
import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
import ghidra.util.Msg;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
/**
* A blocking/modal Ghidra URL content type discovery task
*/
public class GetUrlContentTypeTask extends Task {
private final URL ghidraUrl;
private String contentType;
private boolean done = false;
/**
* Construct a Ghidra URL content type discovery task
* @param ghidraUrl Ghidra URL (local or remote)
* @throws IllegalArgumentException if specified URL is not a Ghidra URL
* (see {@link GhidraURL}).
*/
public GetUrlContentTypeTask(URL ghidraUrl) {
super("Checking URL Content Type", true, false, true);
if (!GhidraURL.isLocalProjectURL(ghidraUrl) &&
!GhidraURL.isServerRepositoryURL(ghidraUrl)) {
throw new IllegalArgumentException("unsupported URL");
}
this.ghidraUrl = ghidraUrl;
}
/**
* Get the discovered content type (e.g., "Program")
* @return content type or null if error occured or unsupported URL content
* @throws IllegalStateException if task has not completed execution
*/
public String getContentType() {
if (!done) {
throw new IllegalStateException("task has not completed");
}
return contentType;
}
@Override
public void run(TaskMonitor monitor) {
final Thread t = Thread.currentThread();
monitor.addCancelledListener(() -> {
t.interrupt();
});
GhidraURLWrappedContent wrappedContent = null;
Object content = null;
try {
GhidraURLConnection c = (GhidraURLConnection) ghidraUrl.openConnection();
Object obj = c.getContent(); // read-only access
if (c.getStatusCode() == StatusCode.UNAUTHORIZED) {
return; // assume user already notified
}
if (obj instanceof GhidraURLWrappedContent) {
wrappedContent = (GhidraURLWrappedContent) obj;
content = wrappedContent.getContent(this);
}
if (!(content instanceof DomainFile)) {
Msg.showError(this, null, "Unsupported Content",
"Invalid project file URL: " + ghidraUrl);
return;
}
contentType = ((DomainFile) content).getContentType();
}
catch (FileNotFoundException e) {
Msg.showError(this, null, "Content Not Found", e.getMessage());
}
catch (MalformedURLException e) {
Msg.showError(this, null, "Invalid Ghidra URL",
"Improperly formed Ghidra URL: " + ghidraUrl);
}
catch (InterruptedIOException e) {
// ignore - assume cancelled
}
catch (IOException e) {
Msg.showError(this, null, "URL Access Failure",
"Failed to open Ghidra URL: " + e.getMessage());
}
finally {
if (content != null) {
wrappedContent.release(content, this);
}
done = true;
}
}
}
@@ -121,7 +121,8 @@ public class GhidraURL {
* Confirm local project URL with {@link #isLocalProjectURL(URL)} prior to method use.
* @param localProjectURL local Ghidra project URL
* @return project locator or null if invalid path specified
* @throws IllegalArgumentException URL is not a valid local project URL
* @throws IllegalArgumentException URL is not a valid
* {@link #isLocalProjectURL(URL) local project URL}.
*/
public static ProjectLocator getProjectStorageLocator(URL localProjectURL) {
if (!isLocalProjectURL(localProjectURL)) {
@@ -330,9 +331,12 @@ public class GhidraURL {
}
/**
* Get normalized URL which corresponds to the local-project or repository
* Get Ghidra URL which corresponds to the local-project or repository with any
* file path or query details removed.
* @param ghidraUrl ghidra file/folder URL (server-only URL not permitted)
* @return local-project or repository URL
* @throws IllegalArgumentException if URL does not specify the {@code ghidra} protocol
* or does not properly identify a remote repository or local project.
*/
public static URL getProjectURL(URL ghidraUrl) {
if (!PROTOCOL.equals(ghidraUrl.getProtocol())) {
@@ -456,6 +460,48 @@ public class GhidraURL {
return host;
}
/**
* Force the specified URL to specify a folder. This may be neccessary when only folders
* are supported since Ghidra permits both a folder and file to have the same name within
* its parent folder. This method simply ensures that the URL path ends with a {@code /}
* character if needed.
* @param ghidraUrl ghidra URL
* @return ghidra folder URL
* @throws IllegalArgumentException if specified URL is niether a
* {@link #isServerRepositoryURL(URL) valid remote server URL}
* or {@link #isLocalProjectURL(URL) local project URL}.
*/
public static URL getFolderURL(URL ghidraUrl) {
if (!GhidraURL.isServerRepositoryURL(ghidraUrl) &&
!GhidraURL.isLocalProjectURL(ghidraUrl)) {
throw new IllegalArgumentException("Invalid Ghidra URL: " + ghidraUrl);
}
URL repoURL = GhidraURL.getProjectURL(ghidraUrl);
String path = GhidraURL.getProjectPathname(ghidraUrl);
path = path.trim();
if (!path.endsWith("/")) {
// force explicit folder path
path += "/";
try {
if (GhidraURL.isServerRepositoryURL(ghidraUrl)) {
ghidraUrl = new URL(repoURL + path);
}
else {
ghidraUrl = new URL(repoURL + "?" + path);
}
}
catch (MalformedURLException e) {
throw new AssertionError(e);
}
}
return ghidraUrl;
}
/**
* Get a normalized URL which eliminates use of host names and optional URL ref
* which may prevent direct comparison.
@@ -634,7 +680,7 @@ public class GhidraURL {
* @param ref optional URL ref or null
* Folder paths should end with a '/' character.
* @return Ghidra Server repository content URL
* @throws IllegalArgumentException if invalid arguments are specified
* @throws IllegalArgumentException if required arguments are blank or invalid
*/
public static URL makeURL(String host, int port, String repositoryName,
String repositoryFolderPath, String fileName, String ref) {
@@ -0,0 +1,157 @@
/* ###
* 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.framework.protocol.ghidra;
import java.io.IOException;
import java.net.URL;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
/**
* {@link GhidraURLQuery} performs remote Ghidra repository and read-only local project
* queries for processing either a {@link DomainFile} or {@link DomainFolder} that a
* Ghidra URL may reference.
*/
public abstract class GhidraURLQuery {
/**
* Perform read-only query using specified GhidraURL and process result.
* Both local project and remote repository URLs are supported.
* This method is intended to be invoked from within a {@link Task} or for headless operations.
* @param ghidraUrl local or remote Ghidra URL
* @param resultHandler query result handler
* @param monitor task monitor
* @throws IOException if an IO error occurs which was re-thrown by {@code resultHandler}
* @throws CancelledException if task is cancelled
*/
public static void queryUrl(URL ghidraUrl, GhidraURLResultHandler resultHandler,
TaskMonitor monitor) throws IOException, CancelledException {
doQueryUrl(ghidraUrl, true, resultHandler, monitor);
}
/**
* Perform query using specified GhidraURL and process result.
* Both local project and remote repository URLs are supported.
* This method is intended to be invoked from within a {@link Task} or for headless operations.
* @param ghidraUrl local or remote Ghidra URL
* @param readOnly allows update/commit (false) or read-only (true) access.
* @param resultHandler query result handler
* @param monitor task monitor
* @throws IOException if an IO error occurs which was re-thrown by {@code resultHandler}
* @throws CancelledException if task is cancelled
*/
public static void queryRepositoryUrl(URL ghidraUrl, boolean readOnly,
GhidraURLResultHandler resultHandler, TaskMonitor monitor)
throws IOException, CancelledException {
if (!GhidraURL.isServerRepositoryURL(ghidraUrl)) {
throw new IllegalArgumentException("Unsupported repository URL: " + ghidraUrl);
}
doQueryUrl(ghidraUrl, readOnly, resultHandler, monitor);
}
private static void doQueryUrl(URL ghidraUrl, boolean readOnly,
GhidraURLResultHandler resultHandler, TaskMonitor monitor)
throws IOException, CancelledException {
GhidraURLConnection c;
Object obj = null;
StatusCode status = null;
try {
c = (GhidraURLConnection) ghidraUrl.openConnection();
c.setReadOnly(readOnly); // writable repository connection
obj = c.getContent(); // read-only access
status = c.getStatusCode();
}
catch (IOException e) {
resultHandler.handleError("URL Connection Error", e.getMessage(), ghidraUrl, e);
}
GhidraURLWrappedContent wrappedContent = null;
Object content = null;
try {
IOException generatedErr = null;
switch (status) {
case OK:
break;
case UNAUTHORIZED:
resultHandler.handleUnauthorizedAccess(ghidraUrl);
return;
case NOT_FOUND:
generatedErr = new IOException("Project or repository not found");
break;
case LOCKED:
// Local projects are only accessed read-only, this condition should not occur
throw new AssertionError("Unexpected local project lock condition");
case UNAVAILABLE:
generatedErr =
new IOException("Server connection error occured (see log files)");
break;
default:
}
if (generatedErr != null) {
resultHandler.handleError("Content Not Found", generatedErr.getMessage(), ghidraUrl,
generatedErr);
return;
}
if (!(obj instanceof GhidraURLWrappedContent)) {
resultHandler.handleError("Unsupported Content",
"URL does not correspond to a file or folder", null, null);
return;
}
wrappedContent = (GhidraURLWrappedContent) obj;
try {
content = wrappedContent.getContent(resultHandler);
}
catch (IOException e) {
resultHandler.handleError("Content Not Found", e.getMessage(), null, e);
return;
}
monitor.checkCancelled();
if (content instanceof DomainFile file) {
resultHandler.processResult(file, ghidraUrl, monitor);
}
else if (content instanceof DomainFolder folder) {
resultHandler.processResult(folder, ghidraUrl, monitor);
}
else {
// unexpected condition
resultHandler.handleError("Unsupported Content",
"Content class: " + content.getClass().getName(), ghidraUrl, null);
}
}
finally {
if (content != null) {
wrappedContent.release(content, resultHandler);
}
monitor.checkCancelled();
}
}
}
@@ -0,0 +1,111 @@
/* ###
* 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.framework.protocol.ghidra;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.URL;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.*;
/**
* {@link GhidraURLQueryTask} provides an abstract Task which performs remote Ghidra
* repository and read-only local project queries for processing either a {@link DomainFile}
* or {@link DomainFolder} that a Ghidra URL may reference.
* <P>
* All implementations of this Task should override one or
* both of the processing methods {@link #processResult(DomainFile, URL, TaskMonitor)}
* and {@link #processResult(DomainFolder, URL, TaskMonitor)}. For any process method
* not overriden the default behavior is reporting <I>Unsupported Content</I>.
* <P>
* If {@link #handleError(String, String, URL, IOException)}
* is not overriden all errors are reported via
* {@link Msg#showError(Object, java.awt.Component, String, Object)}.
*/
public abstract class GhidraURLQueryTask extends Task implements GhidraURLResultHandler {
private final URL ghidraUrl;
private boolean done = false;
/**
* Construct a Ghidra URL read-only query task.
* @param title task dialog title
* @param ghidraUrl Ghidra URL (local or remote)
* @throws IllegalArgumentException if specified URL is not a Ghidra URL
* (see {@link GhidraURL}).
*/
protected GhidraURLQueryTask(String title, URL ghidraUrl) {
super(title, true, false, true);
if (!GhidraURL.isLocalProjectURL(ghidraUrl) &&
!GhidraURL.isServerRepositoryURL(ghidraUrl)) {
throw new IllegalArgumentException("Unsupported URL: " + ghidraUrl);
}
this.ghidraUrl = ghidraUrl;
}
/**
* Determine if the task has completed its execution
* @return true if done executing else false
*/
protected boolean isDone() {
return done;
}
@Override
public void run(TaskMonitor monitor) throws CancelledException {
final Thread t = Thread.currentThread();
CancelledListener cancelledListener = () -> t.interrupt();
monitor.addCancelledListener(cancelledListener);
try {
GhidraURLQuery.queryUrl(ghidraUrl, this, monitor);
}
catch (InterruptedIOException e) {
// ignore - assume cancelled
}
catch (IOException e) {
handleError("URL Access Failure", e.getMessage(), ghidraUrl, e);
}
finally {
monitor.removeCancelledListener(cancelledListener);
monitor.checkCancelled();
done = true;
}
}
@Override
public void handleError(String title, String message, URL url, IOException cause) {
Msg.showError(GhidraURLQuery.class, null, title, message + ":\n" + url);
}
@Override
public void processResult(DomainFile domainFile, URL url, TaskMonitor monitor)
throws IOException {
handleError("Unsupported Content", "File URL: " + url, null, null);
}
@Override
public void processResult(DomainFolder domainFolder, URL url, TaskMonitor monitor)
throws IOException {
handleError("Unsupported Content", "Folder URL: " + url, null, null);
}
}
@@ -0,0 +1,79 @@
/* ###
* 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.framework.protocol.ghidra;
import java.io.IOException;
import java.net.URL;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public interface GhidraURLResultHandler {
/**
* Process the specified {@code domainFile} query result.
* Dissemination of the {@code domainFile} instance should be restricted and any use of it
* completed before the call to this method returns. Upon return from this method call the
* underlying connection will be closed and at which time the {@code domainFile} instance
* will become invalid.
* @param domainFile {@link DomainFile} to which the URL refers.
* @param url URL which was used to retrieve the specified {@code domainFile}
* @param monitor task monitor
* @throws IOException if an IO error occurs
* @throws CancelledException if task is cancelled
*/
void processResult(DomainFile domainFile, URL url, TaskMonitor monitor)
throws IOException, CancelledException;
/**
* Process the specified {@code domainFolder} query result.
* Dissemination of the {@code domainFolder} instance should be restricted and any use of it
* completed before the call to this method returns. Upon return from this method call the
* underlying connection will be closed and at which time the {@code domainFolder} instance
* will become invalid.
* @param domainFolder {@link DomainFolder} to which the URL refers.
* @param url URL which was used to retrieve the specified {@code domainFolder}
* @param monitor task monitor
* @throws IOException if an IO error occurs
* @throws CancelledException if task is cancelled
*/
void processResult(DomainFolder domainFolder, URL url, TaskMonitor monitor)
throws IOException, CancelledException;
/**
* Handle error which occurs during query operation.
* @param title error title
* @param message error detail
* @param url URL which was used for query
* @param cause cause of error (may be null)
* @throws IOException may be thrown if handler decides to propogate error
*/
void handleError(String title, String message, URL url, IOException cause) throws IOException;
/**
* Handle authorization error.
* This condition is generally logged and user notified via GUI during connection processing.
* This method does not do anything by default but is provided to flag failure if needed since
* {@link #handleError(String, String, URL, IOException)} will not be invoked.
* @param url connection URL
* @throws IOException may be thrown if handler decides to propogate error
*/
default void handleUnauthorizedAccess(URL url) throws IOException {
// do nothing - assume user has already been notified or issue has been logged
}
}
@@ -0,0 +1,81 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.protocol.ghidra;
import java.io.IOException;
import java.net.URL;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* {@link GhidraURLResultHandlerAdapter} provides a basic result handler for
* {@link GhidraURLQuery}. All uses of this adapter should override one or
* both of the processing methods {@link #processResult(DomainFile, URL, TaskMonitor)}
* and {@link #processResult(DomainFolder, URL, TaskMonitor)}. For any process method
* not overriden the default behavior is reporting <I>Unsupported Content</I>.
*/
public class GhidraURLResultHandlerAdapter implements GhidraURLResultHandler {
private final boolean throwErrorByDefault;
/**
* Construct adapter. If {@link #handleError(String, String, URL, IOException)}
* is not overriden all errors are reported via
* {@link Msg#showError(Object, java.awt.Component, String, Object)}.
*/
public GhidraURLResultHandlerAdapter() {
throwErrorByDefault = false;
}
/**
* Construct adapter with preferred error handling. There is no need to use this constructor
* if {@link #handleError(String, String, URL, IOException)} is override.
* @param throwErrorByDefault if true all errors will be thrown as an {@link IOException},
* otherwise error is reported via {@link Msg#showError(Object, java.awt.Component, String, Object)}.
*/
public GhidraURLResultHandlerAdapter(boolean throwErrorByDefault) {
this.throwErrorByDefault = throwErrorByDefault;
}
@Override
public void processResult(DomainFile domainFile, URL url, TaskMonitor monitor)
throws IOException, CancelledException {
handleError("Unsupported Content", "File URL: " + url, null, null);
}
@Override
public void processResult(DomainFolder domainFolder, URL url, TaskMonitor monitor)
throws IOException, CancelledException {
handleError("Unsupported Content", "Folder URL: " + url, null, null);
}
@Override
public void handleError(String title, String message, URL url, IOException cause)
throws IOException {
if (!throwErrorByDefault) {
Msg.showError(GhidraURLQuery.class, null, title, message + ":\n" + url);
}
if (cause != null) {
throw cause;
}
throw new IOException(title + ": " + message);
}
}