
com.github.alexdlaird.ngrok.NgrokClient 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;
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.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.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.
*
*
* 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.
*
*
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 = "2.2.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 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.
*/
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 (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 (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();
}
applyCloudEdgeToTunnel(tunnel);
currentTunnels.put(tunnel.getPublicUrl(), tunnel);
return tunnel;
}
private void applyCloudEdgeToTunnel(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