Merge remote-tracking branch

'origin/GP-6796_ghidra1_GhidraURLAllowListInterface' into Ghidra_12.2
This commit is contained in:
ghidra1
2026-09-04 15:34:45 -04:00
39 changed files with 1541 additions and 216 deletions

View File

@@ -22,9 +22,9 @@
hyperlink.</P>
<!-- Annotation Example -->
<P>The following text shows the syntax of a sample URL annotation:</P>
<P>The following text shows the syntax of a sample HTTP URL annotation:</P>
<pre><font size="4"><br>
<b>{@<i>url</i></b> "<i>http://www.google.com</i>"</b> "Search Web"<b>}</b><br>
<b>{@<i>url</i></b> "<i>https://www.google.com</i>"</b> "Search Web"<b>}</b><br>
</font><br></pre>
<P>The bold text is required for all annotations. The italicized text is required but is
@@ -38,7 +38,7 @@
<!-- image -->
<P align="center"><IMG border="0" src="images/CommentDialogURLExample.png" alt=""><BR>
<I>URL Annotation Example</I></P>
<I>HTTP URL Annotation Example</I></P>
<P>The image above shows a URL annotation in its text form as entered into the EOL Comment
tab of the Comments dialog.<BR>
@@ -46,11 +46,13 @@
The image below shows how the annotation is rendered in Ghidra.</P>
<P align="center"><IMG border="2" src="images/RenderedURLExample.png" alt=""><BR>
<I>Rendered URL Annotation Example</I></P>
<I>Rendered HTTP URL Annotation Example</I></P>
<P>When the URL text (e.g., "http://www.google.com") in the above image is clicked from within
<P>When the URL text (e.g., "https://www.google.com") in the above image is clicked from within
Ghidra, a web browser is launched and attempts to load the corresponding web page. </P>
<I>GHIDRA URL Annotation Example</I></P>
<P>If the URL text corresponds to a Ghidra URL and attempt will be made to open the referenced
Program file within the Code Browser. Such a URL may refer to a Program file from a
local project or Ghidra Server. The Ghidra URL forms supported include:</P>
@@ -64,6 +66,14 @@
<i>ghidra:/[&lt;project-path&gt/]&lt;project-name&gt;?/&lt;program-path&gt;[#&lt;address-or-symbol-ref&gt;]</i><BR>
Example: <i>ghidra:/share/MyProject?/notepad.exe#entry</i>
</P>
<P><IMG border="0" src="images/warning.help.png" alt="Note"> Clicking on all remote URL annotations will be
will be subject to the <b>Server Allow List</b> resulting in a possible confirmation dialog.
This is independent of possible SSL/TLS server authentication which may be required.
At anytime the <b>Server Allow List</b> may be cleared via the Project window
<b>Edit->Clear Server Allow List...</b> or managed using the <I>support/updateServerAllowList</I> shell script.
Execute this script without arguments to see usage information.</P>
</BLOCKQUOTE>
<H2>Valid Annotations</H2>
@@ -197,7 +207,7 @@
<TD valign="top" width="12%">
<OL style="margin-left: 15px;">
<LI>URL</LI>
<LI>HTTP/GHIDRA URL</LI>
<LI>[display text]<BR>
</LI>
@@ -219,11 +229,11 @@
<TD valign="top" width="25%">
<UL style="margin-left: 10px;">
<LI>{@url "http://www.google.com"}</LI>
<LI>{@url "https://www.google.com"}</LI>
<LI>{@url "http://www.google.com" "google"}</LI>
<LI>{@url "https://www.google.com" "google"}</LI>
<LI>{@url "http://www.google.com" "click here for google"}</LI>
<LI>{@url "https://www.google.com" "click here for google"}</LI>
<LI>{@url "ghidra://myserver/Repo/notepad.exe"}</LI>
@@ -355,6 +365,10 @@
quotes (") around content inside of the annotation tag, excluding the <B>@<I>name</I></B>
part of the tag. Further, some annotations require quotes, as listed in the table above
(e.g., the <B>Execute</B> annotation requires quotes). It is considered good practice to
quote all annotation parameter values.</P><P><IMG border="0" src="images/warning.help.png" alt="Note"> All annotations support double
quotes (") around content inside of the annotation tag, excluding the <B>@<I>name</I></B>
part of the tag. Further, some annotations require quotes, as listed in the table above
(e.g., the <B>Execute</B> annotation requires quotes). It is considered good practice to
quote all annotation parameter values.</P>

View File

@@ -98,8 +98,6 @@
</UL>
</BLOCKQUOTE>
<H3>&nbsp;</H3>
<H3><A name="Manage_Certificates"></A>Manage Certificates (Windows and macOS only)&nbsp;</H3>
<BLOCKQUOTE>
@@ -125,18 +123,18 @@
When you connect to the server the next time you run Ghidra, you will be prompted for the
key-store password associated with this certificate key file. The path to your PKI
certificate file is saved as part of your Ghidra preferences.</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG src="help/shared/note.png" border="0"> If the Ghidra Server, or other server,
is not using PKI Certificates for user authentication, you can ignore this menu option
since the certificate keystore will not be used.</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG src="help/shared/note.png" border="0"> Specifying the single user certificate
<P><IMG src="help/shared/note.png" border="0"> Setting your user PKI certificate key store
may also be required if Ghidra communicates with other web services which rely on PKI user
authentication.</P>
<P><IMG src="help/shared/note.png" border="0"> Specifying the single user certificate
keystore in this fashion will prevent the OS managed keystore from being used
(applied to Windows and macOS only).</P>
(applies to Windows and macOS only).</P>
</BLOCKQUOTE>
<H3><A name="Clear_PKI_Certificate"></A>Clear PKI Certificate &nbsp;</H3>
@@ -149,6 +147,32 @@
</BLOCKQUOTE>
<H3><A name="Clear_Server_Allow_List"></A>Server Allow List &nbsp;</H3>
<BLOCKQUOTE>
<P>When attempting to communicate with various servers based upon a URL, you may be
prompted to either allow or disallow the connection and all future connection attempts.
The choice will be saved to a <I>Server Allow List</I>. A server entry is identified
by the associated communication protocol (e.g., "https", "ghidra"), its host name
as specified by a URL and the associated TCP port. The "ghidra" protocol is only
identified with its base-port (e.g., 13100) and not the other two related ports.
If you change your mind about a server connection it may be neccessary to revise this
saved <I>Server Allow List</I>. Two options exist for altering this list:</P>
<UL>
<LI>To clear the entire Server Allow List from the Ghidra GUI Project Window,
choose <B>Edit<IMG src="help/shared/arrow.gif" border="0">Clear Server Allow List...</B>.</LI>
<LI>From a system command prompt, the <B>updateServerAllowList</B> command
can be used to view and selectively modify the allow-list. This command can
be found within the Ghidra installation <B>support</B> directory.
Server Allow List entries may be displayed with the <I>-list</I>
option, and added or modified using the <I>-allow or -disallow</I> options.
Entries may also removed entirely by using the <I>-clear or -clearAll</I> option.</LI>
</UL>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="Exit_Ghidra"></A>Exiting Ghidra</H2>
<BLOCKQUOTE>

View File

@@ -25,6 +25,7 @@ import ghidra.*;
import ghidra.app.util.importer.LibrarySearchPathManager;
import ghidra.app.util.opinion.Loader;
import ghidra.framework.*;
import ghidra.framework.client.*;
import ghidra.framework.model.DomainFolder;
import ghidra.framework.protocol.ghidra.Handler;
import ghidra.util.Msg;
@@ -65,6 +66,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
PASSWORD("-p", false),
COMMIT("-commit", false, "[\"<comment>\"]]"),
OK_TO_DELETE("-okToDelete", false),
ALLOW_ALL_ACCESS("-allowAllAccess", false),
MAX_CPU("-max-cpu", true, "<max cpu cores to use>"),
LIBRARY_SEARCH_PATHS("-librarySearchPaths", true, "<path1>[;<path2>...]"),
LOADER(Loader.COMMAND_LINE_ARG_PREFIX, true, "<desired loader name>"),
@@ -186,6 +188,16 @@ public class AnalyzeHeadless implements GhidraLaunchable {
HeadlessOptions options = analyzer.getOptions();
parseOptions(options, args, optionStartIndex, ghidraURL, filesToImport);
// Ensure that we do not rely on prompting user for allowing server access
if (options.allowAllAccess) {
Msg.warn(AnalyzeHeadless.class,
"All remote server access is Allowed (" + Arg.ALLOW_ALL_ACCESS + ")");
ClientUtil.setAllowListProvider(new AllowAllUrlAllowListProvider());
}
else {
ClientUtil.setAllowListProvider(new DefaultlUrlAllowListProvider());
}
Msg.info(AnalyzeHeadless.class,
"Headless startup complete (" + GhidraLauncher.getMillisecondsFromLaunch() + " ms)");
ClassSearcher.logStatistics();
@@ -199,6 +211,11 @@ public class AnalyzeHeadless implements GhidraLaunchable {
analyzer.processLocal(args[0], projectName, rootFolderPath, filesToImport);
}
}
catch (IOException e) {
Msg.error(HeadlessAnalyzer.class,
"Abort due to error: " + e.getMessage());
System.exit(EXIT_CODE_ERROR);
}
catch (Throwable e) {
Msg.error(HeadlessAnalyzer.class,
"Abort due to Headless analyzer error: " + e.getMessage(), e);
@@ -412,6 +429,9 @@ public class AnalyzeHeadless implements GhidraLaunchable {
else if (checkArgument(Arg.OK_TO_DELETE, args, argi)) {
options.setOkToDelete(true);
}
else if (checkArgument(Arg.ALLOW_ALL_ACCESS, args, argi)) {
options.setAllowAllAccess(true);
}
else if (checkArgument(Arg.LIBRARY_SEARCH_PATHS, args, argi)) {
LibrarySearchPathManager.setLibraryPaths(args[++argi].split(";"));
}
@@ -584,4 +604,24 @@ public class AnalyzeHeadless implements GhidraLaunchable {
private boolean isExistingArg(String s) {
return Arrays.stream(Arg.values()).anyMatch(e -> e.matches(s));
}
private static class AllowAllUrlAllowListProvider implements UrlAllowListProvider {
@Override
public boolean isAllowed(URL url) {
return true; // do not cache decision
}
}
private static class DefaultlUrlAllowListProvider extends AbstractUrlAllowListProvider {
@Override
public boolean isAllowed(URL url) {
Boolean allowed = accessAllowed(url);
if (allowed != null) {
return allowed;
}
return false; // do not cache decision
}
}
}

View File

@@ -36,8 +36,7 @@ import ghidra.app.util.importer.ProgramLoader;
import ghidra.app.util.opinion.*;
import ghidra.formats.gfilesystem.*;
import ghidra.framework.*;
import ghidra.framework.client.ClientUtil;
import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.client.*;
import ghidra.framework.data.*;
import ghidra.framework.main.AppInfo;
import ghidra.framework.model.*;
@@ -259,10 +258,8 @@ public class HeadlessAnalyzer {
throws IOException, MalformedURLException, URISyntaxException {
if (options.readOnly && options.commit) {
Msg.error(this,
"Abort due to Headless analyzer error: The requested readOnly option is in conflict " +
throw new IllegalArgumentException("The requested readOnly option is in conflict " +
"with the commit option");
return;
}
if (!"ghidra".equals(ghidraURL.getProtocol())) {
@@ -270,9 +267,8 @@ public class HeadlessAnalyzer {
}
if (GhidraURL.isLocalURL(ghidraURL)) {
Msg.error(this,
throw new IllegalArgumentException(
"Ghidra URL command form does not supported local project URLs (ghidra:/path...)");
return;
}
String path = ghidraURL.getPath();
@@ -293,6 +289,22 @@ public class HeadlessAnalyzer {
}
}
if (!options.allowAllAccess) {
// Check Server Allow List - add access if not already blocked
Boolean hasServerAccess = UrlAllowListManager.getAccess(ghidraURL);
if (hasServerAccess == null) {
ServerSpecification serverSpec = ServerSpecification.get(ghidraURL);
Msg.info(HeadlessAnalyzer.class,
"NOTICE: Adding server to allow list: " + serverSpec.toString());
UrlAllowListManager.updateAccess(ghidraURL, true);
}
else if (!hasServerAccess) {
ServerSpecification serverSpec = ServerSpecification.get(ghidraURL);
throw new IOException(
"Access denied by server allow list: " + serverSpec.toString());
}
}
BundleHost bundleHost = GhidraScriptUtil.acquireBundleHostReference();
bundleHost.add(parseScriptPaths(options.scriptPaths), true, true);
try {

View File

@@ -95,6 +95,10 @@ public class HeadlessOptions {
// -p
boolean allowPasswordPrompt;
// -allowAllAccess - Server Allow List will allow all remote server access, otherwise
// access may be restricted based upon previously allowed server access.
boolean allowAllAccess;
// -commit
boolean commit;
String commitComment;
@@ -143,6 +147,7 @@ public class HeadlessOptions {
keystore = null;
connectUserID = null;
allowPasswordPrompt = false;
allowAllAccess = false;
commit = false;
commitComment = null;
okToDelete = false;
@@ -392,6 +397,17 @@ public class HeadlessOptions {
this.analyze = enabled;
}
/**
* Remote Ghidra Server access relies on Server Allow List established by GUI application.
* This method can be used to allow all access and ignore Server Allow List.
*
* @param allowAccess True if all server access should be allowed, otherwise rely on
* Allow List previously cached by GUI application.
*/
public void setAllowAllAccess(boolean allowAccess) {
this.allowAllAccess = allowAccess;
}
/**
* Sets the language and compiler spec from the provided input. Any null value will attempt
* a "best-guess" if possible.

View File

@@ -0,0 +1,143 @@
/* ###
* 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.util.headless;
import java.io.IOException;
import java.net.*;
import java.util.*;
import ghidra.GhidraApplicationLayout;
import ghidra.GhidraLaunchable;
import ghidra.framework.Application;
import ghidra.framework.ApplicationConfiguration;
import ghidra.framework.client.*;
import ghidra.framework.protocol.ghidra.Handler;
import ghidra.util.Msg;
/**
* {@link UpdateServerAllowList} utility for managing the Server Allow List.
* See {@link UrlAllowListManager}.
*/
public class UpdateServerAllowList implements GhidraLaunchable {
private static final String INVOCATION_NAME_PROPERTY = "UpdateServerAllowList.Name";
public UpdateServerAllowList() {
// Required for GhidraLaunchable
}
private URL parseURL(String urlString) throws MalformedURLException {
URI uri = URI.create(urlString);
return uri.toURL();
}
private void checkMoreArgs(int currentArgIndex, String[] args) {
if (currentArgIndex == args.length - 1) {
usage(args);
}
}
@Override
public void launch(GhidraApplicationLayout layout, String[] args) throws IOException {
Application.initializeApplication(layout, new ApplicationConfiguration());
if (args.length == 0) {
usage(args);
}
// NOTE: May need other protocol handlers to be registered to avoid URL exceptions
Handler.registerHandler();
try {
boolean printList = false;
for (int i = 0; i < args.length; i++) {
String arg = args[i];
switch (arg) {
case "-allow":
checkMoreArgs(i, args);
URL url = parseURL(args[++i]);
UrlAllowListManager.updateAccess(url, true);
break;
case "-disallow":
checkMoreArgs(i, args);
url = parseURL(args[++i]);
UrlAllowListManager.updateAccess(url, false);
break;
case "-clear":
checkMoreArgs(i, args);
url = parseURL(args[++i]);
UrlAllowListManager.clearAccessEntry(url);
break;
case "-clearAll":
UrlAllowListManager.clearAll();
break;
case "-list":
printList = true;
break;
default:
usage(args);
}
}
if (printList) {
Map<ServerSpecification, AccessRecord> accessMap =
UrlAllowListManager.getAccessMap();
List<ServerSpecification> servers = new ArrayList<>(accessMap.keySet());
if (servers.isEmpty()) {
System.out.println("Server Allow List is empty.");
}
else {
Collections.sort(servers);
System.out.println("Server Allow List:");
for (ServerSpecification svr : servers) {
AccessRecord accessRecord = accessMap.get(svr);
String access = accessRecord.accessAllowed() ? "ALLOW " : "DISALLOW";
Date date = new Date(accessRecord.time());
System.out.println(
" " + access + " " + svr.toUrlString() + " (" + date + ")");
}
}
}
}
catch (Exception e) {
Msg.error("Exception processing Allow List updates", e);
}
System.out.println("Done");
}
private static void usage(String[] args) {
for (int i = 0; i < args.length; i++) {
System.err.println("arg " + i + ": " + args[i]);
}
String invocationName = System.getProperty(INVOCATION_NAME_PROPERTY);
StringBuffer buf = new StringBuffer();
buf.append("\nUsage: ");
buf.append(
invocationName != null ? invocationName : UpdateServerAllowList.class.getSimpleName());
buf.append(
" [-allow <protocol>://<hostname>:<port>] [-disallow <protocol>://<hostname>:<port>] [-clear <protocol>://<hostname>:<port>] [-clearAll] [-list]\n");
System.err.println(buf.toString());
System.exit(0);
}
}

View File

@@ -18,7 +18,6 @@ package ghidra.app.util.task;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import docking.widgets.OptionDialog;
import ghidra.app.plugin.core.progmgr.ProgramLocator;
@@ -177,16 +176,20 @@ public class ProgramOpener {
msg += "Please contact the Ghidra team for assistance.";
Msg.showError(this, null, "Error Opening " + filename, msg);
}
catch (Exception e) {
if (domainFile.isInWritableProject() && (e instanceof IOException)) {
RepositoryAdapter repo = domainFile.getParent().getProjectData().getRepository();
catch (IOException e) {
RepositoryAdapter repo = domainFile.getParent().getProjectData().getRepository();
if (repo != null && domainFile.isInWritableProject()) {
ClientUtil.handleException(repo, e, "Open File", null);
}
else {
Msg.showError(this, null, "Error Opening " + filename,
"Getting domain object failed.\n" + e.getMessage(), e);
"Getting domain object failed.\n" + e.getMessage());
}
}
catch (Exception e) {
Msg.showError(this, null, "Error Opening " + filename,
"Getting domain object failed.\n" + e.getMessage(), e);
}
return null;
}

View File

@@ -16,11 +16,14 @@
package ghidra.app.util.viewer.field;
import java.net.*;
import java.util.Set;
import java.util.TreeSet;
import docking.widgets.fieldpanel.field.AttributedString;
import generic.theme.GThemeDefaults.Colors.Messages;
import ghidra.app.nav.Navigatable;
import ghidra.app.services.ProgramManager;
import ghidra.framework.client.ClientUtil;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.program.model.listing.Program;
@@ -33,9 +36,24 @@ import ghidra.util.Msg;
* The first string will be treated as a Java {@link URL} and the optional second string will
* be treated as display text. If there is not display text, then the URL will be
* displayed.
* <p>
* See {@link GhidraServerURLAnnotatedStringHandler} and {@link GhidraLocalURLAnnotatedStringHandler}
* for GHIDRA URL dummy handlers that are used to facilitate supported Comment Editor annotation types.
*/
public class URLAnnotatedStringHandler implements AnnotatedStringHandler {
private static final Set<String> allowedProtocols = new TreeSet<>();
static {
// Set maintains alphabetical order or protocols
// The 'ghidra' protocol must be included here since only one shared
// annotation handler is used to process all supported URL protocols.
allowedProtocols.add("ghidra");
allowedProtocols.add("http");
allowedProtocols.add("https");
}
private static String allowedProtocolsStr = "ghidra, https or http";
private static final String INVALID_SYMBOL_TEXT =
"@url annotation must have a URL string optionally followed by a display string";
@@ -53,7 +71,15 @@ public class URLAnnotatedStringHandler implements AnnotatedStringHandler {
URL url = getURLForString(text[1]);
if (url == null) {
return new AttributedString("Invalid URL annotations - not a URL: " + text[1],
return new AttributedString("Invalid URL annotation - not a valid URL: " + text[1],
Messages.ERROR, prototypeString.getFontMetrics(0), false, Messages.ERROR);
}
String protocol = url.getProtocol();
if (!allowedProtocols.contains(protocol)) {
return new AttributedString(
"Unsupported URL annotation protocol - " + allowedProtocolsStr + " required:\n" +
text[1],
Messages.ERROR, prototypeString.getFontMetrics(0), false, Messages.ERROR);
}
@@ -91,28 +117,45 @@ public class URLAnnotatedStringHandler implements AnnotatedStringHandler {
String urlString = annotationParts[1];
URL url = getURLForString(urlString);
if (url != null) {
String protocol = url.getProtocol();
if (!allowedProtocols.contains(protocol)) {
Msg.showError(this, null, "URL Access Not Allowed",
"Unsupported URL annotation protocol - " + allowedProtocolsStr +
" required:\n\n" +
urlString);
return false;
}
if (!ClientUtil.getAllowListProvider().isAllowed(url)) {
Msg.showError(this, null, "URL Access Not Allowed",
"Access denied by Server Allow List");
return false;
}
if (GhidraURL.PROTOCOL.equals(url.getProtocol())) {
ProgramManager programManager = serviceProvider.getService(ProgramManager.class);
return programManager.openProgram(url, ProgramManager.OPEN_CURRENT) != null;
}
BrowserLoader.display(url, null, serviceProvider);
return true;
}
Msg.showError(this, null, "Invalid URL",
"Unable to create a Java URL object from string: " + urlString);
"Invalid URL annotation - not a valid URL: " + urlString);
return false;
}
@Override
public String getDisplayString() {
return "URL";
return "HTTP-URL";
}
@Override
public String getPrototypeString() {
return "{@url http://www.example.com}";
return "{@url https://www.example.com}";
}
@Override

View File

@@ -21,8 +21,7 @@ import java.io.File;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import docking.options.OptionsService;
import ghidra.framework.options.OptionsChangeListener;
@@ -36,15 +35,6 @@ import ghidra.framework.plugintool.ServiceProvider;
*/
public class BrowserLoader {
/**
* Display the content specified by url in a web browser window. This call will launch
* a new thread and then immediately return.
* @param url The URL to show.
*/
public static void display(URL url) {
display(url, null, null);
}
/**
* Display the content specified by url in a web browser window. This call will launch
* a new thread and then immediately return.
@@ -57,6 +47,8 @@ public class BrowserLoader {
if (url == null) {
return;
}
Objects.requireNonNull(serviceProvider, "serviceProvider instance is required");
// open the browser in a new thread because the call may block
(new Thread(new BrowserRunner(url, fileURL, serviceProvider))).start();
@@ -65,12 +57,7 @@ public class BrowserLoader {
private static void displayFromBrowserRunner(URL url, URL fileURL,
ServiceProvider serviceProvider) {
try {
if (serviceProvider == null) {
displayBrowserForExternalURL(url);
}
else {
displayBrowser(url, fileURL, serviceProvider);
}
displayBrowser(url, fileURL, serviceProvider);
}
catch (Exception e) {
Msg.showError(BrowserLoader.class, null, "Error Loading Browser",
@@ -78,15 +65,6 @@ public class BrowserLoader {
}
}
private static void displayBrowserForExternalURL(URL url) throws Exception {
String[] arguments =
generateCommandArguments(url, null,
ManualViewerCommandWrappedOption.getDefaultBrowserLoaderOptions());
Process p = Runtime.getRuntime().exec(arguments);
p.waitFor();
p.exitValue(); // thought to help memory problems on some versions of windows
}
private static void displayBrowser(URL url, URL fileURL, ServiceProvider serviceProvider) {
OptionsService service = serviceProvider.getService(OptionsService.class);
ToolOptions options =

View File

@@ -135,8 +135,8 @@ public class ManualViewerCommandWrappedOption implements CustomOption {
option.setFileFormat(DEFAULT_URL_REPLACEMENT_STRING);
}
else if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.MAC_OS_X) {
option.setCommandString("open");
option.setCommandArguments(new String[] {});
option.setCommandString("anaconda-navigator");
option.setCommandArguments(new String[] { "--url" });
option.setFileFormat(DEFAULT_URL_REPLACEMENT_STRING);
}
else {