All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.alexdlaird.ngrok.installer.NgrokInstaller Maven / Gradle / Ivy

/*
 * Copyright (c) 2023 Alex Laird
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.github.alexdlaird.ngrok.installer;

import com.github.alexdlaird.exception.JavaNgrokException;
import com.github.alexdlaird.exception.JavaNgrokInstallerException;
import com.github.alexdlaird.exception.JavaNgrokSecurityException;
import com.github.alexdlaird.ngrok.NgrokClient;
import com.github.alexdlaird.ngrok.conf.JavaNgrokConfig;
import com.google.gson.JsonParseException;
import org.yaml.snakeyaml.Yaml;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import static com.github.alexdlaird.util.StringUtils.isBlank;

/**
 * A helper for downloading and installing the ngrok for the current system.
 *
 * 

Config File

* By default, ngrok will look for its config file in the home * directory’s .ngrok2 folder. We can override this behavior with * {@link JavaNgrokConfig.Builder#withConfigPath(Path)}. * *

Binary Path

* The java-ngrok package manages its own ngrok binary. We can use our ngrok * binary if we want by setting it with {@link JavaNgrokConfig.Builder#withNgrokPath(Path)} and passing that config * to {@link NgrokClient}. */ public class NgrokInstaller { private static final Logger LOGGER = Logger.getLogger(String.valueOf(NgrokInstaller.class)); public static final String MAC = "DARWIN"; public static final String WINDOWS = "WINDOWS"; public static final String LINUX = "LINUX"; public static final String FREEBSD = "FREEBSD"; public static final List UNIX_BINARIES = List.of(MAC, LINUX, FREEBSD); public static final Path DEFAULT_NGROK_PATH = Paths.get(System.getProperty("user.home"), ".ngrok2", NgrokInstaller.getNgrokBin()); public static final Path DEFAULT_CONFIG_PATH = Paths.get(System.getProperty("user.home"), ".ngrok2", "ngrok.yml"); private static final List VALID_LOG_LEVELS = List.of("info", "debug"); private final Yaml yaml = new Yaml(); private final Map> configCache = new HashMap<>(); /** * Get the ngrok executable for the current system. * * @return The name of the ngrok executable. */ public static String getNgrokBin() { final String system = getSystem(); if (UNIX_BINARIES.contains(system)) { return "ngrok"; } else { return "ngrok.exe"; } } /** * See {@link #installDefaultConfig(Path, Map, NgrokVersion)}. */ public void installDefaultConfig(final Path configPath, final Map data) { installDefaultConfig(configPath, data, NgrokVersion.V3); } /** * Install the default ngrok config. If a config is not already present for the given path, * create one. * * @param configPath The path to where the ngrok config should be installed. * @param data A map of things to add to the default config. */ public void installDefaultConfig(final Path configPath, final Map data, final NgrokVersion ngrokVersion) { try { Files.createDirectories(configPath.getParent()); if (!Files.exists(configPath)) { Files.createFile(configPath); } final Map config = getNgrokConfig(configPath, false, ngrokVersion); config.putAll(getDefaultConfig(ngrokVersion)); config.putAll(data); validateConfig(config); LOGGER.fine(String.format("Installing default config to %s ...", configPath)); final FileOutputStream out = new FileOutputStream(configPath.toFile()); final StringWriter writer = new StringWriter(); yaml.dump(config, writer); out.write(writer.toString().getBytes()); out.close(); } catch (IOException e) { throw new JavaNgrokInstallerException(String.format("An error while installing the default ngrok config to %s.", configPath), e); } } /** * See {@link #installNgrok(Path, NgrokVersion)}. */ public void installNgrok(final Path ngrokPath) { installNgrok(ngrokPath, NgrokVersion.V3); } /** * Download and install the latest ngrok for the current system, overwriting any existing contents * at the given path. * * @param ngrokPath The path to where the ngrok binary will be downloaded. * @param ngrokVersion The major ngrok version to install. */ public void installNgrok(final Path ngrokPath, final NgrokVersion ngrokVersion) { final NgrokCDNUrl ngrokCDNUrl = getNgrokCDNUrl(ngrokVersion); LOGGER.fine(String.format("Installing ngrok %s to %s%s ...", ngrokVersion, ngrokPath, Files.exists(ngrokPath) ? ", overwriting" : "")); final Path ngrokZip = Paths.get(ngrokPath.getParent().toString(), "ngrok.zip"); downloadFile(ngrokCDNUrl.getUrl(), ngrokZip); installNgrokZip(ngrokZip, ngrokPath); } /** * See {@link #getNgrokCDNUrl}. */ public NgrokCDNUrl getNgrokCDNUrl() { return getNgrokCDNUrl(NgrokVersion.V3); } /** * Determine the ngrok CDN URL for the current OS and architecture. * * @param ngrokVersion The major version of ngrok to install. * @return The ngrok CDN URL. */ public NgrokCDNUrl getNgrokCDNUrl(NgrokVersion ngrokVersion) { final String arch = getArch(); final String system = getSystem(); final String plat = String.format("%s_%s", system, arch); LOGGER.fine(String.format("Platform to download: %s", plat)); if (ngrokVersion == NgrokVersion.V2) { return NgrokV2CDNUrl.valueOf(plat); } else { return NgrokV3CDNUrl.valueOf(plat); } } /** * Validate that the config file at the given path is valid for ngrok and java-ngrok. * * @param configPath The config path to validate. */ public void validateConfig(final Path configPath) { final Map config = getNgrokConfig(configPath); validateConfig(config); } /** * Validate that the given map of config items are valid for ngrok and java-ngrok. * * @param data A map of things to be validated as config items. */ public void validateConfig(final Map data) { if (data.getOrDefault("web_addr", "127.0.0.1:4040").equals("false")) { throw new JavaNgrokException("\"web_addr\" cannot be false, as the ngrok API is a dependency for java-ngrok"); } if (data.getOrDefault("log_format", "term").equals("json")) { throw new JavaNgrokException("\"log_format\" must be \"term\" to be compatible with java-ngrok"); } if (!VALID_LOG_LEVELS.contains((String) data.getOrDefault("log_level", "info"))) { throw new JavaNgrokException("\"log_level\" must be \"info\" to be compatible with java-ngrok"); } } /** * Parse the name fo the OS from system properties and return a friendly name. * * @return The friendly name of the OS. */ public static String getSystem() { final String os = System.getProperty("os.name").replaceAll(" ", "").toLowerCase(); if (os.startsWith("mac")) { return MAC; } else if (os.startsWith("windows") || os.contains("cygwin")) { return WINDOWS; } else if (os.startsWith("linux")) { return LINUX; } else if (os.startsWith("freebsd")) { return FREEBSD; } else { throw new JavaNgrokInstallerException(String.format("Unknown os.name: %s", os)); } } /** * Get the ngrok config from the given path. * * @param configPath The ngrok config path to read. * @param useCache Use the cached version of the config (if populated). * @return A map of the ngrok config. */ public Map getNgrokConfig(final Path configPath, final boolean useCache, final NgrokVersion ngrokVersion) { final String key = configPath.toString(); if (!configCache.containsKey(key) || !useCache) { try { final String config = Files.readString(configPath); if (isBlank(config)) { configCache.put(key, getDefaultConfig(ngrokVersion)); } else { configCache.put(key, yaml.load(config)); } } catch (IOException | JsonParseException e) { throw new JavaNgrokInstallerException(String.format("An error occurred while parsing the config file: %s", configPath), e); } } return configCache.get(key); } /** * Get the default config params for the given major version of ngrok. * * @param ngrokVersion The major version of ngrok installed. * @return The default config. */ public Map getDefaultConfig(final NgrokVersion ngrokVersion) { if (ngrokVersion == NgrokVersion.V2) { return new HashMap<>(); } else { final HashMap config = new HashMap<>(); config.put("version", "2"); config.put("region", "us"); return config; } } /** * See {@link #getNgrokConfig(Path, boolean, NgrokVersion)}. */ public Map getNgrokConfig(final Path configPath) { return getNgrokConfig(configPath, true); } /** * See {@link #getNgrokConfig(Path, boolean, NgrokVersion)}. */ public Map getNgrokConfig(final Path configPath, final boolean useCache) { return getNgrokConfig(configPath, useCache, NgrokVersion.V3); } private void installNgrokZip(final Path zipPath, final Path ngrokPath) { try { final Path dir = ngrokPath.getParent(); LOGGER.fine(String.format("Extracting ngrok binary from %s to %s ...", zipPath, ngrokPath)); Files.createDirectories(dir); final byte[] buffer = new byte[1024]; final ZipInputStream in = new ZipInputStream(new FileInputStream(zipPath.toFile())); ZipEntry zipEntry; while ((zipEntry = in.getNextEntry()) != null) { final Path file = Paths.get(dir.toString(), zipEntry.getName()); if (!file.normalize().startsWith(dir)) { throw new JavaNgrokSecurityException("Bad zip entry, paths don't match"); } if (zipEntry.isDirectory()) { if (!Files.isDirectory(file)) { Files.createDirectories(file); } } else { final Path parent = file.getParent(); if (!Files.isDirectory(parent)) { Files.createDirectories(parent); } final FileOutputStream out = new FileOutputStream(file.toFile()); int len; while ((len = in.read(buffer)) > 0) { out.write(buffer, 0, len); } out.close(); } } in.closeEntry(); in.close(); if (ngrokPath.getFileSystem().supportedFileAttributeViews().contains("posix")) { final Set perms = Files.readAttributes(ngrokPath, PosixFileAttributes.class).permissions(); perms.add(PosixFilePermission.OWNER_EXECUTE); perms.add(PosixFilePermission.GROUP_EXECUTE); perms.add(PosixFilePermission.OTHERS_EXECUTE); Files.setPosixFilePermissions(ngrokPath, perms); } } catch (IOException e) { throw new JavaNgrokInstallerException("An error occurred while unzipping ngrok.", e); } } private void downloadFile(final String url, final Path dest) { try { Files.createDirectories(dest.getParent()); LOGGER.fine(String.format("Download ngrok from %s ...", url)); final InputStream in = new URL(url).openStream(); Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { throw new JavaNgrokInstallerException(String.format("An error occurred while downloading the file from %s.", url), e); } } private String getArch() { final String archProperty = System.getProperty("os.arch"); final StringBuilder arch = new StringBuilder(); if (archProperty.contains("x86_64")) { arch.append("x86_64"); } else { arch.append("i386"); } if (archProperty.startsWith("arm") || archProperty.startsWith("aarch64")) { arch.append("_arm"); } return arch.toString(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy