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); + } + } + } } +