diff --git a/.github/issue_template.md b/.github/issue_template.md
new file mode 100644
index 0000000..19ec540
--- /dev/null
+++ b/.github/issue_template.md
@@ -0,0 +1,25 @@
+
+
+## Expected Behavior
+
+
+## Actual Behavior
+
+
+## Steps to Reproduce the Problem
+
+ 1.
+ 2.
+ 3.
+
+## browserstack local arguments
+
+
+## Platform details
+
+ 1. browserstack-local-java version:
+ 2. java version:
+ 3. os type and version:
+
+## Details
+
diff --git a/.github/workflows/Semgrep.yml b/.github/workflows/Semgrep.yml
new file mode 100644
index 0000000..0347afd
--- /dev/null
+++ b/.github/workflows/Semgrep.yml
@@ -0,0 +1,48 @@
+# Name of this GitHub Actions workflow.
+name: Semgrep
+
+on:
+ # Scan changed files in PRs (diff-aware scanning):
+ # The branches below must be a subset of the branches above
+ pull_request:
+ branches: ["master", "main"]
+ push:
+ branches: ["master", "main"]
+ schedule:
+ - cron: '0 6 * * *'
+
+
+permissions:
+ contents: read
+
+jobs:
+ semgrep:
+ # User definable name of this GitHub Actions job.
+ permissions:
+ contents: read # for actions/checkout to fetch code
+ security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
+ name: semgrep/ci
+ # If you are self-hosting, change the following `runs-on` value:
+ runs-on: ubuntu-latest
+
+ container:
+ # A Docker image with Semgrep installed. Do not change this.
+ image: returntocorp/semgrep
+
+ # Skip any PR created by dependabot to avoid permission issues:
+ if: (github.actor != 'dependabot[bot]')
+
+ steps:
+ # Fetch project source with GitHub Actions Checkout.
+ - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ # Run the "semgrep ci" command on the command line of the docker image.
+ - run: semgrep ci --sarif --output=semgrep.sarif
+ env:
+ # Add the rules that Semgrep uses by setting the SEMGREP_RULES environment variable.
+ SEMGREP_RULES: p/default # more at semgrep.dev/explore
+
+ - name: Upload SARIF file for GitHub Advanced Security Dashboard
+ uses: github/codeql-action/upload-sarif@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0
+ with:
+ sarif_file: semgrep.sarif
+ if: always()
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 54ec726..7a7f809 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,23 @@
language: java
+addons:
+ apt:
+ packages:
+ - openjdk-6-jdk
+
jdk:
- - oraclejdk8
- - oraclejdk7
- openjdk6
-
-before_install:
- - true && `base64 --decode <<< ZXhwb3J0IEJST1dTRVJTVEFDS19BQ0NFU1NfS0VZPUh5VmZydXJvb3dYb041eGhLZEs2Cg==`
+ - oraclejdk8
+ - openjdk7
+ - openjdk8
+
+install:
+ - echo "Downloading Maven 3.0";
+ - wget https://archive.apache.org/dist/maven/binaries/apache-maven-3.0-bin.zip || travis_terminate 1
+ - unzip -qq apache-maven-3.0-bin.zip || travis_terminate 1
+ - export M2_HOME=$PWD/apache-maven-3.0
+ - export PATH=$M2_HOME/bin:$PATH
+ - mvn -version
+ - mvn clean package install -DskipTests -Dgpg.skip
after_failure: cat /home/travis/build/browserstack/browserstack-local-java/target/surefire-reports/*
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000..ddd85cc
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1 @@
+* @browserstack/local-dev
diff --git a/README.md b/README.md
index ae49c68..fd71907 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Add this dependency to your project's POM:
com.browserstack
browserstack-local-java
- 1.0.0
+ 1.1.6
```
@@ -20,20 +20,20 @@ Add this dependency to your project's POM:
```java
import com.browserstack.local.Local;
-# creates an instance of Local
+// creates an instance of Local
Local bsLocal = new Local();
-# replace with your key. You can also set an environment variable - "BROWSERSTACK_ACCESS_KEY".
+// replace with your key. You can also set an environment variable - "BROWSERSTACK_ACCESS_KEY".
HashMap bsLocalArgs = new HashMap();
bsLocalArgs.put("key", "");
-# starts the Local instance with the required arguments
+// starts the Local instance with the required arguments
bsLocal.start(bsLocalArgs);
-# check if BrowserStack local instance is running
+// check if BrowserStack local instance is running
System.out.println(bsLocal.isRunning());
-#stop the Local instance
+// stop the Local instance
bsLocal.stop();
```
@@ -70,6 +70,11 @@ To route all traffic via local(your) machine -
```java
bsLocalArgs.put("forcelocal", "true");
```
+#### Force Proxy
+To route all traffic via the proxy specified.
+```java
+bsLocalArgs.put("forceproxy", "true");
+```
#### Proxy
To use a proxy for local testing -
@@ -85,6 +90,29 @@ bsLocalArgs.put("proxyPort", "8000");
bsLocalArgs.put("proxyUser", "user");
bsLocalArgs.put("proxyPass", "password");
```
+#### Local Proxy
+To use local proxy in local testing -
+
+* localProxyHost: Hostname/IP of proxy, remaining proxy options are ignored if this option is absent
+* localProxyPort: Port for the proxy, defaults to 8081 when -localProxyHost is used
+* localProxyUser: Username for connecting to proxy (Basic Auth Only)
+* localProxyPass: Password for USERNAME, will be ignored if USERNAME is empty or not specified
+
+```java
+bsLocalArgs.put("localProxyHost", "127.0.0.1");
+bsLocalArgs.put("localProxyPort", "8000");
+bsLocalArgs.put("-localProxyUser", "user");
+bsLocalArgs.put("-localProxyPass", "password");
+```
+
+#### PAC (Proxy Auto-Configuration)
+To use PAC (Proxy Auto-Configuration) in local testing -
+
+* pac-file: PAC (Proxy Auto-Configuration) file’s absolute path
+
+```java
+bsLocalArgs.put("-pac-file", "");
+```
#### Local Identifier
If doing simultaneous multiple local testing connections, set this uniquely for different processes -
@@ -107,7 +135,7 @@ To save the logs to the file while running with the '-v' argument, you can speci
To specify the path to file where the logs will be saved -
```java
bsLocalArgs.put("v", "true");
-bsLocalArgs.put("logfile", "/browserstack/logs.txt");
+bsLocalArgs.put("logFile", "/browserstack/logs.txt");
```
## Contribute
diff --git a/pom.xml b/pom.xml
index 65b9088..88c1e3d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,10 +1,9 @@
-
+
4.0.0
com.browserstack
browserstack-local-java
jar
- 1.0.0
+ 1.1.7
browserstack-local-java
Java bindings for BrowserStack Local
@@ -30,31 +29,32 @@
scm:git:git@github.com:browserstack/browserstack-local-java.git
scm:git:git@github.com:browserstack/browserstack-local-java.git
git@github.com:browserstack/browserstack-local-java.git
-
+ HEAD
+
-
- ossrh
- https://oss.sonatype.org/content/repositories/snapshots
-
+
+ central
+ https://central.sonatype.com/api/v1/publisher/deployments/upload/
+
junit
junit
- 4.11
+ 4.13.1
test
- org.apache.commons
+ commons-io
commons-io
- 1.3.2
+ 2.16.1
org.json
json
- 20160212
+ 20231013
@@ -83,14 +83,14 @@
- org.sonatype.plugins
- nexus-staging-maven-plugin
- 1.6.7
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.8.0
true
- ossrh
- https://oss.sonatype.org/
- true
+ central
+ true
+ false
@@ -128,14 +128,14 @@
- org.sonatype.plugins
- nexus-staging-maven-plugin
- 1.6.7
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.8.0
true
- ossrh
- https://oss.sonatype.org/
- false
+ central
+ true
+ false
@@ -143,8 +143,8 @@
maven-compiler-plugin
2.3.2
- 1.5
- 1.5
+ 1.7
+ 1.7
diff --git a/src/main/java/com/browserstack/local/Local.java b/src/main/java/com/browserstack/local/Local.java
index b46210f..09c08f0 100644
--- a/src/main/java/com/browserstack/local/Local.java
+++ b/src/main/java/com/browserstack/local/Local.java
@@ -22,6 +22,8 @@ public class Local {
private LocalProcess proc = null;
+ // Current version of binding package, used for --source option of binary
+ private static final String packageVersion = "1.1.7";
private final Map parameters;
private final Map avoidValueParameters;
@@ -51,12 +53,13 @@ public Local() {
*/
public void start(Map options) throws Exception {
startOptions = options;
+ LocalBinary lb;
if (options.get("binarypath") != null) {
- binaryPath = options.get("binarypath");
+ lb = new LocalBinary(options.get("binarypath"), options.get("key"));
} else {
- LocalBinary lb = new LocalBinary();
- binaryPath = lb.getBinaryPath();
+ lb = new LocalBinary("", options.get("key"));
}
+ binaryPath = lb.getBinaryPath();
makeCommand(options, "start");
@@ -99,6 +102,24 @@ public void stop() throws Exception {
}
}
+ /**
+ * Stops the Local instance specified by the given identifier
+ * @param options Options supplied for the Local instance
+ **/
+ public void stop(Map options) throws Exception {
+ LocalBinary lb;
+ if (options.get("binarypath") != null) {
+ lb = new LocalBinary(options.get("binarypath"), options.get("key"));
+ } else {
+ lb = new LocalBinary("", options.get("key"));
+ }
+ binaryPath = lb.getBinaryPath();
+ makeCommand(options, "stop");
+ proc = runCommand(command);
+ proc.waitFor();
+ pid = 0;
+ }
+
/**
* Checks if Local instance is running
*
@@ -109,6 +130,15 @@ public boolean isRunning() throws Exception {
return isProcessRunning(pid);
}
+ /**
+ * Returns the package version
+ *
+ * @return {String} package version
+ */
+ public static String getPackageVersion() {
+ return packageVersion;
+ }
+
/**
* Creates a list of command-line arguments for the Local instance
*
@@ -119,7 +149,10 @@ private void makeCommand(Map options, String opCode) {
command.add(binaryPath);
command.add("-d");
command.add(opCode);
+ command.add("--key");
command.add(options.get("key"));
+ command.add("--source");
+ command.add("java-" + packageVersion);
for (Map.Entry opt : options.entrySet()) {
String parameter = opt.getKey().trim();
@@ -158,8 +191,14 @@ private boolean isProcessRunning(int pid) throws Exception {
}
else {
//ps exit code 0 if process exists, 1 if it doesn't
+ cmd.add("/bin/sh");
+ cmd.add("-c");
cmd.add("ps");
- cmd.add("-p");
+ cmd.add("-o");
+ cmd.add("pid=");
+ cmd.add("|");
+ cmd.add("grep");
+ cmd.add("-w");
cmd.add(String.valueOf(pid));
}
diff --git a/src/main/java/com/browserstack/local/LocalBinary.java b/src/main/java/com/browserstack/local/LocalBinary.java
index 03e2ca3..bf4542d 100644
--- a/src/main/java/com/browserstack/local/LocalBinary.java
+++ b/src/main/java/com/browserstack/local/LocalBinary.java
@@ -1,21 +1,39 @@
package com.browserstack.local;
import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+
+import org.json.JSONObject;
+
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.File;
+import java.io.FileOutputStream;
import java.net.URL;
+import java.net.URLConnection;
import java.util.regex.Pattern;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.ZipException;
+
+import java.lang.StringBuilder;
class LocalBinary {
- private static final String BIN_URL = "https://s3.amazonaws.com/browserStack/browserstack-local/";
+ private String binaryFileName;
- private String httpPath;
+ private String sourceUrl;
private String binaryPath;
+ private Boolean fallbackEnabled = false;
+
+ private Throwable downloadFailureThrowable = null;
+
+ private String key;
+
private boolean isOSWindows;
private final String orderedPaths[] = {
@@ -24,10 +42,30 @@ class LocalBinary {
System.getProperty("java.io.tmpdir")
};
- LocalBinary() throws LocalException {
+ LocalBinary(String path, String key) throws LocalException {
+ this.key = key;
initialize();
- getBinary();
- checkBinary();
+ downloadAndVerifyBinary(path);
+ }
+
+ private void downloadAndVerifyBinary(String path) throws LocalException {
+ try {
+ if (path != "") {
+ getBinaryOnPath(path);
+ } else {
+ getBinary();
+ }
+ checkBinary();
+ } catch (Throwable e) {
+ if (fallbackEnabled) throw e;
+ File binary_file = new File(binaryPath);
+ if (binary_file.exists()) {
+ binary_file.delete();
+ }
+ fallbackEnabled = true;
+ downloadFailureThrowable = e;
+ downloadAndVerifyBinary(path);
+ }
}
private void initialize() throws LocalException {
@@ -41,12 +79,34 @@ private void initialize() throws LocalException {
binFileName = "BrowserStackLocal-darwin-x64";
} else if (osname.contains("linux")) {
String arch = System.getProperty("os.arch");
- binFileName = "BrowserStackLocal-linux-" + (arch.contains("64") ? "x64" : "ia32");
+ if (arch.contains("64")) {
+ if (isAlpine()) {
+ binFileName = "BrowserStackLocal-alpine";
+ } else {
+ binFileName = "BrowserStackLocal-linux-x64";
+ }
+ } else {
+ binFileName = "BrowserStackLocal-linux-ia32";
+ }
} else {
throw new LocalException("Failed to detect OS type");
}
- httpPath = BIN_URL + binFileName;
+ this.binaryFileName = binFileName;
+ }
+
+ private boolean isAlpine() {
+ String[] cmd = { "/bin/sh", "-c", "grep -w \"NAME\" /etc/os-release" };
+ boolean flag = false;
+
+ try {
+ Process os = Runtime.getRuntime().exec(cmd);
+ BufferedReader stdout = new BufferedReader(new InputStreamReader(os.getInputStream()));
+
+ flag = stdout.readLine().contains("Alpine");
+ } finally {
+ return flag;
+ }
}
private void checkBinary() throws LocalException{
@@ -89,6 +149,14 @@ private boolean validateBinary() throws LocalException{
}
}
+ private void getBinaryOnPath(String path) throws LocalException {
+ binaryPath = path;
+
+ if (!new File(binaryPath).exists()) {
+ downloadBinary(binaryPath, true);
+ }
+ }
+
private void getBinary() throws LocalException {
String destParentDir = getAvailableDirectory();
binaryPath = destParentDir + "/BrowserStackLocal";
@@ -98,7 +166,7 @@ private void getBinary() throws LocalException {
}
if (!new File(binaryPath).exists()) {
- downloadBinary(destParentDir);
+ downloadBinary(destParentDir, false);
}
}
@@ -125,23 +193,76 @@ private boolean makePath(String path) {
}
}
- private void downloadBinary(String destParentDir) throws LocalException {
+ private void fetchSourceUrl() throws LocalException {
+ if ((!fallbackEnabled && sourceUrl != null) || (fallbackEnabled && downloadFailureThrowable == null)) {
+ /* Retry because binary (from any of the endpoints) validation failed */
+ return;
+ }
+
try {
- if (!new File(destParentDir).exists())
- new File(destParentDir).mkdirs();
+ URL url = new URL("https://local.browserstack.com/binary/api/v1/endpoint");
+ URLConnection connection = url.openConnection();
+
+ connection.setDoOutput(true);
+ connection.setRequestProperty("Content-Type", "application/json");
+ connection.setRequestProperty("User-Agent", "browserstack-local-java/" + Local.getPackageVersion());
+ connection.setRequestProperty("Accept", "application/json");
+
+ JSONObject inputParams = new JSONObject();
+ inputParams.put("auth_token", this.key);
+ if (fallbackEnabled) {
+ connection.setRequestProperty("X-Local-Fallback-Cloudflare", "true");
+ inputParams.put("error_message", downloadFailureThrowable.getMessage());
+ }
+ String jsonInputParams = inputParams.toString();
- URL url = new URL(httpPath);
- String source = destParentDir + "/BrowserStackLocal";
- if (isOSWindows) {
- source += ".exe";
+ try (OutputStream os = connection.getOutputStream()) {
+ byte[] input = jsonInputParams.getBytes("utf-8");
+ os.write(input, 0, input.length);
+ }
+
+ try (InputStream is = connection.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is, "utf-8"))) {
+ StringBuilder response = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ response.append(line.trim());
+ }
+ String responseBody = response.toString();
+ JSONObject json = new JSONObject(responseBody);
+ if (json.has("error")) {
+ throw new Exception(json.getString("error"));
+ }
+ this.sourceUrl = json.getJSONObject("data").getString("endpoint");
+ if(fallbackEnabled) downloadFailureThrowable = null;
+ }
+ } catch (Throwable e) {
+ throw new LocalException("Error trying to fetch the source URL: " + e.getMessage());
+ }
+ }
+
+ private void downloadBinary(String destParentDir, Boolean custom) throws LocalException {
+ try {
+ fetchSourceUrl();
+
+ String source = destParentDir;
+ if (!custom) {
+ if (!new File(destParentDir).exists())
+ new File(destParentDir).mkdirs();
+
+ source = destParentDir + "/BrowserStackLocal";
+ if (isOSWindows) {
+ source += ".exe";
+ }
}
+ URL url = new URL(sourceUrl + '/' + binaryFileName);
File f = new File(source);
- FileUtils.copyURLToFile(url, f);
+ newCopyToFile(url, f);
changePermissions(binaryPath);
- } catch (Exception e) {
- throw new LocalException("Error trying to download BrowserStackLocal binary");
+ } catch (Throwable e) {
+ throw new LocalException("Error trying to download BrowserStackLocal binary: " + e.getMessage());
}
}
@@ -155,4 +276,39 @@ private void changePermissions(String path) {
public String getBinaryPath() {
return binaryPath;
}
+
+ private static void newCopyToFile(URL url, File f) throws IOException {
+ URLConnection conn = url.openConnection();
+ conn.setRequestProperty("User-Agent", "browserstack-local-java/" + Local.getPackageVersion());
+ conn.setRequestProperty("Accept-Encoding", "gzip, *");
+ String contentEncoding = conn.getContentEncoding();
+
+ if (contentEncoding == null || !contentEncoding.toLowerCase().contains("gzip")) {
+ customCopyInputStreamToFile(conn.getInputStream(), f, url);
+ return;
+ }
+
+ try (InputStream stream = new GZIPInputStream(conn.getInputStream())) {
+ if (System.getenv().containsKey("BROWSERSTACK_LOCAL_DEBUG_GZIP")) {
+ System.out.println("using gzip in " + conn.getRequestProperty("User-Agent"));
+ }
+
+ customCopyInputStreamToFile(stream, f, url);
+ } catch (ZipException e) {
+ FileUtils.copyURLToFile(url, f);
+ }
+ }
+
+ private static void customCopyInputStreamToFile(InputStream stream, File file, URL url) throws IOException {
+ try {
+ FileUtils.copyInputStreamToFile(stream, file);
+ } catch (Throwable e) {
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ IOUtils.copy(stream, fos);
+ } catch (Throwable th) {
+ FileUtils.copyURLToFile(url, file);
+ }
+ }
+ }
}
+