diff --git a/Ghidra/Features/BSim/ghidra_scripts/AddProgramToH2BSimDatabaseScript.java b/Ghidra/Features/BSim/ghidra_scripts/AddProgramToH2BSimDatabaseScript.java index adbe50e621..de1238652f 100644 --- a/Ghidra/Features/BSim/ghidra_scripts/AddProgramToH2BSimDatabaseScript.java +++ b/Ghidra/Features/BSim/ghidra_scripts/AddProgramToH2BSimDatabaseScript.java @@ -26,7 +26,7 @@ import ghidra.app.script.GhidraScript; import ghidra.features.base.values.GhidraValuesMap; import ghidra.features.bsim.query.*; import ghidra.features.bsim.query.BSimServerInfo.DBType; -import ghidra.features.bsim.query.FunctionDatabase.Error; +import ghidra.features.bsim.query.FunctionDatabase.BSimError; import ghidra.features.bsim.query.FunctionDatabase.ErrorCategory; import ghidra.features.bsim.query.description.DatabaseInformation; import ghidra.features.bsim.query.description.DescriptionManager; @@ -71,8 +71,7 @@ public class AddProgramToH2BSimDatabaseScript extends GhidraScript { askValues("Select Database File", null, values); File h2DbFile = values.getFile(DATABASE); - BSimServerInfo serverInfo = - new BSimServerInfo(DBType.file, null, 0, h2DbFile.getAbsolutePath()); + BSimServerInfo serverInfo = new BSimServerInfo(h2DbFile.getAbsolutePath()); BSimH2FileDataSource existingBDS = BSimH2FileDBConnectionManager.getDataSourceIfExists(serverInfo); @@ -129,7 +128,7 @@ public class AddProgramToH2BSimDatabaseScript extends GhidraScript { InsertRequest insertreq = new InsertRequest(); insertreq.manage = manager; if (insertreq.execute(h2Database) == null) { - Error lastError = h2Database.getLastError(); + BSimError lastError = h2Database.getLastError(); if ((lastError.category == ErrorCategory.Format) || (lastError.category == ErrorCategory.Nonfatal)) { Msg.showWarn(this, null, "Skipping Insert", diff --git a/Ghidra/Features/BSim/ghidra_scripts/CreateH2BSimDatabaseScript.java b/Ghidra/Features/BSim/ghidra_scripts/CreateH2BSimDatabaseScript.java index f30b1c65d5..1d8763bd5d 100644 --- a/Ghidra/Features/BSim/ghidra_scripts/CreateH2BSimDatabaseScript.java +++ b/Ghidra/Features/BSim/ghidra_scripts/CreateH2BSimDatabaseScript.java @@ -25,7 +25,7 @@ import ghidra.app.script.GhidraScript; import ghidra.features.base.values.GhidraValuesMap; import ghidra.features.bsim.query.*; import ghidra.features.bsim.query.BSimServerInfo.DBType; -import ghidra.features.bsim.query.FunctionDatabase.Error; +import ghidra.features.bsim.query.FunctionDatabase.BSimError; import ghidra.features.bsim.query.description.DatabaseInformation; import ghidra.features.bsim.query.file.BSimH2FileDBConnectionManager; import ghidra.features.bsim.query.file.BSimH2FileDBConnectionManager.BSimH2FileDataSource; @@ -89,8 +89,7 @@ public class CreateH2BSimDatabaseScript extends GhidraScript { List cats = parseCSV(exeCatCSV); File dbFile = new File(dbDir, databaseName); - BSimServerInfo serverInfo = - new BSimServerInfo(DBType.file, null, 0, dbFile.getAbsolutePath()); + BSimServerInfo serverInfo = new BSimServerInfo(dbFile.getAbsolutePath()); BSimH2FileDataSource existingBDS = BSimH2FileDBConnectionManager.getDataSourceIfExists(serverInfo); @@ -118,7 +117,7 @@ public class CreateH2BSimDatabaseScript extends GhidraScript { req.tag_name = tag; ResponseInfo resp = req.execute(h2Database); if (resp == null) { - Error lastError = h2Database.getLastError(); + BSimError lastError = h2Database.getLastError(); throw new LSHException(lastError.message); } } @@ -128,7 +127,7 @@ public class CreateH2BSimDatabaseScript extends GhidraScript { req.type_name = cat; ResponseInfo resp = req.execute(h2Database); if (resp == null) { - Error lastError = h2Database.getLastError(); + BSimError lastError = h2Database.getLastError(); throw new LSHException(lastError.message); } } diff --git a/Ghidra/Features/BSim/ghidra_scripts/QueryWithFiltersScript.java b/Ghidra/Features/BSim/ghidra_scripts/QueryWithFiltersScript.java index 73d6f5a103..4a474b506e 100755 --- a/Ghidra/Features/BSim/ghidra_scripts/QueryWithFiltersScript.java +++ b/Ghidra/Features/BSim/ghidra_scripts/QueryWithFiltersScript.java @@ -4,9 +4,9 @@ * 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. @@ -130,7 +130,7 @@ public class QueryWithFiltersScript extends GhidraScript { String dbUrl = askString("", "Enter the URL of the BSim database:", "ghidra://localhost/bsimDb"); queryService.initializeDatabase(dbUrl); - FunctionDatabase.Error error = queryService.getLastError(); + FunctionDatabase.BSimError error = queryService.getLastError(); if (error != null && error.category == ErrorCategory.Nodatabase) { println("Database [" + dbUrl + "] cannot be found (does it exist?)"); return; diff --git a/Ghidra/Features/BSim/src/main/help/help/topics/BSim/CommandLineReference.html b/Ghidra/Features/BSim/src/main/help/help/topics/BSim/CommandLineReference.html index a3de12661f..57cfba217f 100644 --- a/Ghidra/Features/BSim/src/main/help/help/topics/BSim/CommandLineReference.html +++ b/Ghidra/Features/BSim/src/main/help/help/topics/BSim/CommandLineReference.html @@ -326,7 +326,12 @@

Creates a new empty repository. A URL and configuration template (config_template) is required. The new database name - is taken from the path element of the URL.

+ is taken from the path element of the URL. See “Creating a + Database” for more details and discussion on configuration template use. + See “Creating Database Templates“ + if a standard template will not suffice.

Supported configuration templates (config_template) are defined within the Ghidra @@ -792,21 +797,21 @@ PostgreSQL postgresql://<hostname>[:<port>]/<dbname> + "computeroutput">postgresql://[<username>@]<hostname>[:<port>]/<dbname> Elasticsearch https://<hostname>[:<port>]/<dbname> + "computeroutput">https://[<username>@]<hostname>[:<port>]/<dbname> Elasticsearch elastic://<hostname>[:<port>]/<dbname> + "computeroutput">elastic://[<username>@]<hostname>[:<port>]/<dbname> @@ -819,6 +824,24 @@

The use of the https and elastic is equivalent.

+ +

Tip: The inclusion of a + <username> within a BSim URL + supercedes the concurrent use of the --user + option which can still be used to control login to the Ghidra Server. When a + <username> has been specified within a BSim URL + a <password> may be included if neccessary, + albeit highly discouraged (e.g., + postgresql://username:password@hostname/dbname). + The password is appended to the username with a colon (':') separator and has limitations + on the characters which may be used.

+ +

Warning: Inclusion of a user + <password> within a BSim URL is highly + discouraged and only intended for use within restricted environments since the URL entry will persist + within the system process table and possibly within system log files. This may be useful + in controlled situations where console password prompts cannot be handled. Handling + the password prompt is preferred.

For local file URLs, the absolute path the H2 database *.mv.db file must be specified without the *.mv.db extension. Only the '/' character should be diff --git a/Ghidra/Features/BSim/src/main/help/help/topics/BSim/DatabaseConfiguration.html b/Ghidra/Features/BSim/src/main/help/help/topics/BSim/DatabaseConfiguration.html index e9826369c5..e870355987 100644 --- a/Ghidra/Features/BSim/src/main/help/help/topics/BSim/DatabaseConfiguration.html +++ b/Ghidra/Features/BSim/src/main/help/help/topics/BSim/DatabaseConfiguration.html @@ -527,6 +527,34 @@ that are different from the standard PostgreSQL defaults. To provide site specific configuration, changes should be made to the normal PostgreSQL configuration files.

+ +
+
+
+
+

PostgreSQL Firewall Considerations

+
+
+
+ +

Remote client access to the PostgreSQL server may have network firewalls + which must be considered and properly configured to allow network access. + Both dedicated network firewalls and OS-level firewalls must be considered.

+ +

Firewall configurations are beyond the scope of this document, however for simple + single-node installations on Linux the firewall-cmd + may be used to allow incoming connections to the appropriate port (TCP port 5432 is the + default for PostgreSQL). This does not consider network firewall devices which may + also impact connectivity.

+ +

+    sudo firewall-cmd --permanent --add-port=5432/tcp && sudo firewall-cmd --reload
+			
+ +

NOTE: The above Linux firewall command assumes the firewalld + package has been installed on the system.

+ +
@@ -562,13 +590,22 @@

In order to make use of Elasticsearch with BSim, the database administrator must install the lsh.zip plug-in as part of the - Elasticsearch deployment. The plug-in is available in the Ghidra add-on named BSimElasticPlugin, which unpacks into a standard - Ghidra installation. The file lsh.zip is a - standard Elasticsearch plug-in that must be installed on every node of the cluster + Elasticsearch deployment. The plug-in is available in the Ghidra extension named BSimElasticPlugin + (<ghidra-install-dir>/Extensions/Ghidra/ghidra_11.2.1_U_20241105_BSimElasticPlugin.zip). + The extension may be unzipped to a temporary location or use Ghidra's Install Extensions + capability which will unpack it into the users platform-specific config directory + indicated in the detail view of the Install Extensions window. The extension is not + used directly by Ghidra and is only needed for the + lsh.zip elasticsearch plug-in + which must be installed on every node of the cluster before a BSim repository can be created. The description below shows how to enable the BSim plug-in for a single node, but this will need to be repeated for any additional nodes.

+ +

Tip: Refer to the README.md + file within the unpacked extension for important plugin details. The plugin file + stipulates a specific elasticsearch version and may require an adjustment and repack.

Assuming the add-on has been unpacked, the plug-in can be installed to a single node using the elasticsearch-plugin command in the @@ -579,7 +616,7 @@ + file:///path/to/ghidra/extension/BSimElasticPlugin/data/lsh.zip
bin/elasticsearch-plugin install - file:///path/to/ghidra/Ghidra/contrib/BSimElasticPlugin/data/lsh.zip

@@ -610,7 +647,7 @@ -

The open Elasticsearch distribution starts with up with password authentication +

The open Elasticsearch distribution starts with password authentication enabled by default. When a node is started up for the first time, as described above, an elastic user is created with a randomly generated password that is reported, once, to the console. For a toy deployment, it may @@ -665,6 +702,34 @@ curl -k -u elastic:XXXXXX -X POST "https://localhost:9200/_security/user/ghidrau class="xref" href="CommandLineReference.html#URLs">“Ghidra and BSim URLs” for additional information about URLs.

+ +
+
+
+
+

Elasticsearch Firewall Considerations

+
+
+
+ +

Remote client access to the Elasticsearch server/cluster may have network firewalls + which must be considered and properly configured to allow network access. + Both dedicated network firewalls and OS-level firewalls must be considered.

+ +

Firewall configurations are beyond the scope of this document, however for simple + single-node installations on Linux the firewall-cmd + may be used to allow incoming connections to the appropriate port (TCP port 9200 is the + default for elasticsearch). This does not consider network firewall devices which may + also impact connectivity.

+ +

+    sudo firewall-cmd --permanent --add-port=9200/tcp && sudo firewall-cmd --reload
+			
+ +

NOTE: The above Linux firewall command assumes the firewalld + package has been installed on the system.

+ +
@@ -672,8 +737,7 @@ curl -k -u elastic:XXXXXX -X POST "https://localhost:9200/_security/user/ghidrau
-

Creating a - Database

+

Creating a Database

diff --git a/Ghidra/Features/BSim/src/main/help/help/topics/BSimSearchPlugin/BSimSearch.html b/Ghidra/Features/BSim/src/main/help/help/topics/BSimSearchPlugin/BSimSearch.html index ffecfeb75d..9772c21b96 100644 --- a/Ghidra/Features/BSim/src/main/help/help/topics/BSimSearchPlugin/BSimSearch.html +++ b/Ghidra/Features/BSim/src/main/help/help/topics/BSimSearchPlugin/BSimSearch.html @@ -53,9 +53,23 @@
-

The dialog displays a table showing all the currently defined BSim databases/servers. Each - entry shows a name for the BSim database, its type (postgres, elastic, or file), a host ip - and port (if applicable), and finally the number of active connections.

+

The dialog displays a table showing all the currently defined BSim databases/servers. + Table columns displayed include:

+ +

There are four primary actions for this dialog:

@@ -68,8 +82,8 @@ selected entry will be deleted. This action will force an immediate disconnect for an active/idle connection and should be used with care. -
  •   - Connect or disconnect an inactive database/server connection. This action is not supported +
  •  Connect + or disconnect an inactive database/server connection which has been selected. This action is not supported by Elastic database servers.
  •  Change password - A change password @@ -91,9 +105,11 @@

    Choose the type of BSim database first as that will affect the type of information that - needs to be specified. For postgres and elastic, you need to enter host and port. For file, - you see a button for using a filechooser to pick the file that is the local BSim H2 - database.

    + needs to be specified. For postgres and elastic, you must enter a database name, + hostname/address, and port. Specify a host name of "localhost" for a server running on the local system. + Invalid or missing entries are shown in red. For a file database, + the "..." button is used to launch a filechooser for selecting an existing H2 database file + (*.mv.db).

    diff --git a/Ghidra/Features/BSim/src/main/help/help/topics/BSimSearchPlugin/images/AddServerDialog.png b/Ghidra/Features/BSim/src/main/help/help/topics/BSimSearchPlugin/images/AddServerDialog.png index 1210541490..0594435747 100644 Binary files a/Ghidra/Features/BSim/src/main/help/help/topics/BSimSearchPlugin/images/AddServerDialog.png and b/Ghidra/Features/BSim/src/main/help/help/topics/BSimSearchPlugin/images/AddServerDialog.png differ diff --git a/Ghidra/Features/BSim/src/main/help/help/topics/BSimSearchPlugin/images/ManageServersDialog.png b/Ghidra/Features/BSim/src/main/help/help/topics/BSimSearchPlugin/images/ManageServersDialog.png index 59f713dc8c..10c432c4e0 100644 Binary files a/Ghidra/Features/BSim/src/main/help/help/topics/BSimSearchPlugin/images/ManageServersDialog.png and b/Ghidra/Features/BSim/src/main/help/help/topics/BSimSearchPlugin/images/ManageServersDialog.png differ diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/BSimServerManager.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/BSimServerManager.java index df21cbbbfb..387c682b88 100644 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/BSimServerManager.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/BSimServerManager.java @@ -77,10 +77,11 @@ public class BSimServerManager { String dbTypeName = properties.getString("DBType", null); DBType dbType = DBType.valueOf(dbTypeName); String name = properties.getString("Name", null); + String user = properties.getString("User", null); String host = properties.getString("Host", null); int port = properties.getInt("Port", 0); if (dbType != null && name != null) { - BSimServerInfo info = new BSimServerInfo(dbType, host, port, name); + BSimServerInfo info = new BSimServerInfo(dbType, user, host, port, name); return info; } Msg.showError(this, null, "Error reading Bsim Server File", @@ -97,6 +98,10 @@ public class BSimServerManager { GProperties properties = new GProperties("BSimServerInfo"); properties.putString("DBType", info.getDBType().name()); properties.putString("Name", info.getDBName()); + if (!info.hasDefaultLogin()) { + // save specified username - but not password + properties.putString("User", info.getUserName()); + } properties.putString("Host", info.getServerName()); properties.putInt("Port", info.getPort()); diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/dialog/BSimServerDialog.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/dialog/BSimServerDialog.java index bec9aeee2e..71098dabf3 100644 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/dialog/BSimServerDialog.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/dialog/BSimServerDialog.java @@ -35,7 +35,7 @@ import generic.theme.GIcon; import ghidra.features.bsim.gui.BSimServerManager; import ghidra.features.bsim.query.*; import ghidra.features.bsim.query.BSimServerInfo.DBType; -import ghidra.features.bsim.query.FunctionDatabase.Error; +import ghidra.features.bsim.query.FunctionDatabase.BSimError; import ghidra.features.bsim.query.FunctionDatabase.ErrorCategory; import ghidra.framework.plugintool.PluginTool; import ghidra.util.*; @@ -175,7 +175,7 @@ public class BSimServerDialog extends DialogComponentProvider { try (FunctionDatabase db = BSimClientFactory.buildClient(serverInfo, true)) { if (!db.initialize()) { // TODO: Need standardized error handler - Error lastError = db.getLastError(); + BSimError lastError = db.getLastError(); if (lastError.category != ErrorCategory.AuthenticationCancelled) { Msg.showError(this, getComponent(), "BSim DB Connection Failed", lastError.message); @@ -195,7 +195,7 @@ public class BSimServerDialog extends DialogComponentProvider { return; // password dialog entry cancelled by user } - String resp = db.changePassword(db.getUserName(), pwd); + String resp = db.changePassword(pwd); if (resp == null) { Msg.showInfo(this, getComponent(), "Password Changed", "BSim DB password successfully changed"); diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/dialog/BSimServerTableModel.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/dialog/BSimServerTableModel.java index 8bed9cc704..d5bace2917 100644 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/dialog/BSimServerTableModel.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/dialog/BSimServerTableModel.java @@ -21,11 +21,15 @@ import java.util.*; import javax.swing.Icon; import javax.swing.JLabel; +import org.apache.commons.lang3.StringUtils; + import docking.widgets.table.*; import ghidra.docking.settings.Settings; import ghidra.features.bsim.gui.BSimServerManager; -import ghidra.features.bsim.query.BSimServerInfo; +import ghidra.features.bsim.query.*; +import ghidra.features.bsim.query.BSimPostgresDBConnectionManager.BSimPostgresDataSource; import ghidra.features.bsim.query.BSimServerInfo.DBType; +import ghidra.framework.client.ClientUtil; import ghidra.framework.plugintool.ServiceProvider; import ghidra.framework.plugintool.ServiceProviderStub; import ghidra.util.table.column.AbstractGColumnRenderer; @@ -90,6 +94,7 @@ public class BSimServerTableModel extends GDynamicColumnTableModel { + + @Override + public String getColumnName() { + return "User"; + } + + @Override + public String getValue(BSimServerInfo serverInfo, Settings settings, Object data, + ServiceProvider provider) throws IllegalArgumentException { + if (serverInfo.hasDefaultLogin()) { + if (serverInfo.getDBType() == DBType.postgres) { + BSimPostgresDataSource ds = + BSimPostgresDBConnectionManager.getDataSourceIfExists(serverInfo); + if (ds != null) { + return ds.getUserName(); + } + } + // TODO: how can we determine elastic username? + return ""; + } + String info = serverInfo.getUserName(); + boolean hasPassword = serverInfo.hasPassword(); + if (hasPassword) { + info = info + ":****"; // show w/masked password + } + return info; + } + + @Override + public int getColumnPreferredWidth() { + return 100; + } + } + private static class HostColumn extends AbstractDynamicTableColumn { @@ -176,7 +217,6 @@ public class BSimServerTableModel extends GDynamicColumnTableModel checkForValidDialog()); + } + + private static final String HOSTNAME_IP_REGEX = + "^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*(\\.[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*)*$"; + private static final Pattern HOSTNAME_IP_PATTERN = Pattern.compile(HOSTNAME_IP_REGEX); + + private void createHostField() { + + hostField = new GFormattedTextField(FORMATTER_FACTORY, ""); + hostField.setName("Host"); + hostField.setText(""); + hostField.setDefaultValue(""); + hostField.setIsError(true); + hostField.setEditable(true); + + hostField.setInputVerifier(new InputVerifier() { + @Override + public boolean verify(JComponent input) { + setStatus(""); + String hostname = hostField.getText().trim(); + if (hostname.length() == 0) { + setStatus(""); + return false; + } + Matcher hostMatch = HOSTNAME_IP_PATTERN.matcher(hostname); + if (!hostMatch.matches()) { + setStatus("Unsupported host name or IP address"); + return false; + } + return true; + } + + @Override + public boolean shouldYieldFocus(JComponent source, JComponent target) { + return true; + } + }); + + hostField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + e.consume(); + hostField.setText(""); + hostField.setDefaultValue(""); + hostField.setIsError(true); + } + checkForValidDialog(); + } + }); + + hostField.addTextEntryStatusListener(f -> checkForValidDialog()); + } + + // NOTE: Username pattern based on PostgreSQL restrictions + private static final String USERNAME_REGEX = "^[a-zA-Z_][a-zA-Z0-9_$]*$"; + private static final Pattern USERNAME_PATTERN = Pattern.compile(USERNAME_REGEX); + + private void createUserField() { + + userField = new GFormattedTextField(FORMATTER_FACTORY, ""); + userField.setName("User"); + userField.setText(""); + userField.setDefaultValue(""); + userField.setEditable(true); + + userField.setInputVerifier(new InputVerifier() { + @Override + public boolean verify(JComponent input) { + setStatus(""); + String username = userField.getText().trim(); + if (username.length() == 0) { + setStatus(""); + return true; + } + Matcher userMatch = USERNAME_PATTERN.matcher(username); + if (!userMatch.matches()) { + setStatus("Unsupported database user name"); + return false; + } + return true; + } + + @Override + public boolean shouldYieldFocus(JComponent source, JComponent target) { + return true; + } + }); + + userField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + e.consume(); + userField.setText(""); + userField.setDefaultValue(""); + userField.setIsError(false); + } + checkForValidDialog(); + } + }); + + userField.addTextEntryStatusListener(f -> checkForValidDialog()); + } + @Override BSimServerInfo getServerInfo() { + if (nameField.getTextEntryStatus() == Status.INVALID || + userField.getTextEntryStatus() == Status.INVALID || + hostField.getTextEntryStatus() == Status.INVALID) { + return null; + } + + String user = userField.getText().trim(); + if (ClientUtil.getUserName().equals(user)) { + user = null; + } + String name = nameField.getText().trim(); String host = hostField.getText().trim(); + int port = getPort(portField.getText().trim()); if (name.isBlank() || host.isBlank() || port < 0) { return null; } - return new BSimServerInfo(type, host, port, name); + + return new BSimServerInfo(type, user, host, port, name); } } @@ -291,7 +488,7 @@ public class CreateBsimServerInfoDialog extends DialogComponentProvider { if (file.isDirectory()) { return null; } - return new BSimServerInfo(DBType.file, null, -1, path); + return new BSimServerInfo(path); } } @@ -303,24 +500,24 @@ public class CreateBsimServerInfoDialog extends DialogComponentProvider { public NotifyingTextField(String initialText) { super(20); setText(initialText); - getDocument().addDocumentListener(new DocumentListener() { + getDocument().addDocumentListener(new MyFieldListener()); + } + } - @Override - public void insertUpdate(DocumentEvent e) { - checkForValidDialog(); - } + class MyFieldListener implements DocumentListener { + @Override + public void insertUpdate(DocumentEvent e) { + checkForValidDialog(); + } - @Override - public void removeUpdate(DocumentEvent e) { - checkForValidDialog(); - } + @Override + public void removeUpdate(DocumentEvent e) { + checkForValidDialog(); + } - @Override - public void changedUpdate(DocumentEvent e) { - checkForValidDialog(); - } - - }); + @Override + public void changedUpdate(DocumentEvent e) { + checkForValidDialog(); } } diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimPostgresDBConnectionManager.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimPostgresDBConnectionManager.java index bd3ac7176c..cf912e746a 100644 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimPostgresDBConnectionManager.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimPostgresDBConnectionManager.java @@ -25,7 +25,6 @@ import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import org.apache.commons.dbcp2.BasicDataSource; -import org.apache.commons.lang3.StringUtils; import ghidra.features.bsim.query.BSimServerInfo.DBType; import ghidra.features.bsim.query.FunctionDatabase.ConnectionType; @@ -283,10 +282,12 @@ public class BSimPostgresDBConnectionManager { */ private Connection connect() throws SQLException, CancelledException { - String userName = bds.getUsername(); - bds.setUsername(StringUtils.isBlank(userName) ? ClientUtil.getUserName() : userName); - bds.setPassword(null); - connectionType = ConnectionType.SSL_No_Authentication; + String loginError = null; + + serverInfo.setUserInfo(bds); + + connectionType = serverInfo.hasPassword() ? ConnectionType.SSL_Password_Authentication + : ConnectionType.SSL_No_Authentication; try { // Specify SSL connection properties setSSLProperties(); @@ -299,6 +300,10 @@ public class BSimPostgresDBConnectionManager { if (e.getMessage().contains("password-based authentication") || e.getMessage().contains("SCRAM-based") || e.getMessage().contains("password authentication failed")) { + if (serverInfo.hasPassword()) { + loginError = "Access denied: " + serverInfo; + Msg.error(this, loginError); + } // Use Ghidra's authentication infrastructure connectionType = ConnectionType.SSL_Password_Authentication; // Try again with a password // fallthru to second attempt at getConnection @@ -319,7 +324,6 @@ public class BSimPostgresDBConnectionManager { " idle=" + bds.getNumIdle()); } - String loginError = null; while (true) { ClientAuthenticator clientAuthenticator = null; if (connectionType == ConnectionType.SSL_Password_Authentication) { @@ -327,9 +331,11 @@ public class BSimPostgresDBConnectionManager { if (clientAuthenticator == null) { // Make sure authenticator is registered throw new SQLException("No registered authenticator"); } - NameCallback nameCb = new NameCallback("User ID:"); - nameCb.setName(bds.getUsername()); - PasswordCallback passCb = new PasswordCallback("Password:", false); + NameCallback nameCb = new NameCallback("User ID:", bds.getUsername()); + if (!serverInfo.hasDefaultLogin()) { + nameCb.setName(bds.getUsername()); + } + PasswordCallback passCb = new PasswordCallback(" ", false); // force use of default prompting try { if (!clientAuthenticator.processPasswordCallbacks( "BSim Database Authentication", "BSim Database Server", @@ -338,9 +344,8 @@ public class BSimPostgresDBConnectionManager { } bds.setPassword(new String(passCb.getPassword())); // User may have specified new username, or this may return NULL - userName = nameCb.getName(); - if (!StringUtils.isBlank(userName)) { - bds.setUsername(userName); + if (serverInfo.hasDefaultLogin()) { + bds.setUsername(nameCb.getName()); } } finally { diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimServerInfo.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimServerInfo.java index 2fe9cfbc74..05ccc40925 100644 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimServerInfo.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/BSimServerInfo.java @@ -4,9 +4,9 @@ * 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. @@ -16,12 +16,15 @@ package ghidra.features.bsim.query; import java.io.Closeable; -import java.net.MalformedURLException; -import java.net.URL; +import java.net.*; +import java.nio.charset.StandardCharsets; import java.util.Objects; +import org.apache.commons.dbcp2.BasicDataSource; import org.apache.commons.lang3.StringUtils; +import ghidra.framework.client.ClientUtil; + public class BSimServerInfo implements Comparable { /** @@ -48,6 +51,7 @@ public class BSimServerInfo implements Comparable { } private final DBType dbType; + private final String userinfo; // username[:password] private final String host; private final int port; private final String dbName; @@ -56,6 +60,62 @@ public class BSimServerInfo implements Comparable { /** * Construct a new {@link BSimServerInfo} object + * + * @param dbType BSim DB type + * @param userinfo connection user info, {@code username[:password]} (ignored for {@link DBType#file}). + * If blank, {@link ClientUtil#getUserName()} is used. + * @param host host name (ignored for {@link DBType#file}) + * @param port port number (ignored for {@link DBType#file}) + * @param dbName name of database (simple database name except for {@link DBType#file} + * which should reflect an absolute file path. On Windows OS the path may start with a + * drive letter. + * @throws IllegalArgumentException if invalid arguments are specified + */ + public BSimServerInfo(DBType dbType, String userinfo, String host, int port, String dbName) { + Objects.requireNonNull(dbType, "DBType must be specified"); + this.dbType = dbType; + + if ((dbType == DBType.postgres || dbType == DBType.elastic) && StringUtils.isEmpty(host)) { + throw new IllegalArgumentException("host required"); + } + + dbName = dbName.trim(); + if (StringUtils.isEmpty(dbName)) { + throw new IllegalArgumentException("Non-empty dbName required"); + } + + if (dbType == DBType.file) { + host = null; + port = -1; + userinfo = null; + dbName = cleanupFilename(dbName); + } + else { + if (dbName.contains("/") || dbName.contains("\\")) { // may want additional validation + throw new IllegalArgumentException("Invalid " + dbType + " dbName: " + dbName); + } + userinfo = cleanupUserInfo(userinfo); + if (port <= 0) { + port = -1; + } + if (dbType == DBType.postgres && port <= 0) { + port = DEFAULT_POSTGRES_PORT; + } + if (dbType == DBType.elastic && port <= 0) { + port = DEFAULT_ELASTIC_PORT; + } + } + + this.userinfo = userinfo; + this.host = host; + this.port = port; + this.dbName = dbName; + } + + /** + * Construct a new {@link BSimServerInfo} object. For non-file database the user's defaut + * username is used (see {@link ClientUtil#getUserName()}). + * * @param dbType BSim DB type * @param host host name (ignored for {@link DBType#file}) * @param port port number (ignored for {@link DBType#file}) @@ -65,49 +125,34 @@ public class BSimServerInfo implements Comparable { * @throws IllegalArgumentException if invalid arguments are specified */ public BSimServerInfo(DBType dbType, String host, int port, String dbName) { - Objects.requireNonNull(dbType, "DBType must be specified"); - this.dbType = dbType; - - if ((dbType == DBType.postgres || dbType == DBType.elastic) && StringUtils.isEmpty(host)) { - throw new IllegalArgumentException("host required"); - } - this.host = host; - - if (port <= 0) { - port = -1; - } - if (dbType == DBType.postgres && port <= 0) { - port = DEFAULT_POSTGRES_PORT; - } - if (dbType == DBType.elastic && port <= 0) { - port = DEFAULT_ELASTIC_PORT; - } - this.port = port; + this(dbType, null, host, port, dbName); + } + /** + * Construct a new {@link BSimServerInfo} object for a {@link DBType#file} type database. + * + * @param dbName name of database which should reflect an absolute file path. + * On Windows OS the path may start with a drive letter. + * @throws IllegalArgumentException if invalid arguments are specified + */ + public BSimServerInfo(String dbName) { + dbType = DBType.file; + userinfo = null; + host = null; + port = -1; dbName = dbName.trim(); if (StringUtils.isEmpty(dbName)) { throw new IllegalArgumentException("Non-empty dbName required"); } - if (dbType == DBType.file) { - // transform dbName into acceptable H2 DB file path - dbName = dbName.replace("\\", "/"); - if ((!dbName.startsWith("/") && !isWindowsFilePath(dbName)) || dbName.endsWith("/")) { - throw new IllegalArgumentException("Invalid absolute file path: " + dbName); - } - if (!dbName.endsWith(H2_FILE_EXTENSION)) { - dbName += H2_FILE_EXTENSION; - } - } - else if (dbName.contains("/") || dbName.contains("\\")) { // may want additional validation - throw new IllegalArgumentException("Invalid " + dbType + " dbName: " + dbName); - } - this.dbName = dbName; + this.dbName = cleanupFilename(dbName); } /** * Construct a new {@link BSimServerInfo} object from a suitable database URL * (i.e., {@code postgresql:}, {@code https:}, {@code elastic:}, {@code file:}). - * @param url supported BSim database URL + * + * @param url supported BSim database URL. For non-file URLs, the hostname or + * address may be preceeded by a DB username (e.g., postgresql://user@host:port/dbname * @throws IllegalArgumentException if unsupported URL protocol specified */ public BSimServerInfo(URL url) throws IllegalArgumentException { @@ -118,18 +163,21 @@ public class BSimServerInfo implements Comparable { if (protocol.equals("postgresql")) { t = DBType.postgres; host = checkURLField(url.getHost(), "host"); + userinfo = getURLUserInfo(url); int p = url.getPort(); port = p <= 0 ? DEFAULT_POSTGRES_PORT : p; } else if (protocol.equals("https") || protocol.equals("elastic")) { t = DBType.elastic; host = checkURLField(url.getHost(), "host"); + userinfo = getURLUserInfo(url); int p = url.getPort(); port = p <= 0 ? DEFAULT_ELASTIC_PORT : p; } else if (protocol.startsWith("file")) { t = DBType.file; host = null; + userinfo = null; port = -1; if (!"".equals(url.getHost())) { throw new IllegalArgumentException("Remote file URL not supported: " + url); @@ -146,7 +194,7 @@ public class BSimServerInfo implements Comparable { } path = path.substring(1).strip(); } - path = checkURLField(path, "path"); + path = urlDecode(checkURLField(path, "path")); if (dbType == DBType.file) { if (path.endsWith("/")) { throw new IllegalArgumentException("Missing DB filepath in URL: " + url); @@ -162,6 +210,53 @@ public class BSimServerInfo implements Comparable { dbName = path; } + private static String getURLUserInfo(URL url) { + + String userinfo = url.getUserInfo(); + if (userinfo == null) { + return null; + } + + int pwSep = userinfo.indexOf(':'); + String urlUserInfo; + if (pwSep >= 0) { + urlUserInfo = urlDecode(userinfo.substring(0, pwSep)) + ":" + + urlDecode(userinfo.substring(pwSep + 1)); + } + else { + urlUserInfo = urlDecode(userinfo); + } + return cleanupUserInfo(urlUserInfo); + } + + private static String cleanupUserInfo(String userinfo) { + if (StringUtils.isBlank(userinfo)) { + return null; + } + userinfo = userinfo.trim(); + int pwdSep = userinfo.indexOf(':'); + if (pwdSep == 0) { + throw new IllegalArgumentException("Invalid userinfo specified"); + } + else if (pwdSep > 0 && (userinfo.length() - pwdSep) == 0) { + throw new IllegalArgumentException("Invalid userinfo specified"); + } + return userinfo; + } + + private static String cleanupFilename(String name) { + // transform dbName into acceptable H2 DB file path + String dbName = name.trim(); + dbName = dbName.replace("\\", "/"); + if ((!dbName.startsWith("/") && !isWindowsFilePath(dbName)) || dbName.endsWith("/")) { + throw new IllegalArgumentException("Invalid absolute file path: " + dbName); + } + if (!dbName.endsWith(H2_FILE_EXTENSION)) { + dbName += H2_FILE_EXTENSION; + } + return dbName; + } + private static String checkURLField(String val, String name) { if (StringUtils.isEmpty(val)) { throw new IllegalArgumentException("Invalid " + name + " in URL"); @@ -199,29 +294,57 @@ public class BSimServerInfo implements Comparable { } /** - * Return BSim server info in URL format + * Return BSim server info in URL format. + * Warning: If userinfo with password has been specified it will be returned in the URL. * @return BSim server info in URL format */ public String toURLString() { switch (dbType) { case postgres: - return "postgresql://" + host + getPortString() + "/" + dbName; + return "postgresql://" + formatURLUserInfo() + host + getPortString() + "/" + + urlEncode(dbName); case elastic: - return "https://" + host + getPortString() + "/" + dbName; + return "https://" + formatURLUserInfo() + host + getPortString() + "/" + + urlEncode(dbName); case file: // h2: - return "file:" + dbName; + return "file:" + urlEncode(dbName); } throw new RuntimeException("Unsupported DBType: " + dbType); } + private static String urlEncode(String text) { + return URLEncoder.encode(text, StandardCharsets.UTF_8); + } + + private static String urlDecode(String text) { + return URLDecoder.decode(text, StandardCharsets.UTF_8); + } + + private String formatURLUserInfo() { + if (userinfo == null) { + return ""; + } + int pwSep = userinfo.indexOf(':'); + String urlUserInfo; + if (pwSep >= 0) { + urlUserInfo = urlEncode(userinfo.substring(0, pwSep)) + ":" + + urlEncode(userinfo.substring(pwSep + 1)); + } + else { + urlUserInfo = urlEncode(userinfo); + } + return urlUserInfo + "@"; + } + private String getPortString() { return port > 0 ? (":" + Integer.toString(port)) : ""; } /** - * Return BSim server info in URL + * Return BSim server info in URL. + * Warning: If userinfo with password has been specified it will be returned in the URL. * @return BSim server info in URL * @throws MalformedURLException if unable to form supported URL */ @@ -236,6 +359,58 @@ public class BSimServerInfo implements Comparable { return dbType; } + public void setUserInfo(BasicDataSource bds) { + bds.setUsername(getUserName()); + if (hasPassword()) { + bds.setPassword(userinfo.substring(userinfo.indexOf(':') + 1)); + } + } + + /** + * Determine if user information includes password. + * NOTE: Use of passwords with this object and URLs is discouraged. + * @return true if user information includes password which + */ + public boolean hasPassword() { + return userinfo != null && userinfo.contains(":"); + } + + /** + * Determine of user info was stipulated during construction + * @return true if user info was stipulated during construction + */ + public boolean hasDefaultLogin() { + return userinfo == null; + } + + /** + * Get the remote database user name to be used when establishing a connection. + * User name obtained from the user information which was provided during instantiation. + * @return remote database user information (null for {@link DBType#file}). + */ + public String getUserName() { + if (dbType == DBType.file) { + return null; + } + if (userinfo == null) { + return ClientUtil.getUserName(); + } + String username = userinfo; + int pwdSep = userinfo.indexOf(':'); + if (pwdSep > 0) { + username = userinfo.substring(0, pwdSep); + } + return username; + } + + /** + * Get the remote database user information to be used when establishing a connection. + * @return remote database user information (null for {@link DBType#file}). + */ + public String getUserInfo() { + return userinfo; + } + /** * Get the server hostname or IP address as originally specified. * @return hostname or IP address as originally specified @@ -282,7 +457,14 @@ public class BSimServerInfo implements Comparable { @Override public int hashCode() { // use dbType.ordinal; enum hashcodes vary from run to run - return Objects.hash(dbName, dbType.ordinal(), host, port); + int hashcode = Objects.hash(dbName, dbType.ordinal(), host, port); + // Due to the use of hashcode by BSimServerManager for persisting server entries + // we cannot change the hashing function above and must only incorporate inclusion + // of userinfo if it is specified. + if (userinfo != null) { + hashcode = 31 * hashcode + userinfo.hashCode(); + } + return hashcode; } @Override @@ -293,7 +475,8 @@ public class BSimServerInfo implements Comparable { return false; if (obj instanceof BSimServerInfo other) { return Objects.equals(dbName, other.dbName) && dbType == other.dbType && - Objects.equals(host, other.host) && port == other.port; + Objects.equals(userinfo, other.userinfo) && Objects.equals(host, other.host) && + port == other.port; } return false; } diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/FunctionDatabase.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/FunctionDatabase.java index 6998da5447..cfd2430352 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/FunctionDatabase.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/FunctionDatabase.java @@ -33,9 +33,7 @@ import ghidra.features.bsim.query.facade.SFOverviewInfo; import ghidra.features.bsim.query.facade.SFQueryInfo; import ghidra.features.bsim.query.protocol.*; import ghidra.framework.Application; -import ghidra.program.model.data.DataUtilities; import ghidra.util.Msg; -import ghidra.util.StringUtilities; public interface FunctionDatabase extends AutoCloseable { @@ -85,11 +83,11 @@ public interface FunctionDatabase extends AutoCloseable { } } - public static class Error { // Error structure returned by getLastError + public static class BSimError { // Error structure returned by getLastError public ErrorCategory category; public String message; - public Error(ErrorCategory cat, String msg) { + public BSimError(ErrorCategory cat, String msg) { category = cat; message = msg; } @@ -120,12 +118,11 @@ public interface FunctionDatabase extends AutoCloseable { /** * Issue password change request to the server. * The method {@link #isPasswordChangeAllowed()} must be invoked first to ensure that - * the user password may be changed. - * @param username to change + * the user password may be changed. * @param newPassword is password data * @return null if change was successful, or the error message */ - public default String changePassword(String username, char[] newPassword) { + public default String changePassword(char[] newPassword) { if (getStatus() != Status.Ready) { return "Connection not established"; } @@ -134,7 +131,7 @@ public interface FunctionDatabase extends AutoCloseable { } PasswordChange passwordChange = new PasswordChange(); try { - passwordChange.username = username; + passwordChange.username = getUserName(); passwordChange.newPassword = newPassword; ResponsePassword response = passwordChange.execute(this); if (!response.changeSuccessful) { @@ -162,14 +159,6 @@ public interface FunctionDatabase extends AutoCloseable { */ public String getUserName(); - /** - * Set a specific user name for connection. Must be called before connection is initialized. - * If this method is not called, connection will use user name of process - * - * @param userName the user name - */ - public void setUserName(String userName); - /** * @return factory the database is using to create LSHVector objects */ @@ -218,7 +207,7 @@ public interface FunctionDatabase extends AutoCloseable { * If the last query failed to produce a response, use this method to recover the error message * @return a String describing the error */ - public Error getLastError(); + public BSimError getLastError(); /** * Send a query to the database. The response is returned as a QueryResponseRecord. diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/AbstractSQLFunctionDatabase.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/AbstractSQLFunctionDatabase.java index a143d806d4..77a80624ff 100644 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/AbstractSQLFunctionDatabase.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/AbstractSQLFunctionDatabase.java @@ -4,9 +4,9 @@ * 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. @@ -92,7 +92,7 @@ public abstract class AbstractSQLFunctionDatabase protected final VF vectorFactory; // Factory used to generate LSHVector objects - private Error lasterror; + private BSimError lasterror; private Status status; private boolean isinit; @@ -1120,11 +1120,6 @@ public abstract class AbstractSQLFunctionDatabase return null; } - @Override - public void setUserName(String userName) { - // ignore - } - @Override public LSHVectorFactory getLSHVectorFactory() { return vectorFactory; @@ -1155,7 +1150,7 @@ public abstract class AbstractSQLFunctionDatabase } @Override - public Error getLastError() { + public BSimError getLastError() { return lasterror; } @@ -1188,7 +1183,7 @@ public abstract class AbstractSQLFunctionDatabase } catch (CancelledSQLException e) { status = Status.Error; - lasterror = new Error(ErrorCategory.AuthenticationCancelled, + lasterror = new BSimError(ErrorCategory.AuthenticationCancelled, "Authentication cancelled by user"); return false; } @@ -1201,19 +1196,19 @@ public abstract class AbstractSQLFunctionDatabase } String msg = cause.getMessage(); if (msg.contains("already in use:")) { - lasterror = new Error(ErrorCategory.Initialization, + lasterror = new BSimError(ErrorCategory.Initialization, "Database already in use by another process"); } else if (msg.contains("authentication failed") || msg.contains("requires a valid client certificate")) { lasterror = - new Error(ErrorCategory.Authentication, "Could not authenticate with database"); + new BSimError(ErrorCategory.Authentication, "Could not authenticate with database"); } else if (msg.contains("does not exist") && !msg.contains(" role ")) { - lasterror = new Error(ErrorCategory.Nodatabase, cause.getMessage()); + lasterror = new BSimError(ErrorCategory.Nodatabase, cause.getMessage()); } else { - lasterror = new Error(ErrorCategory.Initialization, + lasterror = new BSimError(ErrorCategory.Initialization, "Database error on initialization: " + cause.getMessage()); } return false; @@ -1628,29 +1623,29 @@ public abstract class AbstractSQLFunctionDatabase lasterror = null; try { if (!(query instanceof CreateDatabase) && !initialize()) { - lasterror = new Error(ErrorCategory.Nodatabase, "The database does not exist"); + lasterror = new BSimError(ErrorCategory.Nodatabase, "The database does not exist"); return null; } query.buildResponseTemplate(); QueryResponseRecord response = doQuery(query, db); if (response == null) { - lasterror = new Error(ErrorCategory.Fatal, "Unknown query type"); + lasterror = new BSimError(ErrorCategory.Fatal, "Unknown query type"); query.clearResponse(); } } catch (DatabaseNonFatalException err) { - lasterror = new Error(ErrorCategory.Nonfatal, + lasterror = new BSimError(ErrorCategory.Nonfatal, "Skipping -" + query.getName() + "- : " + err.getMessage()); query.clearResponse(); } catch (LSHException err) { - lasterror = new Error(ErrorCategory.Fatal, + lasterror = new BSimError(ErrorCategory.Fatal, "Fatal error during -" + query.getName() + "- : " + err.getMessage()); query.clearResponse(); } catch (SQLException err) { - lasterror = new Error(ErrorCategory.Fatal, + lasterror = new BSimError(ErrorCategory.Fatal, "SQL error during -" + query.getName() + "- : " + err.getMessage()); query.clearResponse(); } diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/FunctionDatabaseProxy.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/FunctionDatabaseProxy.java index 4f972bc1f6..39ba6e2869 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/FunctionDatabaseProxy.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/FunctionDatabaseProxy.java @@ -4,9 +4,9 @@ * 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. @@ -34,7 +34,7 @@ public class FunctionDatabaseProxy implements FunctionDatabase { private DatabaseInformation info; private LSHVectorFactory vectorFactory; private URL httpURL; - private Error lasterror; + private BSimError lasterror; private Status status; private boolean isinit; private XmlErrorHandler xmlErrorHandler; @@ -83,11 +83,6 @@ public class FunctionDatabaseProxy implements FunctionDatabase { return ClientUtil.getUserName(); } - @Override - public void setUserName(String userName) { - // Not currently implemented - } - @Override public LSHVectorFactory getLSHVectorFactory() { return vectorFactory; @@ -123,7 +118,8 @@ public class FunctionDatabaseProxy implements FunctionDatabase { } if (httpURL == null) { status = Status.Error; - lasterror = new FunctionDatabase.Error(ErrorCategory.Initialization, "MalformedURL"); + lasterror = + new FunctionDatabase.BSimError(ErrorCategory.Initialization, "MalformedURL"); return false; } QueryInfo queryInfo = new QueryInfo(); @@ -145,7 +141,7 @@ public class FunctionDatabaseProxy implements FunctionDatabase { } @Override - public Error getLastError() { + public BSimError getLastError() { return lasterror; } @@ -168,7 +164,8 @@ public class FunctionDatabaseProxy implements FunctionDatabase { ResponseError respError = new ResponseError(); respError.restoreXml(parser, vectorFactory); parser.dispose(); - lasterror = new FunctionDatabase.Error(ErrorCategory.Fatal, respError.errorMessage); + lasterror = + new FunctionDatabase.BSimError(ErrorCategory.Fatal, respError.errorMessage); query.clearResponse(); return null; } @@ -184,7 +181,7 @@ public class FunctionDatabaseProxy implements FunctionDatabase { return response; } catch (Exception ex) { - lasterror = new FunctionDatabase.Error(ErrorCategory.Connection, ex.getMessage()); + lasterror = new FunctionDatabase.BSimError(ErrorCategory.Connection, ex.getMessage()); status = Status.Error; query.clearResponse(); return null; diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/PostgresFunctionDatabase.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/PostgresFunctionDatabase.java index f8fd89c7ed..bb3bccc2df 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/PostgresFunctionDatabase.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/client/PostgresFunctionDatabase.java @@ -190,8 +190,9 @@ public final class PostgresFunctionDatabase @Override protected void generateRawDatabase() throws SQLException { BSimServerInfo serverInfo = postgresDs.getServerInfo(); - BSimServerInfo defaultServerInfo = new BSimServerInfo(DBType.postgres, - serverInfo.getServerName(), serverInfo.getPort(), DEFAULT_DATABASE_NAME); + BSimServerInfo defaultServerInfo = + new BSimServerInfo(DBType.postgres, serverInfo.getUserInfo(), + serverInfo.getServerName(), serverInfo.getPort(), DEFAULT_DATABASE_NAME); String createdbstring = "CREATE DATABASE \"" + serverInfo.getDBName() + '"'; BSimPostgresDataSource defaultDs = BSimPostgresDBConnectionManager.getDataSource(defaultServerInfo); @@ -502,14 +503,6 @@ public final class PostgresFunctionDatabase return postgresDs.getUserName(); } - @Override - public void setUserName(String userName) { - if (postgresDs.getStatus() == Status.Ready) { - throw new IllegalStateException("Connection has already been established"); - } - postgresDs.setPreferredUserName(userName); - } - @Override public QueryResponseRecord doQuery(BSimQuery query, Connection c) throws SQLException, LSHException, DatabaseNonFatalException { diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/elastic/ElasticConnection.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/elastic/ElasticConnection.java index 67fff9e3e4..473118dc88 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/elastic/ElasticConnection.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/elastic/ElasticConnection.java @@ -4,9 +4,9 @@ * 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. @@ -31,8 +31,6 @@ public class ElasticConnection { protected String hostURL; // http://hostname:port protected String httpURLbase; // Main URL to elasticsearch - private HttpURLConnection connection = null; - private Writer writer; private int lastResponseCode; public ElasticConnection(String url, String repo) { @@ -41,61 +39,13 @@ public class ElasticConnection { } public void close() { - if (connection != null) { - connection.disconnect(); - } + // nothing to do - http connections do not persist } public boolean lastRequestSuccessful() { return (lastResponseCode >= 200) && (lastResponseCode < 300); } - /** - * Start a new request to the elastic server. This establishes the OutputStream for writing the body of the request - * @param command is the type of command - * @param path is the overarching index/type/ path - * @throws IOException for problems with the socket - */ - public void startHttpRequest(String command, String path) throws IOException { - URL httpURL = new URL(httpURLbase + path); - connection = (HttpURLConnection) httpURL.openConnection(); - connection.setRequestMethod(command); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setDoOutput(true); - writer = new OutputStreamWriter(connection.getOutputStream()); - } - - public void startHttpBulkRequest(String bulkCommand) throws IOException { - URL httpURL = new URL(hostURL + bulkCommand); - connection = (HttpURLConnection) httpURL.openConnection(); - connection.setRequestMethod(POST); - connection.setRequestProperty("Content-Type", "application/x-ndjson"); - connection.setDoOutput(true); - writer = new OutputStreamWriter(connection.getOutputStream()); - } - - public void startHttpRawRequest(String command, String path) throws IOException { - URL httpURL = new URL(hostURL + path); - connection = (HttpURLConnection) httpURL.openConnection(); - connection.setRequestMethod(command); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setDoOutput(true); - writer = new OutputStreamWriter(connection.getOutputStream()); - } - - /** - * Start a request with no input body, URI only - * @param command is the command to issue - * @param path is the overarching request path: index/... - * @throws IOException for problems with the socket - */ - public void startHttpURICommand(String command, String path) throws IOException { - URL httpURL = new URL(httpURLbase + path); - connection = (HttpURLConnection) httpURL.openConnection(); - connection.setRequestMethod(command); - connection.setDoOutput(true); - } - /** * Assuming the writer has been closed and connection.getResponseCode() is called * placing the value in lastResponseCode, read the response and parse into a JSONObject @@ -103,15 +53,21 @@ public class ElasticConnection { * @throws IOException for problems with the socket * @throws ParseException for JSON parse errors */ - private JSONObject grabResponse() throws IOException, ParseException { + private JSONObject grabResponse(HttpURLConnection connection) + throws IOException, ParseException { JSONParser parser = new JSONParser(); - Reader reader; + InputStream in; if (lastRequestSuccessful()) { - reader = new InputStreamReader(connection.getInputStream()); + in = connection.getInputStream(); } else { - reader = new InputStreamReader(connection.getErrorStream()); + in = connection.getErrorStream(); } + if (in == null) { + // Connection error occurred + throw new IOException(connection.getResponseMessage()); + } + Reader reader = new InputStreamReader(in); JSONObject jsonObject = (JSONObject) parser.parse(reader); return jsonObject; } @@ -156,12 +112,18 @@ public class ElasticConnection { */ public JSONObject executeRawStatement(String command, String path, String body) throws ElasticException { + HttpURLConnection connection = null; try { - startHttpRawRequest(command, path); - writer.write(body); - writer.close(); + URL httpURL = new URL(hostURL + path); + connection = (HttpURLConnection) httpURL.openConnection(); + connection.setRequestMethod(command); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + try (Writer writer = new OutputStreamWriter(connection.getOutputStream())) { + writer.write(body); + } lastResponseCode = connection.getResponseCode(); - JSONObject resp = grabResponse(); + JSONObject resp = grabResponse(connection); if (!lastRequestSuccessful()) { throw new ElasticException(parseErrorJSON(resp)); } @@ -173,6 +135,11 @@ public class ElasticConnection { catch (ParseException e) { throw new ElasticException("Error parsing response: " + e.getMessage()); } + finally { + if (connection != null) { + connection.disconnect(); + } + } } @@ -185,12 +152,18 @@ public class ElasticConnection { */ public void executeStatementNoResponse(String command, String path, String body) throws ElasticException { + HttpURLConnection connection = null; try { - startHttpRequest(command, path); - writer.write(body); - writer.close(); + URL httpURL = new URL(httpURLbase + path); + connection = (HttpURLConnection) httpURL.openConnection(); + connection.setRequestMethod(command); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + try (Writer writer = new OutputStreamWriter(connection.getOutputStream())) { + writer.write(body); + } lastResponseCode = connection.getResponseCode(); - JSONObject resp = grabResponse(); + JSONObject resp = grabResponse(connection); if (!lastRequestSuccessful()) { throw new ElasticException(parseErrorJSON(resp)); } @@ -201,6 +174,11 @@ public class ElasticConnection { catch (ParseException e) { throw new ElasticException("Error parsing response: " + e.getMessage()); } + finally { + if (connection != null) { + connection.disconnect(); + } + } } /** @@ -213,12 +191,18 @@ public class ElasticConnection { */ public JSONObject executeStatement(String command, String path, String body) throws ElasticException { + HttpURLConnection connection = null; try { - startHttpRequest(command, path); - writer.write(body); - writer.close(); + URL httpURL = new URL(httpURLbase + path); + connection = (HttpURLConnection) httpURL.openConnection(); + connection.setRequestMethod(command); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + try (Writer writer = new OutputStreamWriter(connection.getOutputStream())) { + writer.write(body); + } lastResponseCode = connection.getResponseCode(); - JSONObject resp = grabResponse(); + JSONObject resp = grabResponse(connection); if (!lastRequestSuccessful()) { throw new ElasticException(parseErrorJSON(resp)); } @@ -230,6 +214,11 @@ public class ElasticConnection { catch (ParseException e) { throw new ElasticException("Error parsing response: " + e.getMessage()); } + finally { + if (connection != null) { + connection.disconnect(); + } + } } /** @@ -243,12 +232,18 @@ public class ElasticConnection { */ public JSONObject executeStatementExpectFailure(String command, String path, String body) throws ElasticException { + HttpURLConnection connection = null; try { - startHttpRequest(command, path); - writer.write(body); - writer.close(); + URL httpURL = new URL(httpURLbase + path); + connection = (HttpURLConnection) httpURL.openConnection(); + connection.setRequestMethod(command); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + try (Writer writer = new OutputStreamWriter(connection.getOutputStream())) { + writer.write(body); + } lastResponseCode = connection.getResponseCode(); - JSONObject resp = grabResponse(); + JSONObject resp = grabResponse(connection); return resp; } catch (IOException e) { @@ -257,6 +252,11 @@ public class ElasticConnection { catch (ParseException e) { throw new ElasticException("Error parsing response: " + e.getMessage()); } + finally { + if (connection != null) { + connection.disconnect(); + } + } } /** @@ -268,12 +268,18 @@ public class ElasticConnection { * @throws ElasticException for any problems with the connection */ public JSONObject executeBulk(String path, String body) throws ElasticException { + HttpURLConnection connection = null; try { - startHttpBulkRequest(path); - writer.write(body); - writer.close(); + URL httpURL = new URL(hostURL + path); + connection = (HttpURLConnection) httpURL.openConnection(); + connection.setRequestMethod(POST); + connection.setRequestProperty("Content-Type", "application/x-ndjson"); + connection.setDoOutput(true); + try (Writer writer = new OutputStreamWriter(connection.getOutputStream())) { + writer.write(body); + } lastResponseCode = connection.getResponseCode(); - JSONObject resp = grabResponse(); + JSONObject resp = grabResponse(connection); if (!lastRequestSuccessful()) { throw new ElasticException(parseErrorJSON(resp)); } @@ -285,13 +291,22 @@ public class ElasticConnection { catch (ParseException e) { throw new ElasticException("Error parsing response: " + e.getMessage()); } + finally { + if (connection != null) { + connection.disconnect(); + } + } } public JSONObject executeURIOnly(String command, String path) throws ElasticException { + HttpURLConnection connection = null; try { - startHttpURICommand(command, path); + URL httpURL = new URL(httpURLbase + path); + connection = (HttpURLConnection) httpURL.openConnection(); + connection.setRequestMethod(command); + connection.setDoOutput(true); lastResponseCode = connection.getResponseCode(); - JSONObject resp = grabResponse(); + JSONObject resp = grabResponse(connection); if (!lastRequestSuccessful()) { throw new ElasticException(parseErrorJSON(resp)); } @@ -303,5 +318,10 @@ public class ElasticConnection { catch (ParseException e) { throw new ElasticException("Error parsing response: " + e.getMessage()); } + finally { + if (connection != null) { + connection.disconnect(); + } + } } } diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/elastic/ElasticDatabase.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/elastic/ElasticDatabase.java index 2d49fb8fb1..5a44869994 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/elastic/ElasticDatabase.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/elastic/ElasticDatabase.java @@ -4,9 +4,9 @@ * 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. @@ -60,14 +60,13 @@ public class ElasticDatabase implements FunctionDatabase { public static final int MAX_VECTOR_BULK = 200; // Maximum vectors ingested in one bulk request private ElasticConnection connection; // Low-level connection to the database - private String userName = null; // User name for server authentication private ConnectionType connectionType = ConnectionType.Unencrypted_No_Authentication; private DatabaseInformation info; // Information about the active database private Base64VectorFactory vectorFactory; // factory used to create BSim feature vectors private final BSimServerInfo serverInfo; // NOTE: does not reflect the use of http vs https private final String baseURL; // Base URL for connecting to elasticsearch, i.e. http://hostname:9200 private final String repository; // Name of the repository, prefix to all elasticsearch indices - private Error lastError; // Info on error caused by last action taken on this interface (null if no error) + private BSimError lastError; // Info on error caused by last action taken on this interface (null if no error) private Status status; // status of the connection private boolean initialized; // true if the connection has been successfully initialized @@ -2022,8 +2021,8 @@ public class ElasticDatabase implements FunctionDatabase { throw new MalformedURLException("URL path must indicate the repository only"); } repository = path.substring(1); - this.serverInfo = - new BSimServerInfo(DBType.elastic, baseURL.getHost(), baseURL.getPort(), repository); + this.serverInfo = new BSimServerInfo(DBType.elastic, null, baseURL.getHost(), + baseURL.getPort(), repository); this.baseURL = fullURL.substring(0, fullURL.length() - path.length()); lastError = null; @@ -2398,15 +2397,7 @@ public class ElasticDatabase implements FunctionDatabase { @Override public String getUserName() { - if (userName != null) { - return userName; - } - return ClientUtil.getUserName(); - } - - @Override - public void setUserName(String userName) { - this.userName = userName; + return serverInfo.getUserName(); } @Override @@ -2450,14 +2441,14 @@ public class ElasticDatabase implements FunctionDatabase { vectorFactory.set(config.weightfactory, config.idflookup, config.info.settings); } catch (ElasticException err) { - lastError = new Error(ErrorCategory.Initialization, + lastError = new BSimError(ErrorCategory.Initialization, "Database error on initialization: " + err.getMessage()); status = Status.Error; return false; } catch (NoDatabaseException err) { info = null; - lastError = new Error(ErrorCategory.Nodatabase, + lastError = new BSimError(ErrorCategory.Nodatabase, "Database has not been created yet: " + err.getMessage()); initialized = true; status = Status.Ready; @@ -2480,14 +2471,14 @@ public class ElasticDatabase implements FunctionDatabase { } @Override - public Error getLastError() { + public BSimError getLastError() { return lastError; } @Override public QueryResponseRecord query(BSimQuery query) { if ((!isInitialized()) && (!(query instanceof CreateDatabase))) { - lastError = new Error(ErrorCategory.Nodatabase, "The database does not exist"); + lastError = new BSimError(ErrorCategory.Nodatabase, "The database does not exist"); return null; } lastError = null; @@ -2554,22 +2545,22 @@ public class ElasticDatabase implements FunctionDatabase { fdbPasswordChange((PasswordChange) query); } else { - lastError = new Error(ErrorCategory.Fatal, "Unknown query type"); + lastError = new BSimError(ErrorCategory.Fatal, "Unknown query type"); query.clearResponse(); } } catch (DatabaseNonFatalException err) { - lastError = new Error(ErrorCategory.Nonfatal, + lastError = new BSimError(ErrorCategory.Nonfatal, "Skipping -" + query.getName() + "- : " + err.getMessage()); query.clearResponse(); } catch (LSHException err) { - lastError = new Error(ErrorCategory.Fatal, + lastError = new BSimError(ErrorCategory.Fatal, "Fatal error during -" + query.getName() + "- : " + err.getMessage()); query.clearResponse(); } catch (ElasticException err) { - lastError = new Error(ErrorCategory.Fatal, + lastError = new BSimError(ErrorCategory.Fatal, "Elastic error during -" + query.getName() + "- : " + err.getMessage()); query.clearResponse(); } diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/facade/SimilarFunctionQueryService.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/facade/SimilarFunctionQueryService.java index 062b7ee460..ecd975bf6d 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/facade/SimilarFunctionQueryService.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/facade/SimilarFunctionQueryService.java @@ -4,9 +4,9 @@ * 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. @@ -384,7 +384,7 @@ public class SimilarFunctionQueryService implements AutoCloseable { return database.getLSHVectorFactory(); } - public FunctionDatabase.Error getLastError() { + public FunctionDatabase.BSimError getLastError() { if (database == null) { return null; } diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BSimLaunchable.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BSimLaunchable.java index 73cfd81d0a..6cebdb57ff 100644 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BSimLaunchable.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BSimLaunchable.java @@ -4,9 +4,9 @@ * 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. @@ -17,7 +17,8 @@ package ghidra.features.bsim.query.ingest; import java.io.File; import java.io.IOException; -import java.net.*; +import java.net.MalformedURLException; +import java.net.URL; import java.util.*; import org.apache.commons.lang3.StringUtils; @@ -32,10 +33,8 @@ import ghidra.features.bsim.query.protocol.QueryName; import ghidra.framework.*; import ghidra.framework.client.ClientUtil; import ghidra.framework.client.HeadlessClientAuthenticator; -import ghidra.framework.data.DomainObjectAdapter; import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.net.SSLContextInitializer; -import ghidra.program.database.ProgramDB; import ghidra.util.Msg; import ghidra.util.SystemUtilities; import ghidra.util.exception.CancelledException; @@ -335,10 +334,6 @@ public class BSimLaunchable implements GhidraLaunchable { optionValueMap.put(option, params[i]); } } - String connectingUserName = optionValueMap.get(USER_OPTION); - if (connectingUserName == null) { - connectingUserName = optionValueMap.put(USER_OPTION, ClientUtil.getUserName()); - } return subParams; } @@ -981,9 +976,9 @@ public class BSimLaunchable implements GhidraLaunchable { " - large_32 | medium_32 | medium_64 | medium_cpool | medium_nosize \n" + "\n" + "BSim URL Forms (bsimURL):\n" + - " postgresql://[:]/\n" + - " elastic://[:]/\n" + - " https://[:]/\n" + + " postgresql://[username@][:]/\n" + + " elastic://[username@][:]/\n" + + " https://[username@][:]/\n" + " file:/[/]\n" + "\n" + "Ghidra URL Forms (ghidraURL):\n" + @@ -1010,7 +1005,13 @@ public class BSimLaunchable implements GhidraLaunchable { run(params); } catch (MalformedURLException e) { - Msg.error(this, "Invalid URL specified: " + e.getMessage()); + String msg = e.getMessage(); + if (msg == null) { + e.printStackTrace(); + } + else { + Msg.error(this, "Invalid URL specified: " + msg); + } System.exit(22); // EINVAL } catch (IllegalArgumentException e) { @@ -1019,7 +1020,13 @@ public class BSimLaunchable implements GhidraLaunchable { System.exit(22); // EINVAL } catch (Exception e) { - Msg.error(this, e.getMessage()); + String msg = e.getMessage(); + if (msg == null) { + e.printStackTrace(); + } + else { + Msg.error(this, msg); + } System.exit(1); // Misc Error } } @@ -1059,7 +1066,7 @@ public class BSimLaunchable implements GhidraLaunchable { // Use BSim log config to ensure we get desired console output System.setProperty(LoggingInitialization.LOG4J2_CONFIGURATION_PROPERTY, - BSIM_LOGGING_CONFIGURATION_FILE); + BSIM_LOGGING_CONFIGURATION_FILE); ApplicationConfiguration config; switch (type) { @@ -1084,6 +1091,10 @@ public class BSimLaunchable implements GhidraLaunchable { ghidra.framework.protocol.ghidra.Handler.registerHandler(); ghidra.features.bsim.query.postgresql.Handler.registerHandler(); + if (connectingUserName == null) { + // Force default login name + connectingUserName = ClientUtil.getUserName(); + } HeadlessClientAuthenticator.installHeadlessClientAuthenticator(connectingUserName, certPath, true); } diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BulkSignatures.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BulkSignatures.java index 586760eff6..76e2ae4179 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BulkSignatures.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/BulkSignatures.java @@ -4,9 +4,9 @@ * 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. @@ -16,7 +16,6 @@ package ghidra.features.bsim.query.ingest; import java.io.*; -import java.net.MalformedURLException; import java.net.URL; import java.util.*; @@ -29,9 +28,8 @@ import org.xml.sax.SAXException; import generic.lsh.vector.LSHVectorFactory; import ghidra.app.decompiler.DecompileException; import ghidra.features.bsim.query.*; -import ghidra.features.bsim.query.FunctionDatabase.Error; +import ghidra.features.bsim.query.FunctionDatabase.BSimError; import ghidra.features.bsim.query.FunctionDatabase.ErrorCategory; -import ghidra.features.bsim.query.FunctionDatabase.Status; import ghidra.features.bsim.query.client.Configuration; import ghidra.features.bsim.query.client.tables.ExeTable.ExeTableOrderColumn; import ghidra.features.bsim.query.description.*; @@ -52,7 +50,6 @@ public class BulkSignatures implements AutoCloseable { // FIXME: May need to use Msg.showError for popup messages in GUI workbench case private final BSimServerInfo bsimServerInfo; // may be null - private final String connectingUserName; private FunctionDatabase querydb; @@ -61,14 +58,36 @@ public class BulkSignatures implements AutoCloseable { * @param bsimServerInfo the BSim database server info. May be {@code null} if use limited to * signature and update generation only (based upon configuration template). * @param connectingUserName user name to use for BSim server authentication. May be null if - * not required or default should be used (see {@link ClientUtil#getUserName()}). - * @throws MalformedURLException if the given URL string cannot be parsed + * not required or default should be used (see {@link ClientUtil#getUserName()}). If specified + * a new {@link BSimServerInfo} instance will be created with the user information set. This + * argument is ignored if DB user specified by {@code bsimServerInfo}. */ - public BulkSignatures(BSimServerInfo bsimServerInfo, String connectingUserName) - throws MalformedURLException { + public BulkSignatures(BSimServerInfo bsimServerInfo, String connectingUserName) { + if (bsimServerInfo != null && !StringUtils.isBlank(connectingUserName)) { + if (!bsimServerInfo.hasDefaultLogin()) { + String username = bsimServerInfo.getUserName(); + if (!username.equals(connectingUserName)) { + Msg.warn(this, "BSim DB server info specifies user '" + username + + "'. Ignoring user name option: '" + connectingUserName + "'"); + } + } + else { + bsimServerInfo = new BSimServerInfo(bsimServerInfo.getDBType(), connectingUserName, + bsimServerInfo.getServerName(), bsimServerInfo.getPort(), + bsimServerInfo.getDBName()); + } + } + this.bsimServerInfo = bsimServerInfo; + } + + /** + * Constructor + * @param bsimServerInfo the BSim database server info. May be {@code null} if use limited to + * signature and update generation only (based upon configuration template). If specified, + * this object will convey the connecting user name. + */ + public BulkSignatures(BSimServerInfo bsimServerInfo) { this.bsimServerInfo = bsimServerInfo; - this.connectingUserName = - connectingUserName != null ? connectingUserName : ClientUtil.getUserName(); } private void checkBSimServerOperation() { @@ -93,9 +112,6 @@ public class BulkSignatures implements AutoCloseable { checkBSimServerOperation(); querydb = BSimClientFactory.buildClient(bsimServerInfo, async); - if (querydb.getStatus() == Status.Unconnected) { // may have previously connected - querydb.setUserName(connectingUserName); - } if (!querydb.initialize()) { throw new IOException(querydb.getLastError().message); @@ -103,7 +119,7 @@ public class BulkSignatures implements AutoCloseable { DatabaseInformation info = querydb.getInfo(); if (info == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); if (lastError != null && lastError.category == ErrorCategory.Nodatabase) { throw new IOException(lastError.message); } @@ -190,7 +206,7 @@ public class BulkSignatures implements AutoCloseable { continue; } if (insertreq.execute(querydb) == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); if ((lastError.category == ErrorCategory.Format) || (lastError.category == ErrorCategory.Nonfatal)) { Msg.warn(this, file.getName() + ": " + lastError.message); @@ -216,7 +232,7 @@ public class BulkSignatures implements AutoCloseable { loadSignatureXml(file, update.manage); ResponseUpdate respup = update.execute(querydb); if (respup == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); if ((lastError.category == ErrorCategory.Format) || (lastError.category == ErrorCategory.Nonfatal)) { Msg.warn(this, file.getName() + ": " + lastError.message); @@ -400,9 +416,6 @@ public class BulkSignatures implements AutoCloseable { checkBSimServerOperation(); querydb = BSimClientFactory.buildClient(bsimServerInfo, true); - if (querydb.getStatus() == Status.Unconnected) { // may have previously connected - querydb.setUserName(connectingUserName); - } // TODO: Should this output differ for command-line vs workbench? debug only? try { @@ -533,7 +546,7 @@ public class BulkSignatures implements AutoCloseable { establishQueryServerConnection(true); ResponseDelete respdel = query.execute(querydb); if (respdel == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); throw new LSHException("Could not perform delete: " + lastError.message); } @@ -561,7 +574,7 @@ public class BulkSignatures implements AutoCloseable { query.doRebuild = false; ResponseAdjustIndex response = query.execute(querydb); if (response == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); throw new LSHException("Could not drop index: " + lastError.message); } String dbDetail = "for database " + info.databasename + " (" + bsimServerInfo + ")"; @@ -590,7 +603,7 @@ public class BulkSignatures implements AutoCloseable { System.out.println("Starting rebuild ..."); ResponseAdjustIndex response = query.execute(querydb); if (response == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); throw new LSHException("Could not rebuild index: " + lastError.message); } String dbDetail = "for database " + info.databasename + " (" + bsimServerInfo + ")"; @@ -617,7 +630,7 @@ public class BulkSignatures implements AutoCloseable { PrewarmRequest request = new PrewarmRequest(); ResponsePrewarm response = request.execute(querydb); if (response == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); throw new LSHException("Prewarm failed: " + lastError.message); } String dbDetail = "for database " + info.databasename + " (" + bsimServerInfo + ")"; @@ -662,7 +675,7 @@ public class BulkSignatures implements AutoCloseable { ResponseExe response = exeQuery.execute(querydb); if (response == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); throw new LSHException("Could not perform getexeinfo: " + lastError.message); } @@ -735,7 +748,7 @@ public class BulkSignatures implements AutoCloseable { req.description = description; ResponseInfo resp = req.execute(querydb); if (resp == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); throw new LSHException("Could not change metadata: " + lastError.message); } info = resp.info; @@ -764,7 +777,7 @@ public class BulkSignatures implements AutoCloseable { ResponseInfo resp = req.execute(querydb); if (resp == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); throw new LSHException("Could not install new category: " + lastError.message); } info = resp.info; @@ -798,7 +811,7 @@ public class BulkSignatures implements AutoCloseable { req.tag_name = dequoteString(tagName); ResponseInfo resp = req.execute(querydb); if (resp == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); throw new LSHException(lastError.message); } info = resp.info; @@ -859,7 +872,7 @@ public class BulkSignatures implements AutoCloseable { while (count != 0) { ResponsePair responsePair = query.execute(querydb); if (responsePair == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); throw new LSHException(lastError.message); } for (PairNote note : responsePair.notes) { @@ -893,7 +906,7 @@ public class BulkSignatures implements AutoCloseable { establishQueryServerConnection(true); ResponseName resp = query.execute(querydb); if (resp == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); throw new LSHException(lastError.message); } resp.printRaw(outStream, querydb.getLSHVectorFactory(), 0); @@ -941,7 +954,7 @@ public class BulkSignatures implements AutoCloseable { query.fillinCallgraph = info.trackcallgraph; ResponseName responseName = query.execute(querydb); if (responseName == null) { - Error lastError = querydb.getLastError(); + BSimError lastError = querydb.getLastError(); throw new LSHException(lastError.message); } if (!responseName.uniqueexecutable) { diff --git a/Ghidra/Features/BSim/src/screen/java/help/screenshot/BSimSearchPluginScreenShots.java b/Ghidra/Features/BSim/src/screen/java/help/screenshot/BSimSearchPluginScreenShots.java index c18470ef4a..82b4c616b5 100755 --- a/Ghidra/Features/BSim/src/screen/java/help/screenshot/BSimSearchPluginScreenShots.java +++ b/Ghidra/Features/BSim/src/screen/java/help/screenshot/BSimSearchPluginScreenShots.java @@ -20,8 +20,10 @@ import java.util.*; import org.junit.Before; import org.junit.Test; +import docking.DialogComponentProvider; import docking.DockingWindowManager; import docking.action.DockingActionIf; +import docking.widgets.textfield.GFormattedTextField; import ghidra.app.services.ProgramManager; import ghidra.features.bsim.gui.*; import ghidra.features.bsim.gui.overview.BSimOverviewProvider; @@ -90,9 +92,11 @@ public class BSimSearchPluginScreenShots extends GhidraScreenShotGenerator { @Test public void testManageServersDialog() { - addTestServer(new BSimServerInfo(DBType.postgres, "100.50.123.5", 123, "testDB")); - addTestServer(new BSimServerInfo(DBType.postgres, "100.50.123.5", 134, "anotherDB")); - addTestServer(new BSimServerInfo(DBType.file, "100.50.123.5", 134, "/bsim/database1")); + addTestServer( + new BSimServerInfo(DBType.postgres, "mylogin", "100.50.123.5", 123, "testDB")); + addTestServer( + new BSimServerInfo(DBType.postgres, "mylogin", "100.50.123.5", 134, "anotherDB")); + addTestServer(new BSimServerInfo("/bsim/database1")); DockingActionIf action = getAction(plugin, "Manage BSim Servers"); performAction(action, false); @@ -106,7 +110,11 @@ public class BSimSearchPluginScreenShots extends GhidraScreenShotGenerator { public void testAddServerDialog() { CreateBsimServerInfoDialog dialog = new CreateBsimServerInfoDialog(); runSwingLater(() -> DockingWindowManager.showDialog(dialog)); - waitForSwing(); + DialogComponentProvider entryDialog = waitForDialogComponent("Add BSim Server"); + GFormattedTextField userField = + (GFormattedTextField) findComponentByName(entryDialog, "User"); + userField.setText("mylogin"); + userField.setDefaultValue("mylogin"); captureDialog(dialog); dialog.close(); } diff --git a/Ghidra/Features/BSim/src/test.slow/java/ghidra/features/bsim/gui/BSimSearchPluginTest.java b/Ghidra/Features/BSim/src/test.slow/java/ghidra/features/bsim/gui/BSimSearchPluginTest.java index 4523a79ff1..3f40419dcd 100755 --- a/Ghidra/Features/BSim/src/test.slow/java/ghidra/features/bsim/gui/BSimSearchPluginTest.java +++ b/Ghidra/Features/BSim/src/test.slow/java/ghidra/features/bsim/gui/BSimSearchPluginTest.java @@ -4,9 +4,9 @@ * 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. @@ -178,7 +178,7 @@ public class BSimSearchPluginTest extends AbstractBSimPluginTest { private FunctionDatabase database; public TestBSimServerInfo(FunctionDatabase database) { - super(DBType.postgres, "0.0.0.0", 123, "testDB"); + super(DBType.postgres, null, "0.0.0.0", 123, "testDB"); this.database = database; } diff --git a/Ghidra/Features/BSim/src/test.slow/java/ghidra/features/bsim/query/file/BSimH2DatabaseManagerTest.java b/Ghidra/Features/BSim/src/test.slow/java/ghidra/features/bsim/query/file/BSimH2DatabaseManagerTest.java index f3dd96c173..32aa009ca5 100644 --- a/Ghidra/Features/BSim/src/test.slow/java/ghidra/features/bsim/query/file/BSimH2DatabaseManagerTest.java +++ b/Ghidra/Features/BSim/src/test.slow/java/ghidra/features/bsim/query/file/BSimH2DatabaseManagerTest.java @@ -24,7 +24,7 @@ import org.junit.*; import ghidra.features.bsim.query.*; import ghidra.features.bsim.query.BSimServerInfo.DBType; -import ghidra.features.bsim.query.FunctionDatabase.Error; +import ghidra.features.bsim.query.FunctionDatabase.BSimError; import ghidra.features.bsim.query.description.DatabaseInformation; import ghidra.features.bsim.query.file.BSimH2FileDBConnectionManager.BSimH2FileDataSource; import ghidra.features.bsim.query.protocol.CreateDatabase; @@ -68,7 +68,7 @@ public class BSimH2DatabaseManagerTest extends AbstractGhidraHeadedIntegrationTe } private BSimServerInfo getBsimServerInfo(String name) { - return new BSimServerInfo(DBType.file, null, -1, getDbName(name)); + return new BSimServerInfo(getDbName(name)); } private BSimServerInfo createDatabase(String databaseName) { @@ -103,7 +103,7 @@ public class BSimH2DatabaseManagerTest extends AbstractGhidraHeadedIntegrationTe ResponseInfo response = command.execute(h2Database); if (response == null) { if (expectedError != null) { - Error lastError = h2Database.getLastError(); + BSimError lastError = h2Database.getLastError(); assertNotNull(lastError); assertTrue(lastError.message.contains(expectedError)); } @@ -186,7 +186,7 @@ public class BSimH2DatabaseManagerTest extends AbstractGhidraHeadedIntegrationTe BSimServerInfo serverInfo = getBsimServerInfo("test"); try (FunctionDatabase fdb = serverInfo.getFunctionDatabase(false)) { assertFalse(fdb.initialize()); - Error lastError = fdb.getLastError(); + BSimError lastError = fdb.getLastError(); assertNotNull(lastError); assertTrue(lastError.message.startsWith("Database does not exist: ")); } diff --git a/Ghidra/Features/BSim/src/test.slow/java/ghidra/features/bsim/query/test/BSimServerTest.java b/Ghidra/Features/BSim/src/test.slow/java/ghidra/features/bsim/query/test/BSimServerTest.java index 07154c507b..8408f218b5 100755 --- a/Ghidra/Features/BSim/src/test.slow/java/ghidra/features/bsim/query/test/BSimServerTest.java +++ b/Ghidra/Features/BSim/src/test.slow/java/ghidra/features/bsim/query/test/BSimServerTest.java @@ -35,7 +35,7 @@ import ghidra.app.util.headless.HeadlessOptions; import ghidra.features.bsim.gui.filters.ExecutableCategoryBSimFilterType; import ghidra.features.bsim.gui.filters.HasNamedChildBSimFilterType; import ghidra.features.bsim.query.*; -import ghidra.features.bsim.query.FunctionDatabase.Error; +import ghidra.features.bsim.query.FunctionDatabase.BSimError; import ghidra.features.bsim.query.client.tables.ExeTable.ExeTableOrderColumn; import ghidra.features.bsim.query.description.*; import ghidra.features.bsim.query.ingest.BSimLaunchable; @@ -264,7 +264,7 @@ public class BSimServerTest { private static void testForError(QueryResponseRecord response) throws LSHException { if (response == null) { - Error lastError = client.getLastError(); + BSimError lastError = client.getLastError(); if (lastError == null) { throw new LSHException("Unknown error"); } diff --git a/Ghidra/Features/BSim/src/test/java/ghidra/features/bsim/query/facade/FunctionDatabaseTestDouble.java b/Ghidra/Features/BSim/src/test/java/ghidra/features/bsim/query/facade/FunctionDatabaseTestDouble.java index 760beba768..05e85b863d 100755 --- a/Ghidra/Features/BSim/src/test/java/ghidra/features/bsim/query/facade/FunctionDatabaseTestDouble.java +++ b/Ghidra/Features/BSim/src/test/java/ghidra/features/bsim/query/facade/FunctionDatabaseTestDouble.java @@ -4,9 +4,9 @@ * 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. @@ -106,11 +106,6 @@ public class FunctionDatabaseTestDouble implements SQLFunctionDatabase { return ClientUtil.getUserName(); } - @Override - public void setUserName(String userName) { - // Currently not implemented - } - @Override public String getURLString() { return urlString; @@ -141,8 +136,8 @@ public class FunctionDatabaseTestDouble implements SQLFunctionDatabase { } @Override - public Error getLastError() { - return new Error(ErrorCategory.Unused, errorString); + public BSimError getLastError() { + return new BSimError(ErrorCategory.Unused, errorString); } void setErrorString(String errorString) { diff --git a/Ghidra/Features/BSim/src/test/java/ghidra/features/bsim/query/facade/TestBSimServerInfo.java b/Ghidra/Features/BSim/src/test/java/ghidra/features/bsim/query/facade/TestBSimServerInfo.java index d95cf4bd85..92091e4715 100644 --- a/Ghidra/Features/BSim/src/test/java/ghidra/features/bsim/query/facade/TestBSimServerInfo.java +++ b/Ghidra/Features/BSim/src/test/java/ghidra/features/bsim/query/facade/TestBSimServerInfo.java @@ -4,9 +4,9 @@ * 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. @@ -23,7 +23,7 @@ public class TestBSimServerInfo extends BSimServerInfo { private FunctionDatabase database; public TestBSimServerInfo(FunctionDatabase database) { - super(DBType.postgres, "100.50.123.5", 123, "testDB"); + super(DBType.postgres, null, "100.50.123.5", 123, "testDB"); this.database = database; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/PasswordDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PasswordDialog.java index 8a84b609cd..43d06e5cc7 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/PasswordDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/PasswordDialog.java @@ -4,9 +4,9 @@ * 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. @@ -38,7 +38,8 @@ public class PasswordDialog extends DialogComponentProvider { private JPasswordField passwordField; private JComboBox choiceCB; private JCheckBox anonymousAccess; - boolean okPressed = false; + private boolean okPressed = false; + private String defaultUserID; /** * Construct a new PasswordDialog. @@ -47,7 +48,7 @@ public class PasswordDialog extends DialogComponentProvider { * @param serverName name of server or keystore pathname * @param passPrompt password prompt to show in the dialog; may be null, in which case * "Password:" is displayed next to the password field - * @param namePrompt name prompt to show in the dialog, if null a name will not be prompted for. + * @param userIdPrompt User ID / Name prompt to show in the dialog, if null a name will not be prompted for. * @param defaultUserID default name when prompting for a name * @param choicePrompt namePrompt name prompt to show in the dialog, if null a name will not be prompted for. * @param choices array of choices to present if choicePrompt is not null @@ -55,9 +56,9 @@ public class PasswordDialog extends DialogComponentProvider { * @param includeAnonymousOption true signals to add a checkbox to request anonymous login */ public PasswordDialog(String title, String serverType, String serverName, String passPrompt, - String namePrompt, String defaultUserID, String choicePrompt, String[] choices, + String userIdPrompt, String defaultUserID, String choicePrompt, String[] choices, int defaultChoice, boolean includeAnonymousOption) { - this(title, serverType, serverName, passPrompt, namePrompt, defaultUserID); + this(title, serverType, serverName, passPrompt, userIdPrompt, defaultUserID); if (choicePrompt != null) { workPanel.add(new GLabel(choicePrompt)); choiceCB = new GComboBox<>(choices); @@ -94,12 +95,12 @@ public class PasswordDialog extends DialogComponentProvider { * @param serverName name of server or keystore pathname * @param passPrompt password prompt to show in the dialog; may be null, in which case * "Password:" is displayed next to the password field - * @param namePrompt name prompt to show in the dialog, if null a name will not be prompted for. + * @param userIdPrompt User ID / Name prompt to show in the dialog, if null a name will not be prompted for. * @param defaultUserID default name when prompting for a name */ public PasswordDialog(String title, String serverType, String serverName, String passPrompt, - String namePrompt, String defaultUserID) { - this(title, serverType, serverName, passPrompt, namePrompt, defaultUserID, true); + String userIdPrompt, String defaultUserID) { + this(title, serverType, serverName, passPrompt, userIdPrompt, defaultUserID, true); } /** @@ -109,14 +110,17 @@ public class PasswordDialog extends DialogComponentProvider { * @param serverName name of server or keystore pathname * @param passPrompt password prompt to show in the dialog; may be null, in which case * "Password:" is displayed next to the password field - * @param namePrompt name prompt to show in the dialog, if null a name will not be prompted for. + * @param userIdPrompt User ID / Name prompt to show in the dialog, if null a name will not be prompted for. * @param defaultUserID default name when prompting for a name * @param hasMessages true if the client will set messages on this dialog. If true, the * dialog's minimum size will be increased */ public PasswordDialog(String title, String serverType, String serverName, String passPrompt, - String namePrompt, String defaultUserID, boolean hasMessages) { + String userIdPrompt, String defaultUserID, boolean hasMessages) { super(title, true); + + this.defaultUserID = defaultUserID; + setRememberSize(false); setTransient(true); @@ -132,8 +136,8 @@ public class PasswordDialog extends DialogComponentProvider { workPanel.add(new GLabel(serverName)); } - if (namePrompt != null) { - workPanel.add(new GLabel(namePrompt)); + if (userIdPrompt != null) { + workPanel.add(new GLabel(userIdPrompt)); nameField = new JTextField(defaultUserID, 16); nameField.setName("NAME-ENTRY-COMPONENT"); workPanel.add(nameField); @@ -237,11 +241,11 @@ public class PasswordDialog extends DialogComponentProvider { } /** - * Return the user ID entered in the password field - * @return the user ID entered in the password field + * Return the user ID / Name entered in the password field + * @return the user ID / Name entered in the password field */ public String getUserID() { - return nameField != null ? nameField.getText().trim() : null; + return nameField != null ? nameField.getText().trim() : defaultUserID; } /** diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/DefaultClientAuthenticator.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/DefaultClientAuthenticator.java index b41a615b6b..8f5a3f3d18 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/DefaultClientAuthenticator.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/DefaultClientAuthenticator.java @@ -4,9 +4,9 @@ * 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. @@ -16,11 +16,12 @@ package ghidra.framework.client; import java.awt.Component; -import java.net.Authenticator; -import java.net.PasswordAuthentication; +import java.net.*; import javax.security.auth.callback.*; +import org.apache.commons.lang3.StringUtils; + import docking.DockingWindowManager; import docking.widgets.*; import ghidra.framework.preferences.Preferences; @@ -35,23 +36,63 @@ public class DefaultClientAuthenticator extends PopupKeyStorePasswordProvider private Authenticator authenticator = new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { - Msg.debug(this, "PasswordAuthentication requested for " + getRequestingURL()); - NameCallback nameCb = null; - if (!"NO_NAME".equals(getRequestingScheme())) { - nameCb = new NameCallback("Name: ", ClientUtil.getUserName()); + + String serverName = getRequestingHost(); + URL requestingURL = getRequestingURL(); + + String pwd = null; + String userName = ClientUtil.getUserName(); + boolean useDefaultUser = true; + + if (requestingURL != null) { + String userInfo = requestingURL.getUserInfo(); + if (userInfo != null) { + // Use user info from URL + int pwdSep = userInfo.indexOf(':'); + if (pwdSep < 0) { + userName = userInfo; + useDefaultUser = false; + } + else { + pwd = userInfo.substring(pwdSep + 1); + if (pwdSep != 0) { + userName = userInfo.substring(0, pwdSep); + useDefaultUser = false; + } + } + } + + URL minimalURL = DefaultClientAuthenticator.getMinimalURL(requestingURL); + if (minimalURL != null) { + serverName = minimalURL.toExternalForm(); + } } + + Msg.debug(this, "PasswordAuthentication requested for " + serverName); + + if (pwd != null) { + // Requesting URL specified password + return new PasswordAuthentication(userName, pwd.toCharArray()); + } + + NameCallback nameCb = new NameCallback("Name: ", userName); + if (!useDefaultUser) { + // Prevent modification of user name by password prompting + nameCb.setName(userName); + } + + // Prompt for password String prompt = getRequestingPrompt(); - if (prompt == null) { - prompt = "Password:"; + if (StringUtils.isBlank(prompt) || "security".equals(prompt)) { + prompt = "Password:"; // assume dialog will show user name via nameCb } PasswordCallback passCb = new PasswordCallback(prompt, false); try { ServerPasswordPrompt pp = new ServerPasswordPrompt("Connection Authentication", - "Server", getRequestingHost(), nameCb, passCb, null, null, null); + "Server", serverName, nameCb, passCb, null, null, null); SystemUtilities.runSwingNow(pp); if (pp.okWasPressed()) { - return new PasswordAuthentication(nameCb != null ? nameCb.getName() : null, - passCb.getPassword()); + return new PasswordAuthentication(nameCb.getName(), passCb.getPassword()); } } finally { @@ -61,6 +102,21 @@ public class DefaultClientAuthenticator extends PopupKeyStorePasswordProvider } }; + /** + * Produce minimal URL (i.e., protocol, host and port) + * @param url request URL + * @return minimal URL + */ + public static URL getMinimalURL(URL url) { + try { + return new URL(url, "/"); + } + catch (MalformedURLException e) { + // ignore + } + return null; + } + @Override public Authenticator getAuthenticator() { return authenticator; @@ -165,10 +221,25 @@ public class DefaultClientAuthenticator extends PopupKeyStorePasswordProvider choicePrompt = choiceCb.getPrompt(); choices = choiceCb.getChoices(); } - PasswordDialog pwdDialog = - new PasswordDialog(title, serverType, serverName, passCb.getPrompt(), - nameCb != null ? nameCb.getPrompt() : null, getDefaultUserName(), choicePrompt, - choices, getDefaultChoice(), anonymousCb != null); + + String defaultUserName = null; + String namePrompt = null; + if (nameCb != null) { + defaultUserName = nameCb.getName(); + if (defaultUserName == null) { + // Name entry only permitted with name callback where name has not be pre-set + defaultUserName = nameCb.getDefaultName(); + namePrompt = nameCb.getPrompt(); + } + } + if (defaultUserName == null) { + defaultUserName = getDefaultUserName(); + } + + PasswordDialog pwdDialog = new PasswordDialog(title, serverType, serverName, + passCb.getPrompt(), namePrompt, defaultUserName, choicePrompt, choices, + getDefaultChoice(), anonymousCb != null); + if (errorMsg != null) { pwdDialog.setErrorText(errorMsg); } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/HeadlessClientAuthenticator.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/HeadlessClientAuthenticator.java index 78d9cee859..2eaccec7e0 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/HeadlessClientAuthenticator.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/HeadlessClientAuthenticator.java @@ -4,9 +4,9 @@ * 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. @@ -39,38 +39,58 @@ public class HeadlessClientAuthenticator implements ClientAuthenticator { private final static char[] BADPASSWORD = "".toCharArray(); private static Object sshPrivateKey; - private static String userID = ClientUtil.getUserName(); // default username + private static String defaultUserName = ClientUtil.getUserName(); private static boolean passwordPromptAllowed; private Authenticator authenticator = new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { - Msg.debug(this, "PasswordAuthentication requested for " + getRequestingURL()); - String usage = null; - String prompt = getRequestingPrompt(); - if ("security".equals(prompt)) { - prompt = null; // squash generic "security" prompt + + if (defaultUserName == null) { + throw new IllegalStateException("Default user name is unknown"); } - URL requestingURL = getRequestingURL(); + + String serverName = getRequestingHost(); + URL requestingURL = getRequestingURL(); // may be null + + String pwd = null; + String userName = defaultUserName; + if (requestingURL != null) { - URL minimalURL = null; - try { - minimalURL = new URL(requestingURL, "/"); + String userInfo = requestingURL.getUserInfo(); + if (userInfo != null) { + // Use user info from URL + int pwdSep = userInfo.indexOf(':'); + if (pwdSep < 0) { + userName = userInfo; + } + else { + pwd = userInfo.substring(pwdSep + 1); + if (pwdSep != 0) { + userName = userInfo.substring(0, pwdSep); + } + } } - catch (MalformedURLException e) { - // ignore + + URL minimalURL = DefaultClientAuthenticator.getMinimalURL(requestingURL); + if (minimalURL != null) { + serverName = minimalURL.toExternalForm(); } - usage = "Access password requested for " + - (minimalURL != null ? minimalURL.toExternalForm() - : requestingURL.getAuthority()); - prompt = "Password:"; } - if (prompt == null) { - // Assume Ghidra Server access - String host = getRequestingHost(); - prompt = (host != null ? (host + " ") : "") + "(" + userID + ") Password:"; + + Msg.debug(this, "PasswordAuthentication requested for " + serverName); + + if (pwd != null) { + // Requesting URL specified password + return new PasswordAuthentication(userName, pwd.toCharArray()); } - return new PasswordAuthentication(userID, getPassword(usage, prompt)); + + String usage = "Access password requested for " + serverName; + String prompt = getRequestingPrompt(); + if (StringUtils.isBlank(prompt) || "security".equals(prompt)) { + prompt = "Password for " + userName +":"; + } + return new PasswordAuthentication(userName, getPassword(usage, prompt)); } }; @@ -85,7 +105,8 @@ public class HeadlessClientAuthenticator implements ClientAuthenticator { /** * Install headless client authenticator for Ghidra Server * @param username optional username to be used with a Ghidra Server which - * allows username to be specified + * allows username to be specified. If null, {@link ClientUtil#getUserName()} + * will be used. * @param keystorePath optional PKI or SSH keystore path. May also be specified * as resource path for SSH key. * @param allowPasswordPrompt if true the user may be prompted for passwords @@ -97,7 +118,7 @@ public class HeadlessClientAuthenticator implements ClientAuthenticator { boolean allowPasswordPrompt) throws IOException { passwordPromptAllowed = allowPasswordPrompt; if (username != null) { - userID = username; + defaultUserName = username; } // clear existing key store settings @@ -175,7 +196,7 @@ public class HeadlessClientAuthenticator implements ClientAuthenticator { passwordPrompt += "\n"; } - if (prompt == null) { + if (StringUtils.isBlank(prompt)) { prompt = "Password:"; } @@ -233,17 +254,39 @@ public class HeadlessClientAuthenticator implements ClientAuthenticator { anonymousCb.setAnonymousAccessRequested(true); return true; } + + if (defaultUserName == null) { + throw new IllegalStateException("Default user name is unknown"); + } + if (choiceCb != null) { choiceCb.setSelectedIndex(1); } - if (nameCb != null && userID != null) { - nameCb.setName(userID); + + String userName = null; + if (nameCb != null) { + userName = nameCb.getName(); + if (userName == null) { + userName = nameCb.getDefaultName(); + } } + if (userName == null) { + userName = defaultUserName; + } + + if (nameCb != null) { + nameCb.setName(defaultUserName); + } + String usage = null; if (serverName != null) { usage = serverType + ": " + serverName; } - char[] password = getPassword(usage, passCb.getPrompt()); + + // Ignore prompt specified by passCb + String prompt = "Password for " + userName +":"; + + char[] password = getPassword(usage, prompt); passCb.setPassword(password); return password != null; } @@ -278,7 +321,7 @@ public class HeadlessClientAuthenticator implements ClientAuthenticator { return false; } if (nameCb != null) { - nameCb.setName(userID); + nameCb.setName(defaultUserName); } try { sshCb.sign(sshPrivateKey); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java index 1c09ae4d27..53cb117531 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java @@ -4,9 +4,9 @@ * 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. @@ -687,6 +687,10 @@ public class GhidraURL { if (StringUtils.isBlank(host)) { throw new IllegalArgumentException("host required"); } + // TODO: Need to improve checks and use of URL encoding + if (host.indexOf('@') >= 0) { // prevent user info with hostname + throw new IllegalArgumentException("invalid host name"); + } if (StringUtils.isBlank(repositoryName)) { throw new IllegalArgumentException("repository name required"); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java index 447b97bea3..75517863bd 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java @@ -4,9 +4,9 @@ * 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. @@ -109,6 +109,9 @@ public class GhidraURLConnection extends URLConnection { public GhidraURLConnection(URL url, GhidraProtocolHandler protocolHandler) throws MalformedURLException { super(url); + if (url.getUserInfo() != null) { + throw new MalformedURLException("User info not supported by Ghidra URLs"); + } if (protocolHandler == null) { throw new IllegalArgumentException("missing required protocol handler"); }