TLS and Graphics, what more?
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 43s
Release j3270 / Build & Publish Release (push) Successful in 1m7s

This commit is contained in:
2026-08-21 02:51:24 -04:00
parent f0fc1b6e70
commit d7d2f8bad5
49 changed files with 3611 additions and 102 deletions
@@ -227,8 +227,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private void showConnectDialog() {
ConnectDialog dialog = new ConnectDialog(this);
dialog.setInitialHost(lastHost);
dialog.setInitialPort(lastPort);
String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : org.pubvm.j3270.config.Settings.getAutoConnectHost();
int initialPort = lastPort > 0 ? lastPort : org.pubvm.j3270.config.Settings.getAutoConnectPort();
dialog.setInitialHost(initialHost);
dialog.setInitialPort(initialPort);
dialog.setInitialTls(org.pubvm.j3270.config.Settings.getAutoConnectTls());
dialog.setInitialVerifyCert(org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert());
dialog.setVisible(true);
if (dialog.isConfirmed()) {
@@ -255,10 +259,19 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// Save for auto-connect
org.pubvm.j3270.config.Settings.setAutoConnectHost(config.getHost());
org.pubvm.j3270.config.Settings.setAutoConnectPort(config.getPort());
org.pubvm.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
org.pubvm.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
// Disconnect existing connection
disconnect();
// Wire interactive certificate verifier if none configured
if (config.getCertificateVerifier() == null) {
config.setCertificateVerifier((chain, authType, exception) ->
org.pubvm.j3270.ui.UntrustedCertificateDialog.showPrompt(this, config.getHost(), config.getPort(), chain, exception)
);
}
client = new Telnet3270Client(config);
client.addConnectionListener(this);
client.addScreenUpdateListener(this);
@@ -266,7 +279,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
terminalPanel.setClient(client);
statusBar.setClient(client);
setTitle("j3270 — " + config.getHost() + ":" + config.getPort());
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
// Resize window to match model
terminalPanel.guardedPack();
@@ -426,11 +440,24 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
public static void main(String[] args) {
boolean debug = false;
boolean cliTls = false;
boolean cliNoVerifyCert = false;
org.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
String configFile = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) {
if ("--debug".equals(args[i]) || "-d".equals(args[i])) {
debug = true;
} else if ("--tls".equals(args[i]) || "--ssl".equals(args[i]) || "-s".equals(args[i])) {
cliTls = true;
} else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) {
cliNoVerifyCert = true;
} else if (args[i].startsWith("--graphics=")) {
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.fromString(args[i].substring(11));
} else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.fromString(args[++i]);
} else if ("--no-graphics".equals(args[i])) {
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.NONE;
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
configFile = args[++i];
} else {
@@ -439,33 +466,41 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}
// Configure logging for application packages
Level logLevel = debug ? Level.ALL : Level.INFO;
Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h);
}
globalRoot.setLevel(Level.WARNING);
globalRoot.setLevel(Level.ALL);
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(debug ? Level.ALL : Level.INFO);
handler.setFormatter(new SimpleFormatter());
java.util.logging.Filter appFilter = record -> {
String name = record.getLoggerName();
return name != null && (name.startsWith("org.lib3270j") || name.startsWith("org.pubvm.j3270"));
};
Logger appLogger = Logger.getLogger("org.pubvm.j3270");
appLogger.setLevel(Level.ALL);
appLogger.addHandler(handler);
appLogger.setUseParentHandlers(false);
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(logLevel);
consoleHandler.setFormatter(new SimpleFormatter());
consoleHandler.setFilter(appFilter);
globalRoot.addHandler(consoleHandler);
Logger libLogger = Logger.getLogger("org.lib3270j");
libLogger.setLevel(Level.ALL);
libLogger.addHandler(handler);
libLogger.setUseParentHandlers(false);
Logger.getLogger("org.pubvm").setLevel(logLevel);
Logger.getLogger("org.pubvm.j3270").setLevel(logLevel);
Logger.getLogger("org.lib3270j").setLevel(logLevel);
try {
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false);
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
@Override
public synchronized void publish(java.util.logging.LogRecord record) {
super.publish(record);
flush();
}
};
fileHandler.setLevel(Level.ALL);
fileHandler.setFormatter(new SimpleFormatter());
appLogger.addHandler(fileHandler);
libLogger.addHandler(fileHandler);
log.info("Logging protocol trace to j3270.log");
fileHandler.setFilter(appFilter);
globalRoot.addHandler(fileHandler);
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
} catch (Exception e) {
System.err.println("Could not create j3270.log: " + e.getMessage());
}
@@ -492,18 +527,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
System.setProperty("apple.laf.useScreenMenuBar", "true");
System.setProperty("apple.awt.application.name", "j3270");
final boolean finalTls = cliTls;
final boolean finalNoVerify = cliNoVerifyCert;
final org.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
SwingUtilities.invokeLater(() -> {
J3270App app = new J3270App();
app.setVisible(true);
// If host:port given on command line, connect directly
if (!remainingArgs.isEmpty()) {
String host = remainingArgs.get(0);
int port = 23;
String hostArg = remainingArgs.get(0);
int port = finalTls ? 992 : 23;
if (remainingArgs.size() >= 2) {
try {
port = Integer.parseInt(remainingArgs.get(1));
} catch (NumberFormatException e) {
} catch (NumberFormatException ignored) {
}
}
TerminalModel model = TerminalModel.IBM_3279_4;
@@ -511,10 +550,21 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
try {
int modelNum = Integer.parseInt(remainingArgs.get(2));
model = TerminalModel.forModel(modelNum, true);
} catch (Exception e) {
} catch (Exception ignored) {
}
}
ConnectionConfig config = new ConnectionConfig(host, port, model);
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
if (finalTls) {
config.setUseTls(true);
}
if (finalNoVerify) {
config.setTlsVerifyCert(false);
}
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
config.setGraphicsMode(org.pubvm.j3270.config.Settings.getGraphicsMode());
}
app.connect(config);
} else {
org.pubvm.j3270.config.Settings.StartupBehavior behavior = org.pubvm.j3270.config.Settings
@@ -526,8 +576,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
case AUTO_CONNECT:
String host = org.pubvm.j3270.config.Settings.getAutoConnectHost();
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
boolean tls = org.pubvm.j3270.config.Settings.getAutoConnectTls();
boolean verify = org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert();
if (host != null && !host.isEmpty()) {
app.connect(new ConnectionConfig(host, port, TerminalModel.IBM_3279_4));
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
config.setTlsVerifyCert(verify);
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
config.setGraphicsMode(org.pubvm.j3270.config.Settings.getGraphicsMode());
}
app.connect(config);
} else {
SwingUtilities.invokeLater(app::showConnectDialog);
}
@@ -62,6 +62,31 @@ public class Settings {
prefs.putInt("autoConnectPort", port);
}
public static boolean getAutoConnectTls() {
return prefs.getBoolean("autoConnectTls", false);
}
public static void setAutoConnectTls(boolean tls) {
prefs.putBoolean("autoConnectTls", tls);
}
public static boolean getAutoConnectVerifyCert() {
return prefs.getBoolean("autoConnectVerifyCert", true);
}
public static void setAutoConnectVerifyCert(boolean verify) {
prefs.putBoolean("autoConnectVerifyCert", verify);
}
public static org.lib3270j.graphics.GraphicsMode getGraphicsMode() {
String modeStr = prefs.get("graphicsMode", org.lib3270j.graphics.GraphicsMode.NONE.name());
return org.lib3270j.graphics.GraphicsMode.fromString(modeStr);
}
public static void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) {
prefs.put("graphicsMode", (mode != null ? mode : org.lib3270j.graphics.GraphicsMode.NONE).name());
}
public static Color getColorOverride(int index, Color defaultColor) {
String hex = prefs.get("color_" + index, null);
try {
@@ -194,15 +219,33 @@ public class Settings {
break;
case "behavior":
case "connection":
switch (key) {
case "startupBehavior":
setStartupBehavior(StartupBehavior.valueOf(value.toUpperCase()));
break;
case "autoConnectHost": setAutoConnectHost(value); break;
case "autoConnectPort": setAutoConnectPort(Integer.parseInt(value)); break;
case "autoConnectHost":
case "host":
setAutoConnectHost(value);
break;
case "autoConnectPort":
case "port":
setAutoConnectPort(Integer.parseInt(value));
break;
case "autoConnectTls":
case "tls":
case "ssl":
case "useTls":
setAutoConnectTls(Boolean.parseBoolean(value));
break;
case "autoConnectVerifyCert":
case "verifyCert":
case "tlsVerifyCert":
setAutoConnectVerifyCert(Boolean.parseBoolean(value));
break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
default:
log.warning("Unknown behavior key: " + key);
log.warning("Unknown behavior/connection key: " + key);
}
break;
@@ -222,6 +265,14 @@ public class Settings {
setKeyBinding(key, value);
break;
case "graphics":
if ("graphicsMode".equalsIgnoreCase(key) || "mode".equalsIgnoreCase(key)) {
setGraphicsMode(org.lib3270j.graphics.GraphicsMode.fromString(value));
} else {
log.warning("Unknown graphics key: " + key);
}
break;
default:
// Allow bare keys outside any section treat as raw prefs
log.fine("Setting raw preference: " + key + " = " + value);
@@ -256,10 +307,17 @@ public class Settings {
if (acHost != null && !acHost.isEmpty()) {
w.println("autoConnectHost = " + acHost);
w.println("autoConnectPort = " + getAutoConnectPort());
w.println("autoConnectTls = " + getAutoConnectTls());
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
}
w.println("blockSelectMode = " + getBlockSelectMode());
w.println();
// [graphics]
w.println("[graphics]");
w.println("graphicsMode = " + getGraphicsMode().name());
w.println();
// [colors]
w.println("[colors]");
// Host colors 0-15
@@ -14,7 +14,10 @@ public class ConnectDialog extends JDialog {
private JTextField hostField;
private JTextField portField;
private JComboBox<TerminalModel> modelCombo;
private JComboBox<org.lib3270j.graphics.GraphicsMode> graphicsCombo;
private JTextField luField;
private JCheckBox tlsCheckBox;
private JCheckBox verifyCertCheckBox;
private boolean confirmed;
private ConnectionConfig result;
@@ -93,6 +96,56 @@ public class ConnectDialog extends JDialog {
luField = createDarkField(12);
mainPanel.add(luField, gbc);
// Graphics Mode
gbc.gridx = 0;
gbc.gridy = 4;
gbc.weightx = 0;
JLabel graphicsLabel = new JLabel("Graphics:");
graphicsLabel.setForeground(fg);
graphicsLabel.setFont(labelFont);
mainPanel.add(graphicsLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
graphicsCombo = new JComboBox<>(org.lib3270j.graphics.GraphicsMode.values());
graphicsCombo.setSelectedItem(org.pubvm.j3270.config.Settings.getGraphicsMode());
graphicsCombo.setBackground(new Color(45, 45, 45));
graphicsCombo.setForeground(fg);
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
mainPanel.add(graphicsCombo, gbc);
// TLS / SSL Checkbox
gbc.gridx = 1;
gbc.gridy = 5;
gbc.weightx = 1.0;
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
tlsCheckBox.setBackground(new Color(30, 30, 30));
tlsCheckBox.setForeground(fg);
tlsCheckBox.setFont(labelFont);
tlsCheckBox.setFocusPainted(false);
tlsCheckBox.addActionListener(e -> {
boolean isTls = tlsCheckBox.isSelected();
verifyCertCheckBox.setEnabled(isTls);
String currentPort = portField.getText().trim();
if (isTls && "23".equals(currentPort)) {
portField.setText("992");
} else if (!isTls && "992".equals(currentPort)) {
portField.setText("23");
}
});
mainPanel.add(tlsCheckBox, gbc);
// Verify Certificate Checkbox
gbc.gridx = 1;
gbc.gridy = 6;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
verifyCertCheckBox.setBackground(new Color(30, 30, 30));
verifyCertCheckBox.setForeground(new Color(160, 160, 160));
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
verifyCertCheckBox.setSelected(true);
verifyCertCheckBox.setEnabled(false);
verifyCertCheckBox.setFocusPainted(false);
mainPanel.add(verifyCertCheckBox, gbc);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttonPanel.setBackground(new Color(30, 30, 30));
@@ -116,7 +169,7 @@ public class ConnectDialog extends JDialog {
buttonPanel.add(connectBtn);
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridy = 7;
gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc);
@@ -158,6 +211,9 @@ public class ConnectDialog extends JDialog {
if (!lu.isEmpty()) {
result.setLuName(lu);
}
result.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
result.setUseTls(tlsCheckBox.isSelected());
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
confirmed = true;
dispose();
}
@@ -178,4 +234,13 @@ public class ConnectDialog extends JDialog {
public void setInitialPort(int port) {
portField.setText(String.valueOf(port));
}
public void setInitialTls(boolean tls) {
tlsCheckBox.setSelected(tls);
verifyCertCheckBox.setEnabled(tls);
}
public void setInitialVerifyCert(boolean verify) {
verifyCertCheckBox.setSelected(verify);
}
}
@@ -31,6 +31,7 @@ public class SettingsDialog extends JDialog {
// Appearance tab
private JComboBox<String> fontBox;
private JSpinner fontSizeSpinner;
private JComboBox<org.lib3270j.graphics.GraphicsMode> graphicsModeBox;
// Behavior tab
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
@@ -264,8 +265,21 @@ public class SettingsDialog extends JDialog {
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(fontSizeSpinner, gbc);
// Fill remaining space
// Graphics Mode
gbc.gridx = 0;
gbc.gridy = 2;
gbc.fill = GridBagConstraints.NONE;
JLabel graphicsLabel = new JLabel("Graphics Mode:");
panel.add(graphicsLabel, gbc);
graphicsModeBox = new JComboBox<>(org.lib3270j.graphics.GraphicsMode.values());
graphicsModeBox.setSelectedItem(Settings.getGraphicsMode());
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(graphicsModeBox, gbc);
// Fill remaining space
gbc.gridy = 3;
gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc);
@@ -611,6 +625,9 @@ public class SettingsDialog extends JDialog {
Settings.setFontFamily(fontFam);
}
Settings.setFontSize((Integer) fontSizeSpinner.getValue());
if (graphicsModeBox.getSelectedItem() != null) {
Settings.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsModeBox.getSelectedItem());
}
// Apply Behavior
Settings.StartupBehavior behavior = (Settings.StartupBehavior) startupBehaviorBox.getSelectedItem();
@@ -15,6 +15,7 @@ import java.awt.*;
public class StatusBar extends JPanel {
private final JLabel connectionStatus;
private final JLabel tlsStatus;
private final JLabel cursorPosition;
private final JLabel luName;
private final JLabel lockStatus;
@@ -37,6 +38,7 @@ public class StatusBar extends JPanel {
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM);
tlsStatus = createLabel("", oiaFont, OIA_FG);
luName = createLabel("", oiaFont, OIA_FG);
lockStatus = createLabel("", oiaFont, OIA_ALERT);
modelInfo = createLabel("", oiaFont, OIA_DIM);
@@ -44,6 +46,8 @@ public class StatusBar extends JPanel {
add(Box.createHorizontalStrut(6));
add(connectionStatus);
add(Box.createHorizontalStrut(10));
add(tlsStatus);
add(Box.createHorizontalStrut(12));
add(luName);
add(Box.createHorizontalStrut(12));
@@ -110,6 +114,26 @@ public class StatusBar extends JPanel {
break;
}
// TLS Status
if (state.isConnected() && client.getConfig().isUseTls()) {
boolean verified = client.getConfig().isTlsVerifyCert();
javax.net.ssl.SSLSession session = client.getSslSession();
String cipher = session != null ? session.getCipherSuite() : "TLS";
String protocol = session != null ? session.getProtocol() : "TLS";
if (verified) {
tlsStatus.setText("🔒 TLS");
tlsStatus.setForeground(OIA_FG);
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
} else {
tlsStatus.setText("🔓 TLS (Unverified)");
tlsStatus.setForeground(new Color(255, 180, 80));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
}
} else {
tlsStatus.setText("");
tlsStatus.setToolTipText(null);
}
// LU name
String lu = "";
if (client.getConnectionState().isTn3270e()) {
@@ -921,13 +921,21 @@ public class TerminalPanel extends JPanel {
g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
// Draw character
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
g2.setFont(f);
g2.setColor(fgColor);
g2.drawString(String.valueOf(ch), x, y + fontAscent);
// Draw character or Programmed Symbol
int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0);
boolean drawnAsPs = false;
if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
drawnAsPs = client.getProgramSymbolManager().drawSymbol(g2, cs, ea.ec & 0xFF, x, y, cellWidth, cellHeight, fgColor, bgColor);
}
if (!drawnAsPs) {
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
g2.setFont(f);
g2.setColor(fgColor);
g2.drawString(String.valueOf(ch), x, y + fontAscent);
}
}
// Draw underline
@@ -945,6 +953,14 @@ public class TerminalPanel extends JPanel {
}
}
// Draw Vector Graphics Plane overlay if present
if (client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()) {
int gridW = cols * cellWidth;
int gridH = rows * cellHeight;
client.getGraphicsPlane().resize(gridW, gridH);
g2.drawImage(client.getGraphicsPlane().getImage(), ox, oy, gridW, gridH, null);
}
// Draw cursor
if (cursorVisible && client.getConnectionState().isFullSession()) {
int curAddr = sb.getCursorAddress();
@@ -0,0 +1,153 @@
package org.pubvm.j3270.ui;
import javax.swing.*;
import java.awt.*;
import java.security.MessageDigest;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Modal dialog for inspecting and confirming untrusted TLS/SSL server certificates.
*/
public class UntrustedCertificateDialog extends JDialog {
private boolean accepted = false;
public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
super(parent, "Untrusted SSL/TLS Certificate", true);
buildUI(host, port, chain, exception);
pack();
setLocationRelativeTo(parent);
setResizable(false);
}
private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) {
JPanel mainPanel = new JPanel(new BorderLayout(12, 12));
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
mainPanel.setBackground(new Color(30, 30, 30));
// Header
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
headerPanel.setBackground(new Color(30, 30, 30));
JLabel iconLabel = new JLabel("⚠️");
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
headerPanel.add(iconLabel);
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
titleBox.setBackground(new Color(30, 30, 30));
JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
titleLabel.setForeground(new Color(255, 180, 80));
titleBox.add(titleLabel);
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
subtitleLabel.setForeground(new Color(180, 180, 180));
titleBox.add(subtitleLabel);
headerPanel.add(titleBox);
mainPanel.add(headerPanel, BorderLayout.NORTH);
// Certificate Details Area
X509Certificate cert = (chain != null && chain.length > 0) ? chain[0] : null;
StringBuilder sb = new StringBuilder();
if (exception != null) {
sb.append("Validation Error:\n ").append(exception.getMessage() != null ? exception.getMessage() : exception.toString()).append("\n\n");
}
if (cert != null) {
sb.append("Subject:\n ").append(cert.getSubjectX500Principal().getName()).append("\n\n");
sb.append("Issuer:\n ").append(cert.getIssuerX500Principal().getName()).append("\n\n");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
sb.append("Validity:\n From: ").append(sdf.format(cert.getNotBefore()))
.append("\n To: ").append(sdf.format(cert.getNotAfter())).append("\n\n");
sb.append("Serial Number:\n ").append(cert.getSerialNumber().toString(16).toUpperCase()).append("\n\n");
sb.append("SHA-256 Fingerprint:\n ").append(computeFingerprint(cert, "SHA-256")).append("\n\n");
sb.append("SHA-1 Fingerprint:\n ").append(computeFingerprint(cert, "SHA-1"));
} else {
sb.append("No peer certificate information available.");
}
JTextArea detailsArea = new JTextArea(sb.toString());
detailsArea.setEditable(false);
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
detailsArea.setBackground(new Color(20, 20, 20));
detailsArea.setForeground(new Color(210, 210, 210));
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
JScrollPane scrollPane = new JScrollPane(detailsArea);
scrollPane.setPreferredSize(new Dimension(520, 260));
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
mainPanel.add(scrollPane, BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
buttonPanel.setBackground(new Color(30, 30, 30));
JButton cancelBtn = new JButton("Cancel Connection");
cancelBtn.setBackground(new Color(60, 60, 60));
cancelBtn.setForeground(new Color(220, 220, 220));
cancelBtn.addActionListener(e -> {
accepted = false;
dispose();
});
JButton trustBtn = new JButton("Connect Anyway");
trustBtn.setBackground(new Color(180, 100, 40));
trustBtn.setForeground(Color.WHITE);
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
trustBtn.addActionListener(e -> {
accepted = true;
dispose();
});
buttonPanel.add(cancelBtn);
buttonPanel.add(trustBtn);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
getRootPane().setDefaultButton(trustBtn);
}
public boolean isAccepted() {
return accepted;
}
private static String computeFingerprint(X509Certificate cert, String algorithm) {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] digest = md.digest(cert.getEncoded());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < digest.length; i++) {
if (i > 0) sb.append(':');
sb.append(String.format("%02X", digest[i]));
}
return sb.toString();
} catch (Exception e) {
return "Unable to compute fingerprint: " + e.getMessage();
}
}
/**
* Show the prompt modal dialog on the Swing EDT and return user choice.
*/
public static boolean showPrompt(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
if (SwingUtilities.isEventDispatchThread()) {
UntrustedCertificateDialog dialog = new UntrustedCertificateDialog(parent, host, port, chain, exception);
dialog.setVisible(true);
return dialog.isAccepted();
} else {
AtomicBoolean result = new AtomicBoolean(false);
try {
SwingUtilities.invokeAndWait(() -> {
UntrustedCertificateDialog dialog = new UntrustedCertificateDialog(parent, host, port, chain, exception);
dialog.setVisible(true);
result.set(dialog.isAccepted());
});
} catch (Exception e) {
return false;
}
return result.get();
}
}
}