
com.github.alexdlaird.ngrok.NgrokClient Maven / Gradle / Ivy
/*
* Copyright (c) 2021 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;
import com.github.alexdlaird.exception.JavaNgrokException;
import com.github.alexdlaird.exception.JavaNgrokHTTPException;
import com.github.alexdlaird.http.DefaultHttpClient;
import com.github.alexdlaird.http.HttpClient;
import com.github.alexdlaird.http.HttpClientException;
import com.github.alexdlaird.http.Response;
import com.github.alexdlaird.ngrok.conf.JavaNgrokConfig;
import com.github.alexdlaird.ngrok.installer.NgrokInstaller;
import com.github.alexdlaird.ngrok.process.NgrokProcess;
import com.github.alexdlaird.ngrok.protocol.BindTls;
import com.github.alexdlaird.ngrok.protocol.CreateTunnel;
import com.github.alexdlaird.ngrok.protocol.Proto;
import com.github.alexdlaird.ngrok.protocol.Tunnel;
import com.github.alexdlaird.ngrok.protocol.Tunnels;
import com.github.alexdlaird.ngrok.protocol.Version;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
/**
* A client for interacting with ngrok, its binary, and its APIs.
* Can be configured with {@link JavaNgrokConfig}.
*
* Open a Tunnel
* To open a tunnel, use the {@link NgrokClient#connect(CreateTunnel) NgrokClient.connect()} method, which returns a {@link Tunnel}, and
* this returned object has a reference to the public URL generated by ngrok
in its
* {@link Tunnel#getPublicUrl()} method.
*
*
* final NgrokClient ngrokClient = new NgrokClient.Builder().build();
*
* // Open a HTTP tunnel on the default port 80
* // <Tunnel: "http://<public_sub>.ngrok.io" -> "http://localhost:80">
* final Tunnel httpTunnel = ngrokClient.connect();
*
* // Open a SSH tunnel
* // <Tunnel: "tcp://0.tcp.ngrok.io:12345" -> "localhost:22">
* final CreateTunnel sshCreateTunnel = new CreateTunnel.Builder()
* .withProto(Proto.TCP)
* .withAddr(22)
* .build();
* final Tunnel sshTunnel = ngrokClient.connect(sshCreateTunnel);
*
* // Open a tunnel to MySQL with a Reserved TCP Address
* // <NgrokTunnel: "tcp://1.tcp.ngrok.io:12345" -> "localhost:3306">
* final CreateTunnel mysqlCreateTunnel = new CreateTunnel.Builder()
* .withProto(Proto.TCP)
* .withAddr(3306)
* .withRemoteAddr("1.tcp.ngrok.io:12345")
* .build();
* final Tunnel mysqlTunnel = ngrokClient.connect(mysqlCreateTunnel);
*
* // Open a tunnel to a local file server
* // <NgrokTunnel: "http://<public_sub>.ngrok.io" -> "file:///">
* final CreateTunnel fileserverCreateTunnel = new CreateTunnel.Builder()
* .withAddr("file:///)
* .build();
* final Tunnel fileserverTunnel = ngrokClient.connect(fileserverCreateTunnel);
*
*
* The {@link NgrokClient#connect(CreateTunnel) NgrokClient.connect()} method can also take a {@link CreateTunnel}
* (which can be built through {@link CreateTunnel.Builder its Builder}) that allows us to pass additional properties
* that are supported by ngrok.
*
* Note: ngrok
's default behavior for http
when no additional properties
* are passed is to open two tunnels, one http
and one https
. This method will
* return a reference to the http
tunnel in this case. If only a single tunnel is needed, call
* {@link CreateTunnel.Builder#withBindTls(BindTls)} with {@link BindTls#TRUE} and a reference to the
* https
tunnel will be returned.
*
*
Get Active Tunnels
* It can be useful to ask the ngrok
client what tunnels are currently open. This can be accomplished
* with the {@link NgrokClient#getTunnels()} method, which returns a list of {@link Tunnel} objects.
*
*
* [<Tunnel: "http://<public_sub>.ngrok.io" -> "http://localhost:80">]
* final List<Tunnel> tunnels = ngrokClient.getTunnels();
*
*
* Close a Tunnel
* All open tunnels will automatically be closed when the Java process terminates, but we can also close them
* manually with {@link NgrokClient#disconnect(String)}.
*
*
* // The Tunnel returned from methods like connect(), getTunnels(), etc. contains the public URL
* ngrokClient.disconnect(publicUrl);
*
*
* Integration Examples
* java-ngrok
is useful in any number of integrations, for instance to test locally without having to
* deploy or configure. Here are some common usage examples.
*
*
*/
public class NgrokClient {
private static final Logger LOGGER = Logger.getLogger(String.valueOf(NgrokClient.class));
private static final String VERSION = "1.4.0";
private final JavaNgrokConfig javaNgrokConfig;
private final NgrokProcess ngrokProcess;
private final HttpClient httpClient;
private final Map currentTunnels = new HashMap<>();
private NgrokClient(final Builder builder) {
this.javaNgrokConfig = builder.javaNgrokConfig;
this.ngrokProcess = builder.ngrokProcess;
this.httpClient = builder.httpClient;
}
/**
* Establish a new ngrok
tunnel for the tunnel definition, returning an object representing
* the connected tunnel.
*
* If ngrok
is not running, calling this method will first start a process with
* {@link JavaNgrokConfig}.
*
* Note: ngrok
's default behavior for http
when no additional properties
* are passed is to open two tunnels, one http
and one https
. This method will
* return a reference to the http
tunnel in this case. If only a single tunnel is needed, call
* {@link CreateTunnel.Builder#withBindTls(BindTls)} with {@link BindTls#TRUE} and a reference to the
* https
tunnel will be returned.
*
* @param createTunnel The tunnel definition.
* @return The created Tunnel.
*/
public Tunnel connect(final CreateTunnel createTunnel) {
ngrokProcess.start();
final CreateTunnel finalTunnel = interpolateTunnelDefinition(createTunnel);
LOGGER.info(String.format("Opening tunnel named: %s", finalTunnel.getName()));
final Response response;
try {
response = httpClient.post(String.format("%s/api/tunnels", ngrokProcess.getApiUrl()), finalTunnel, Tunnel.class);
} catch (HttpClientException e) {
throw new JavaNgrokHTTPException(String.format("An error occurred when POSTing to create the tunnel %s.", finalTunnel.getName()),
e, e.getUrl(), e.getStatusCode(), e.getBody());
}
final Tunnel tunnel;
if (finalTunnel.getProto() == Proto.HTTP && finalTunnel.getBindTls() == BindTls.BOTH) {
try {
final Response getResponse = httpClient.get(ngrokProcess.getApiUrl() + response.getBody().getUri() + "%20%28http%29", Tunnel.class);
tunnel = getResponse.getBody();
} catch (HttpClientException e) {
throw new JavaNgrokHTTPException(String.format("An error occurred when GETing the HTTP tunnel %s.", response.getBody().getName()),
e, e.getUrl(), e.getStatusCode(), e.getBody());
}
} else {
tunnel = response.getBody();
}
currentTunnels.put(tunnel.getPublicUrl(), tunnel);
return tunnel;
}
/**
* See {@link #connect(CreateTunnel)}.
*/
public Tunnel connect() {
return connect(new CreateTunnel.Builder().build());
}
/**
* Disconnect the ngrok
tunnel for the given URL, if open.
*
* @param publicUrl The public URL of the tunnel to disconnect.
*/
public void disconnect(final String publicUrl) {
// If ngrok is not running, there are no tunnels to disconnect
if (!ngrokProcess.isRunning()) {
return;
}
if (!currentTunnels.containsKey(publicUrl)) {
getTunnels();
// One more check, if the given URL is still not in the list of tunnels, it is not active
if (!currentTunnels.containsKey(publicUrl)) {
return;
}
}
final Tunnel tunnel = currentTunnels.get(publicUrl);
ngrokProcess.start();
LOGGER.info(String.format("Disconnecting tunnel: %s", tunnel.getPublicUrl()));
try {
httpClient.delete(ngrokProcess.getApiUrl() + tunnel.getUri());
} catch (HttpClientException e) {
throw new JavaNgrokHTTPException(String.format("An error occurred when DELETing the tunnel %s.", publicUrl),
e, e.getUrl(), e.getStatusCode(), e.getBody());
}
}
/**
* Get a list of active ngrok
tunnels.
*
* If ngrok
is not running, calling this method will first start a process with
* {@link JavaNgrokConfig}.
*
* @return The active ngrok
tunnels.
*/
public List getTunnels() {
ngrokProcess.start();
try {
final Response response = httpClient.get(String.format("%s/api/tunnels", ngrokProcess.getApiUrl()), Tunnels.class);
currentTunnels.clear();
for (final Tunnel tunnel : response.getBody().getTunnels()) {
currentTunnels.put(tunnel.getPublicUrl(), tunnel);
}
return new ArrayList<>(currentTunnels.values());
} catch (HttpClientException e) {
throw new JavaNgrokHTTPException("An error occurred when GETing the tunnels.", e, e.getUrl(),
e.getStatusCode(), e.getBody());
}
}
/**
* Get the latest metrics for the given {@link Tunnel} and update its metrics
attribute.
*
* @param tunnel The Tunnel to update.
*/
public void refreshMetrics(final Tunnel tunnel) {
Response latestTunnel = httpClient.get(String.format("%s%s", ngrokProcess.getApiUrl(), tunnel.getUri()), Tunnel.class);
if (isNull(latestTunnel.getBody().getMetrics()) || latestTunnel.getBody().getMetrics().isEmpty()) {
throw new JavaNgrokException("The ngrok API did not return \"metrics\" in the response");
}
tunnel.setMetrics(latestTunnel.getBody().getMetrics());
}
/**
* Set the ngrok
auth token in the config file, enabling authenticated features (for instance,
* more concurrent tunnels, custom subdomains, etc.).
*
* @param authToken The auth token.
*/
public void setAuthToken(final String authToken) {
ngrokProcess.setAuthToken(authToken);
}
/**
* Update ngrok
, if an update is available.
*/
public void update() {
ngrokProcess.update();
}
/**
* Get the ngrok
and java-ngrok
version.
*
* @return The versions.
*/
public Version getVersion() {
final String ngrokVersion = ngrokProcess.getVersion();
return new Version(ngrokVersion, VERSION);
}
/**
* Get the java-ngrok
to use when interacting with the ngrok
binary.
*/
public JavaNgrokConfig getJavaNgrokConfig() {
return javaNgrokConfig;
}
/**
* Get the class used to manage the ngrok
binary.
*/
public NgrokProcess getNgrokProcess() {
return ngrokProcess;
}
/**
* Get the class used to make HTTP requests to ngrok
's APIs.
*/
public HttpClient getHttpClient() {
return httpClient;
}
private CreateTunnel interpolateTunnelDefinition(final CreateTunnel createTunnel) {
final CreateTunnel.Builder createTunnelBuilder = new CreateTunnel.Builder(createTunnel);
final Map config;
if (Files.exists(javaNgrokConfig.getConfigPath())) {
config = ngrokProcess.getNgrokInstaller().getNgrokConfig(javaNgrokConfig.getConfigPath());
} else {
config = Collections.emptyMap();
}
final String name;
final Map tunnelDefinitions = (Map) config.getOrDefault("tunnels", Collections.emptyMap());
if (isNull(createTunnel.getName()) && tunnelDefinitions.containsKey("java-ngrok-default")) {
name = "java-ngrok-default";
createTunnelBuilder.withName(name);
} else {
name = createTunnel.getName();
}
if (nonNull(name) && tunnelDefinitions.containsKey(name)) {
createTunnelBuilder.withTunnelDefinition((Map) tunnelDefinitions.get(name));
}
return createTunnelBuilder.build();
}
/**
* Builder for a {@link NgrokClient}, see docs for that class for example usage.
*/
public static class Builder {
private JavaNgrokConfig javaNgrokConfig;
private NgrokInstaller ngrokInstaller;
private NgrokProcess ngrokProcess;
private HttpClient httpClient;
/**
* The java-ngrok
to use when interacting with the ngrok
binary.
*/
public Builder withJavaNgrokConfig(final JavaNgrokConfig javaNgrokConfig) {
this.javaNgrokConfig = javaNgrokConfig;
return this;
}
/**
* The class used to download and install ngrok
. Only needed if
* {@link #withNgrokProcess(NgrokProcess)} is not called.
*/
public Builder withNgrokInstaller(final NgrokInstaller ngrokInstaller) {
this.ngrokInstaller = ngrokInstaller;
return this;
}
/**
* The class used to manage the ngrok
binary.
*/
public Builder withNgrokProcess(final NgrokProcess ngrokProcess) {
this.ngrokProcess = ngrokProcess;
return this;
}
/**
* The class used to make HTTP requests to ngrok
's APIs.
*/
public Builder withHttpClient(final HttpClient httpClient) {
this.httpClient = httpClient;
return this;
}
public NgrokClient build() {
if (isNull(javaNgrokConfig)) {
javaNgrokConfig = new JavaNgrokConfig.Builder().build();
}
if (isNull(ngrokInstaller)) {
ngrokInstaller = new NgrokInstaller();
}
if (isNull(ngrokProcess)) {
ngrokProcess = new NgrokProcess(javaNgrokConfig, ngrokInstaller);
}
if (isNull(httpClient)) {
httpClient = new DefaultHttpClient.Builder().build();
}
return new NgrokClient(this);
}
}
}