package ctbrec.ui; import static java.nio.charset.StandardCharsets.*; import java.io.BufferedReader; 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.util.Arrays; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.OS; import ctbrec.Settings.ProxyType; import ctbrec.io.StreamRedirector; public class ExternalBrowser implements AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(ExternalBrowser.class); private static final ExternalBrowser INSTANCE = new ExternalBrowser(); private Lock lock = new ReentrantLock(); private Process p; private Consumer messageListener; private InputStream in; private OutputStream out; private Socket socket; private Thread reader; private volatile boolean stopped = true; private volatile boolean browserReady = false; private Object browserReadyLock = new Object(); public static ExternalBrowser getInstance() { return INSTANCE; } public void run(JSONObject jsonConfig, Consumer messageListener) throws InterruptedException, IOException { LOG.debug("Running browser with config {}", jsonConfig); lock.lock(); try { stopped = false; this.messageListener = messageListener; addProxyConfig(jsonConfig.getJSONObject("config")); File configDir = new File(Config.getInstance().getConfigDir(), "ctbrec-minimal-browser"); String[] cmdline = OS.getBrowserCommand(configDir.getCanonicalPath()); p = new ProcessBuilder(cmdline).start(); if (LOG.isTraceEnabled()) { new Thread(new StreamRedirector(p.getInputStream(), System.out)).start(); new Thread(new StreamRedirector(p.getErrorStream(), System.err)).start(); } else { new Thread(new StreamRedirector(p.getInputStream(), OutputStream.nullOutputStream())).start(); new Thread(new StreamRedirector(p.getErrorStream(), OutputStream.nullOutputStream())).start(); } LOG.debug("Browser started: {}", Arrays.toString(cmdline)); connectToRemoteControlSocket(); while (!browserReady) { synchronized (browserReadyLock) { browserReadyLock.wait(100); } } if(LOG.isTraceEnabled()) { LOG.debug("Connected to remote control server. Sending config {}", jsonConfig); } else { LOG.debug("Connected to remote control server. Sending config"); } out.write(jsonConfig.toString().getBytes(UTF_8)); out.write('\n'); out.flush(); LOG.debug("Waiting for browser to terminate"); p.waitFor(); int exitValue = p.exitValue(); p = null; reader = null; in = null; out = null; this.messageListener = null; LOG.debug("Browser Process terminated with {}", exitValue); } finally { lock.unlock(); } } private void connectToRemoteControlSocket() throws IOException { for (int i = 0; i < 20; i++) { try { socket = new Socket("localhost", 3202); in = socket.getInputStream(); out = socket.getOutputStream(); reader = new Thread(this::readBrowserOutput); reader.start(); LOG.debug("Connected to control socket"); return; } catch (IOException e) { if(i == 19) { LOG.error("Connection to remote control socket failed", e); throw e; } } // wait a bit, so that the remote server socket can be opened by electron try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public void executeJavaScript(String javaScript) throws IOException { //LOG.debug("Executing JS {}", javaScript); JSONObject script = new JSONObject(); script.put("execute", javaScript); out.write(script.toString().getBytes(UTF_8)); out.write('\n'); out.flush(); if(javaScript.equals("quit")) { stopped = true; } } @Override public void close() throws IOException { if(stopped) { return; } stopped = true; executeJavaScript("quit"); } private void readBrowserOutput() { LOG.debug("Browser output reader started"); try { BufferedReader br = new BufferedReader(new InputStreamReader(in)); String line; synchronized (browserReadyLock) { browserReady = true; browserReadyLock.notifyAll(); } while( !Thread.interrupted() && (line = br.readLine()) != null ) { LOG.debug("Browser output: {}", line); if(!line.startsWith("{")) { } else { if(messageListener != null) { messageListener.accept(line); } } } } catch (IOException e) { if(!stopped) { LOG.error("Couldn't read browser output", e); } } finally { stopped = true; synchronized (browserReadyLock) { browserReady = true; browserReadyLock.notifyAll(); } } } private void addProxyConfig(JSONObject jsonConfig) { ProxyType proxyType = Config.getInstance().getSettings().proxyType; switch (proxyType) { case HTTP: JSONObject proxy = new JSONObject(); proxy.put("address", "http=" + 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()) { String username = Config.getInstance().getSettings().proxyUser; String password = Config.getInstance().getSettings().proxyPassword; proxy.put("user", username); proxy.put("password", password); } jsonConfig.put("proxy", proxy); break; case SOCKS4: proxy = new JSONObject(); proxy.put("address", "socks4://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort); jsonConfig.put("proxy", proxy); break; case SOCKS5: proxy = new JSONObject(); proxy.put("address", "socks5://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort); if(Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) { String username = Config.getInstance().getSettings().proxyUser; String password = Config.getInstance().getSettings().proxyPassword; proxy.put("user", username); proxy.put("password", password); } jsonConfig.put("proxy", proxy); break; case DIRECT: default: // nothing to do here break; } } }