forked from j62/ctbrec
1
0
Fork 0

Fix NPE in ExternalBrowser

This commit is contained in:
0xb00bface 2023-11-11 12:39:18 +01:00
parent ef2e354d65
commit 8a71668508
1 changed files with 80 additions and 85 deletions

View File

@ -1,47 +1,37 @@
package ctbrec.ui; package ctbrec.ui;
import static java.nio.charset.StandardCharsets.*; import ctbrec.Config;
import ctbrec.OS;
import ctbrec.io.ProcessOutputLogger;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import java.io.BufferedReader; import java.io.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket; import java.net.Socket;
import java.util.Arrays; import java.util.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.json.JSONObject; import static java.nio.charset.StandardCharsets.UTF_8;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config; @Slf4j
import ctbrec.OS; public class ExternalBrowser implements AutoCloseable { // NOSONAR singleton is wanted
import ctbrec.io.ProcessOutputLogger;
public class ExternalBrowser implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(ExternalBrowser.class);
private static final ExternalBrowser INSTANCE = new ExternalBrowser(); private static final ExternalBrowser INSTANCE = new ExternalBrowser();
private Lock lock = new ReentrantLock(); private static final String MSGID = "msgid";
private final Lock lock = new ReentrantLock();
private Socket socket;
private Consumer<String> messageListener; private Consumer<String> messageListener;
private InputStream in; private InputStream in;
private OutputStream out; private OutputStream out;
private Socket socket; // NOSONAR
private Thread reader; private Thread reader;
private volatile boolean stopped = true; private volatile boolean stopped = true;
private volatile boolean browserReady = false; private volatile boolean browserReady = false;
private Object browserReadyLock = new Object(); private final Object browserReadyLock = new Object();
private Map<String, CompletableFuture<Object>> responseFutures = new HashMap<>(); private final Map<String, CompletableFuture<Object>> responseFutures = new HashMap<>();
private Runnable onReadyCallback; private Runnable onReadyCallback;
public static ExternalBrowser getInstance() { public static ExternalBrowser getInstance() {
@ -49,7 +39,7 @@ public class ExternalBrowser implements AutoCloseable {
} }
public void run(JSONObject jsonConfig, Consumer<String> messageListener) throws InterruptedException, IOException { public void run(JSONObject jsonConfig, Consumer<String> messageListener) throws InterruptedException, IOException {
LOG.debug("Running browser with config {}", jsonConfig); log.debug("Running browser with config {}", jsonConfig);
lock.lock(); lock.lock();
try { try {
stopped = false; stopped = false;
@ -62,7 +52,7 @@ public class ExternalBrowser implements AutoCloseable {
Process p = new ProcessBuilder(cmdline).start(); Process p = new ProcessBuilder(cmdline).start();
new Thread(new ProcessOutputLogger(p.getInputStream(), "ExternalBrowser stdout")).start(); new Thread(new ProcessOutputLogger(p.getInputStream(), "ExternalBrowser stdout")).start();
new Thread(new ProcessOutputLogger(p.getErrorStream(), "ExternalBrowser stderr")).start(); new Thread(new ProcessOutputLogger(p.getErrorStream(), "ExternalBrowser stderr")).start();
LOG.debug("Browser started: {}", Arrays.toString(cmdline)); log.debug("Browser started: {}", Arrays.toString(cmdline));
connectToRemoteControlSocket(); connectToRemoteControlSocket();
while (!browserReady) { while (!browserReady) {
@ -70,10 +60,10 @@ public class ExternalBrowser implements AutoCloseable {
browserReadyLock.wait(100); browserReadyLock.wait(100);
} }
} }
if(LOG.isTraceEnabled()) { if (log.isTraceEnabled()) {
LOG.debug("Connected to remote control server. Sending config {}", jsonConfig); log.debug("Connected to remote control server. Sending config {}", jsonConfig);
} else { } else {
LOG.debug("Connected to remote control server. Sending config"); log.debug("Connected to remote control server. Sending config");
} }
out.write(jsonConfig.toString().getBytes(UTF_8)); out.write(jsonConfig.toString().getBytes(UTF_8));
out.write('\n'); out.write('\n');
@ -81,14 +71,14 @@ public class ExternalBrowser implements AutoCloseable {
Optional.ofNullable(onReadyCallback).ifPresent(Runnable::run); Optional.ofNullable(onReadyCallback).ifPresent(Runnable::run);
LOG.debug("Waiting for browser to terminate"); log.debug("Waiting for browser to terminate");
p.waitFor(); p.waitFor();
int exitValue = p.exitValue(); int exitValue = p.exitValue();
reader = null; reader = null;
in = null; in = null;
out = null; out = null;
this.messageListener = null; this.messageListener = null;
LOG.debug("Browser Process terminated with {}", exitValue); log.debug("Browser Process terminated with {}", exitValue);
} finally { } finally {
lock.unlock(); lock.unlock();
} }
@ -102,11 +92,11 @@ public class ExternalBrowser implements AutoCloseable {
out = socket.getOutputStream(); out = socket.getOutputStream();
reader = new Thread(this::readBrowserOutput); reader = new Thread(this::readBrowserOutput);
reader.start(); reader.start();
LOG.debug("Connected to control socket"); log.debug("Connected to control socket");
return; return;
} catch (IOException e) { } catch (IOException e) {
if (i == 19) { if (i == 19) {
LOG.error("Connection to remote control socket failed", e); log.error("Connection to remote control socket failed", e);
throw e; throw e;
} }
} }
@ -122,9 +112,9 @@ public class ExternalBrowser implements AutoCloseable {
public CompletableFuture<Object> executeJavaScript(String javaScript) throws IOException { public CompletableFuture<Object> executeJavaScript(String javaScript) throws IOException {
String id = UUID.randomUUID().toString(); String id = UUID.randomUUID().toString();
var future = new CompletableFuture<Object>(); var future = new CompletableFuture<>();
var script = new JSONObject(); var script = new JSONObject();
script.put("msgid", id); script.put(MSGID, id);
script.put("execute", javaScript); script.put("execute", javaScript);
if (out != null) { if (out != null) {
out.write(script.toString().getBytes(UTF_8)); out.write(script.toString().getBytes(UTF_8));
@ -148,7 +138,7 @@ public class ExternalBrowser implements AutoCloseable {
} }
private void readBrowserOutput() { private void readBrowserOutput() {
LOG.debug("Browser output reader started"); log.debug("Browser output reader started");
try (var br = new BufferedReader(new InputStreamReader(in))) { try (var br = new BufferedReader(new InputStreamReader(in))) {
String line; String line;
synchronized (browserReadyLock) { synchronized (browserReadyLock) {
@ -156,19 +146,21 @@ public class ExternalBrowser implements AutoCloseable {
browserReadyLock.notifyAll(); browserReadyLock.notifyAll();
} }
while (!Thread.interrupted() && (line = br.readLine()) != null) { while (!Thread.interrupted() && (line = br.readLine()) != null) {
LOG.debug("Browser output: {}", line); log.debug("Browser output: {}", line);
if (line.startsWith("{")) { if (line.startsWith("{")) {
JSONObject json = new JSONObject(line); JSONObject json = new JSONObject(line);
if (json.has("msgid")) { if (json.has(MSGID)) {
handleExecuteScriptResponse(json); handleExecuteScriptResponse(json);
} else { } else {
if (messageListener != null) {
messageListener.accept(line); messageListener.accept(line);
} }
} }
} }
}
} catch (IOException e) { } catch (IOException e) {
if (!stopped) { if (!stopped) {
LOG.error("Couldn't read browser output", e); log.error("Couldn't read browser output", e);
} }
} finally { } finally {
stopped = true; stopped = true;
@ -180,29 +172,32 @@ public class ExternalBrowser implements AutoCloseable {
} }
private void handleExecuteScriptResponse(JSONObject json) { private void handleExecuteScriptResponse(JSONObject json) {
var msgid = json.getString("msgid"); var msgid = json.getString(MSGID);
LOG.debug("Future {}", msgid); log.debug("Future {}", msgid);
CompletableFuture<Object> future = responseFutures.get(msgid); CompletableFuture<Object> future = responseFutures.get(msgid);
if (future != null) { if (future != null) {
responseFutures.remove(msgid); responseFutures.remove(msgid);
if (json.has("result")) { final String RESULT = "result";
LOG.debug("Future {} done. Result: {}", msgid, json.get("result")); if (json.has(RESULT)) {
future.complete(json.get("result")); log.debug("Future {} done. Result: {}", msgid, json.get(RESULT));
future.complete(json.get(RESULT));
} else if (json.has("error")) { } else if (json.has("error")) {
LOG.debug("Future {} failed", msgid); log.debug("Future {} failed", msgid);
future.completeExceptionally(new Exception(json.getJSONObject("error").toString())); future.completeExceptionally(new Exception(json.getJSONObject("error").toString()));
} }
} else { } else {
LOG.warn("No future for previous request {}", msgid); log.warn("No future for previous request {}", msgid);
} }
} }
private void addProxyConfig(JSONObject jsonConfig) { private void addProxyConfig(JSONObject jsonConfig) {
var proxyType = Config.getInstance().getSettings().proxyType; var proxyType = Config.getInstance().getSettings().proxyType;
var proxy = new JSONObject();
final String KEY_ADDRESS = "address";
final String KEY_PROXY = "proxy";
switch (proxyType) { switch (proxyType) {
case HTTP: case HTTP:
var proxy = new JSONObject(); proxy.put(KEY_ADDRESS,
proxy.put("address",
"http=" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort "http=" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort
+ ";https=" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort); + ";https=" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
if (Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) { if (Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) {
@ -211,23 +206,23 @@ public class ExternalBrowser implements AutoCloseable {
proxy.put("user", username); proxy.put("user", username);
proxy.put("password", password); proxy.put("password", password);
} }
jsonConfig.put("proxy", proxy); jsonConfig.put(KEY_PROXY, proxy);
break; break;
case SOCKS4: case SOCKS4:
proxy = new JSONObject(); proxy = new JSONObject();
proxy.put("address", "socks4://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort); proxy.put(KEY_ADDRESS, "socks4://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
jsonConfig.put("proxy", proxy); jsonConfig.put(KEY_PROXY, proxy);
break; break;
case SOCKS5: case SOCKS5:
proxy = new JSONObject(); proxy = new JSONObject();
proxy.put("address", "socks5://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort); proxy.put(KEY_ADDRESS, "socks5://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
if (Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) { if (Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) {
String username = Config.getInstance().getSettings().proxyUser; String username = Config.getInstance().getSettings().proxyUser;
String password = Config.getInstance().getSettings().proxyPassword; String password = Config.getInstance().getSettings().proxyPassword;
proxy.put("user", username); proxy.put("user", username);
proxy.put("password", password); proxy.put("password", password);
} }
jsonConfig.put("proxy", proxy); jsonConfig.put(KEY_PROXY, proxy);
break; break;
case DIRECT: case DIRECT:
default: default: