From 867fdeaa1ff83ef160ecb2700f72160fac8628cf Mon Sep 17 00:00:00 2001 From: ghidra1 Date: Thu, 12 Dec 2024 14:15:22 -0500 Subject: [PATCH] GP-5167 Updated BSimServerInfo to support DB user info and conveying via URL. Updated elastic and postgresql URL for bsim commands to allow include of user info. Renamed Error to BSImError to avoid naming conflict. --- .../AddProgramToH2BSimDatabaseScript.java | 7 +- .../CreateH2BSimDatabaseScript.java | 9 +- .../QueryWithFiltersScript.java | 6 +- .../topics/BSim/CommandLineReference.html | 31 +- .../topics/BSim/DatabaseConfiguration.html | 80 ++++- .../topics/BSimSearchPlugin/BSimSearch.html | 32 +- .../images/AddServerDialog.png | Bin 8944 -> 7042 bytes .../images/ManageServersDialog.png | Bin 20021 -> 12289 bytes .../features/bsim/gui/BSimServerManager.java | 7 +- .../gui/search/dialog/BSimServerDialog.java | 6 +- .../search/dialog/BSimServerTableModel.java | 44 ++- .../dialog/CreateBsimServerInfoDialog.java | 261 +++++++++++++++-- .../BSimPostgresDBConnectionManager.java | 29 +- .../features/bsim/query/BSimServerInfo.java | 273 +++++++++++++++--- .../features/bsim/query/FunctionDatabase.java | 23 +- .../client/AbstractSQLFunctionDatabase.java | 33 +-- .../query/client/FunctionDatabaseProxy.java | 21 +- .../client/PostgresFunctionDatabase.java | 13 +- .../bsim/query/elastic/ElasticConnection.java | 178 +++++++----- .../bsim/query/elastic/ElasticDatabase.java | 37 +-- .../facade/SimilarFunctionQueryService.java | 6 +- .../bsim/query/ingest/BSimLaunchable.java | 41 ++- .../bsim/query/ingest/BulkSignatures.java | 77 +++-- .../BSimSearchPluginScreenShots.java | 16 +- .../bsim/gui/BSimSearchPluginTest.java | 6 +- .../query/file/BSimH2DatabaseManagerTest.java | 8 +- .../bsim/query/test/BSimServerTest.java | 4 +- .../facade/FunctionDatabaseTestDouble.java | 13 +- .../bsim/query/facade/TestBSimServerInfo.java | 6 +- .../java/docking/widgets/PasswordDialog.java | 36 ++- .../client/DefaultClientAuthenticator.java | 105 +++++-- .../client/HeadlessClientAuthenticator.java | 103 +++++-- .../framework/protocol/ghidra/GhidraURL.java | 8 +- .../protocol/ghidra/GhidraURLConnection.java | 7 +- 34 files changed, 1097 insertions(+), 429 deletions(-) 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:

+ +
    +
  • Name - Indicates the name of the BSim database. In the cases of an H2 file + database this represents the name of the file (hovering on this will show the full absolute path).
  • +
  • Type - The type of database (postgres, elastic or file).
  • +
  • Host - The network host name or IP address for a postgres or + elastic database.
  • +
  • Port - The TCP port for a postgres or elastic database.
  • +
  • User - The non-default user name to be used when connecting to a postgres or + elastic database. When actively connected to a postgres database + the actual name used for authentication will be shown.
  • +
  • Active/Idle Connections - The active/idle database connections for the + connection pool associated with a postgres or file database. + This field will be blank for these databases when in a disconnected state.
  • +

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 121054149040671f3af88304901b59be88b3db0d..0594435747e008a12b6bf851b9d8cfea38795402 100644 GIT binary patch literal 7042 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 literal 8944 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 v7}fA{{pQ5wuK$)>5icw2u5=^gDbC literal 20021 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 { + + @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"); }