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

com.github.alexdlaird.ngrok.NgrokClient Maven / Gradle / Ivy

There is a newer version: 2.3.1
Show newest version
/*
 * Copyright (c) 2021-2024 Alex Laird
 *
 * SPDX-License-Identifier: MIT
 */

package com.github.alexdlaird.ngrok;

import com.github.alexdlaird.exception.JavaNgrokException;
import com.github.alexdlaird.exception.JavaNgrokHTTPException;
import com.github.alexdlaird.exception.JavaNgrokSecurityException;
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.conf.JavaNgrokVersion;
import com.github.alexdlaird.ngrok.installer.NgrokInstaller;
import com.github.alexdlaird.ngrok.installer.NgrokVersion;
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.Comparator;
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}.
 * 

Basic Usage

*

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: "https://<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 named tunnel from the config file
 * final CreateTunnel createNamedTunnel = new CreateTunnel.Builder()
 *         .withName("my_tunnel_name")
 *         .build();
 * final Tunnel namedTunnel = ngrokClient.connect(createNamedTunnel);
 * 
* *

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. * * *

java-ngrok is compatible with ngrok v2 and v3, but by default it will install v3. To * install v2 instead, set the version with {@link JavaNgrokConfig.Builder#withNgrokVersion(NgrokVersion)} and * {@link CreateTunnel.Builder#withNgrokVersion(NgrokVersion)}. * *

Note: ngrok v2'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. *

ngrok's Edge

* To use ngrok's Edges with * java-ngrok, first configure an * Edge on ngrok's dashboard (with at least one Endpoint mapped to the Edge), and define a labeled * tunnel in the ngrok config file that points to the Edge. * *

 * tunnels:
 *   some-edge-tunnel:
 *     labels:
 *       - edge=my_edge_id
 *     addr: http://localhost:80
 * 
* To start a labeled tunnel in java-ngrok, set {@link CreateTunnel.Builder#withName(String)}. * *

 * final NgrokClient ngrokClient = new NgrokClient.Builder().build();
 *
 * // Open a named tunnel from the config file
 * final CreateTunnel createNamedTunnel = new CreateTunnel.Builder()
 *         .withName("some-edge-tunnel")
 *         .build();
 * final Tunnel namedTunnel = ngrokClient.connect(createNamedTunnel);
 * 
* Once an Edge tunnel is started, it can be managed through * ngrok's dashboard. *

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: "https://<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);
 * 
*

Expose Other Services

* Using ngrok we can expose any number of non-HTTP services, for instances databases, game servers, etc. * This can be accomplished by using java-ngrok to open a tcp tunnel to the desired service. * *

 * final NgrokClient ngrokClient = new NgrokClient.Builder().build();
 *
 * // 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);
 * 
* *

We can also serve up local directories via * ngrok’s built-in * fileserver. * *

 * final NgrokClient ngrokClient = new NgrokClient.Builder().build();
 *
 * // Open a tunnel to a local file server
 * // <NgrokTunnel: "https://<public_sub>.ngrok.io" -> "file:///">
 * final CreateTunnel fileserverCreateTunnel = new CreateTunnel.Builder()
 *         .withAddr("file:///)
 *         .build();
 * final Tunnel fileserverTunnel = ngrokClient.connect(fileserverCreateTunnel);
 * 
*

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 final Map currentTunnels = new HashMap<>(); private final String javaNgrokVersion; private final JavaNgrokConfig javaNgrokConfig; private final NgrokProcess ngrokProcess; private final HttpClient httpClient; private NgrokClient(final Builder builder) { this.javaNgrokVersion = builder.javaNgrokVersion; this.javaNgrokConfig = builder.javaNgrokConfig; this.ngrokProcess = builder.ngrokProcess; this.httpClient = builder.httpClient; } /** * Establish a new ngrok tunnel for the Tunnel creation request, returning an object representing the * connected tunnel. * *

If a tunnel definition in ngrok's config file matches the given * {@link CreateTunnel.Builder#withName(String)}, it will be loaded and used to start the tunnel. When * {@link CreateTunnel.Builder#withName(String)} is not set and a "java-ngrok-default" tunnel definition exists in * ngrok's config, it will be loaded and use. Any properties defined on {@link CreateTunnel} will * override properties from the loaded tunnel definition. * *

If ngrok is not installed at {@link JavaNgrokConfig}'s ngrokPath, calling this * method will first download and install ngrok. * *

java-ngrok is compatible with ngrok v2 and v3, but by default it will install v2. * To install v3 instead, set the version with {@link JavaNgrokConfig.Builder#withNgrokVersion(NgrokVersion)} and * {@link CreateTunnel.Builder#withNgrokVersion(NgrokVersion)}. * *

If ngrok is not running, calling this method will first start a process with * {@link JavaNgrokConfig}. * *

Note: ngrok v2'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. * @throws JavaNgrokException The tunnel definition was invalid, or response was incompatible with * java-ngrok. * @throws JavaNgrokHTTPException An HTTP error occurred communicating with the ngrok API. * @throws JavaNgrokSecurityException The URL was not supported. */ 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 (final 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 (javaNgrokConfig.getNgrokVersion() == NgrokVersion.V2 && 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 (final 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(); } applyEdgeToTunnel(tunnel); currentTunnels.put(tunnel.getPublicUrl(), tunnel); return tunnel; } /** * See {@link #connect(CreateTunnel)}. */ public Tunnel connect() { return connect(new CreateTunnel.Builder().withNgrokVersion(javaNgrokConfig.getNgrokVersion()).build()); } /** * Disconnect the ngrok tunnel for the given URL, if open. * * @param publicUrl The public URL of the tunnel to disconnect. * @throws JavaNgrokHTTPException An HTTP error occurred communicating with the ngrok API. * @throws JavaNgrokSecurityException The URL was not supported. */ 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 (final 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. * @throws JavaNgrokException The response was invalid or not compatible with java-ngrok. * @throws JavaNgrokHTTPException An HTTP error occurred communicating with the ngrok API. * @throws JavaNgrokSecurityException The URL was not supported. */ 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()) { applyEdgeToTunnel(tunnel); currentTunnels.put(tunnel.getPublicUrl(), tunnel); } final List sortedTunnels = new ArrayList<>(currentTunnels.values()); sortedTunnels.sort(Comparator.comparing(Tunnel::getProto)); return List.of(sortedTunnels.toArray(new Tunnel[]{})); } catch (final 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. * @throws JavaNgrokException The API did not return metrics. * @throws JavaNgrokSecurityException The URL was not supported. */ 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()); } /** * Terminate the ngrok processes, if running. This method will not block, it will just issue a kill * request. */ public void kill() { ngrokProcess.stop(); currentTunnels.clear(); } /** * 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, javaNgrokVersion); } /** * 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 void applyEdgeToTunnel(final Tunnel tunnel) { if ((isNull(tunnel.getPublicUrl()) || tunnel.getPublicUrl().isEmpty()) && nonNull(javaNgrokConfig.getApiKey()) && nonNull(tunnel.getId())) { final Map ngrokApiHeaders = Map.of( "Authorization", String.format("Bearer %s", javaNgrokConfig.getApiKey()), "Ngrok-Version", "2"); final Response tunnelResponse = httpClient.get(String.format("https://api.ngrok.com/tunnels/%s", tunnel.getId()), List.of(), ngrokApiHeaders, Map.class); if (!tunnelResponse.getBody().containsKey("labels") || !(tunnelResponse.getBody().get("labels") instanceof Map) || !((Map) tunnelResponse.getBody().get("labels")).containsKey("edge")) { throw new JavaNgrokException(String.format("Tunnel %s does not have 'labels', use a Tunnel " + "configured on an Edge.", tunnel.getId())); } final String edge = (String) ((Map) tunnelResponse.getBody().get("labels")).get("edge"); final String edgesPrefix; if (edge.startsWith("edghts_")) { edgesPrefix = "https"; } else if (edge.startsWith("edgtcp")) { edgesPrefix = "tcp"; } else if (edge.startsWith("edgtls")) { edgesPrefix = "tls"; } else { throw new JavaNgrokException(String.format("Unknown Edge prefix: %s.", edge)); } final Response edgeResponse = httpClient.get(String.format("https://api.ngrok.com/edges/%s/%s", edgesPrefix, edge), List.of(), ngrokApiHeaders, Map.class); if (!edgeResponse.getBody().containsKey("hostports") || !(edgeResponse.getBody().get("hostports") instanceof List) || ((List) edgeResponse.getBody().get("hostports")).isEmpty()) { throw new JavaNgrokException(String.format("No Endpoint is attached to your Edge %s, " + "login to the ngrok dashboard to attach an Endpoint to " + "your Edge first.", edge)); } tunnel.setPublicUrl(String.format("%s://%s", edgesPrefix, ((List) edgeResponse.getBody().get("hostports")).get(0))); tunnel.setProto(edgesPrefix); } } 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 = ngrokProcess.getNgrokInstaller().getDefaultConfig(javaNgrokConfig.getNgrokVersion()); } final String name; final Map tunnelDefinitions = (Map) config.getOrDefault("tunnels", Map.of()); 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)) { if (((Map) tunnelDefinitions.get(name)).containsKey("labels") && isNull(javaNgrokConfig.getApiKey())) { throw new JavaNgrokException("'JavaNgrokConfig.apiKey' must be set when 'labels' is " + "on the tunnel definition."); } 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 String javaNgrokVersion; 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; } /** * Build the {@link NgrokClient}. */ public NgrokClient build() { javaNgrokVersion = JavaNgrokVersion.getInstance().getVersion(); 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); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy