mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 13:16:44 +00:00
repackage http client as tern library dependency
This commit is contained in:
parent
d49d5967b2
commit
46034b8f11
26 changed files with 10 additions and 1681 deletions
|
@ -122,7 +122,7 @@ dependencies {
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
||||||
implementation('org.eclipse.jetty:jetty-client:9.4.54.v20240208')
|
implementation('com.sparrowwallet:tern:1.0.2')
|
||||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||||
implementation('org.apache.commons:commons-lang3:3.7')
|
implementation('org.apache.commons:commons-lang3:3.7')
|
||||||
|
@ -158,7 +158,7 @@ processResources {
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson"]
|
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson", "--add-reads=org.flywaydb.core=java.desktop"]
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
|
import com.sparrowwallet.tern.http.client.HttpResponseException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ package com.sparrowwallet.sparrow.net;
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
|
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
|
||||||
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
|
import com.sparrowwallet.tern.http.client.HttpResponseException;
|
||||||
import javafx.concurrent.ScheduledService;
|
import javafx.concurrent.ScheduledService;
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
|
|
|
@ -1,50 +1,12 @@
|
||||||
package com.sparrowwallet.sparrow.net;
|
package com.sparrowwallet.sparrow.net;
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort;
|
import com.google.common.net.HostAndPort;
|
||||||
import com.sparrowwallet.sparrow.net.http.client.AsyncUtil;
|
|
||||||
import com.sparrowwallet.sparrow.net.http.client.HttpUsage;
|
|
||||||
import com.sparrowwallet.sparrow.net.http.client.IHttpClient;
|
|
||||||
import com.sparrowwallet.sparrow.net.http.client.JettyHttpClientService;
|
|
||||||
import io.reactivex.Observable;
|
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
|
|
||||||
import java.util.Map;
|
public class HttpClientService extends com.sparrowwallet.tern.http.client.HttpClientService {
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class HttpClientService extends JettyHttpClientService {
|
|
||||||
private static final int REQUEST_TIMEOUT = 120000;
|
|
||||||
|
|
||||||
public HttpClientService(HostAndPort torProxy) {
|
public HttpClientService(HostAndPort torProxy) {
|
||||||
super(REQUEST_TIMEOUT, new HttpProxySupplier(torProxy));
|
super(new HttpProxySupplier(torProxy));
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T requestJson(String url, Class<T> responseType, Map<String, String> headers) throws Exception {
|
|
||||||
return getHttpClient(HttpUsage.DEFAULT).getJson(url, responseType, headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> Observable<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body) {
|
|
||||||
return getHttpClient(HttpUsage.DEFAULT).postJson(url, responseType, headers, body).toObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String postString(String url, Map<String, String> headers, String contentType, String content) throws Exception {
|
|
||||||
IHttpClient httpClient = getHttpClient(HttpUsage.DEFAULT);
|
|
||||||
return AsyncUtil.getInstance().blockingGet(httpClient.postString(url, headers, contentType, content)).get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public HostAndPort getTorProxy() {
|
|
||||||
return getHttpProxySupplier().getTorProxy();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTorProxy(HostAndPort torProxy) {
|
|
||||||
//Ensure all http clients are shutdown first
|
|
||||||
stop();
|
|
||||||
getHttpProxySupplier()._setTorProxy(torProxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public HttpProxySupplier getHttpProxySupplier() {
|
|
||||||
return (HttpProxySupplier)super.getHttpProxySupplier();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ShutdownService extends Service<Boolean> {
|
public static class ShutdownService extends Service<Boolean> {
|
||||||
|
|
|
@ -1,44 +1,10 @@
|
||||||
package com.sparrowwallet.sparrow.net;
|
package com.sparrowwallet.sparrow.net;
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort;
|
import com.google.common.net.HostAndPort;
|
||||||
import com.sparrowwallet.sparrow.net.http.client.HttpProxy;
|
|
||||||
import com.sparrowwallet.sparrow.net.http.client.HttpProxyProtocol;
|
|
||||||
import com.sparrowwallet.sparrow.net.http.client.HttpUsage;
|
|
||||||
import com.sparrowwallet.sparrow.net.http.client.IHttpProxySupplier;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class HttpProxySupplier implements IHttpProxySupplier {
|
|
||||||
private HostAndPort torProxy;
|
|
||||||
private HttpProxy httpProxy;
|
|
||||||
|
|
||||||
|
public class HttpProxySupplier extends com.sparrowwallet.tern.http.client.HttpProxySupplier {
|
||||||
public HttpProxySupplier(HostAndPort torProxy) {
|
public HttpProxySupplier(HostAndPort torProxy) {
|
||||||
this.torProxy = torProxy;
|
super(torProxy);
|
||||||
this.httpProxy = computeHttpProxy(torProxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpProxy computeHttpProxy(HostAndPort hostAndPort) {
|
|
||||||
if (hostAndPort == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new HttpProxy(HttpProxyProtocol.SOCKS, hostAndPort.getHost(), hostAndPort.getPort());
|
|
||||||
}
|
|
||||||
|
|
||||||
public HostAndPort getTorProxy() {
|
|
||||||
return torProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
// shouldnt call directly but use httpClientService.setTorProxy()
|
|
||||||
public void _setTorProxy(HostAndPort hostAndPort) {
|
|
||||||
// set proxy
|
|
||||||
this.torProxy = hostAndPort;
|
|
||||||
this.httpProxy = computeHttpProxy(hostAndPort);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<HttpProxy> getHttpProxy(HttpUsage httpUsage) {
|
|
||||||
return Optional.ofNullable(httpProxy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,150 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
import io.reactivex.Completable;
|
|
||||||
import io.reactivex.Observable;
|
|
||||||
import io.reactivex.Single;
|
|
||||||
import io.reactivex.functions.Action;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import org.slf4j.MDC;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.Callable;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
public class AsyncUtil {
|
|
||||||
private static final ThreadUtil threadUtil = ThreadUtil.getInstance();
|
|
||||||
private static AsyncUtil instance;
|
|
||||||
|
|
||||||
public static AsyncUtil getInstance() {
|
|
||||||
if(instance == null) {
|
|
||||||
instance = new AsyncUtil();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T unwrapException(Callable<T> c) throws Exception {
|
|
||||||
try {
|
|
||||||
return c.call();
|
|
||||||
} catch(RuntimeException e) {
|
|
||||||
// blockingXXX wraps errors with RuntimeException, unwrap it
|
|
||||||
throw unwrapException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Exception unwrapException(Exception e) throws Exception {
|
|
||||||
if(e.getCause() != null && e.getCause() instanceof Exception) {
|
|
||||||
throw (Exception) e.getCause();
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T blockingGet(Single<T> o) throws Exception {
|
|
||||||
try {
|
|
||||||
return unwrapException(o::blockingGet);
|
|
||||||
} catch(ExecutionException e) {
|
|
||||||
// blockingGet(threadUtil.runWithTimeoutAndRetry()) wraps InterruptedException("exit (done)")
|
|
||||||
// with ExecutionException, unwrap it
|
|
||||||
throw unwrapException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T blockingGet(Single<T> o, long timeoutMs) throws Exception {
|
|
||||||
Callable<T> callable = () -> blockingGet(o);
|
|
||||||
return blockingGet(runAsync(callable, timeoutMs));
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T blockingGet(Future<T> o, long timeoutMs) throws Exception {
|
|
||||||
return o.get(timeoutMs, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T blockingLast(Observable<T> o) throws Exception {
|
|
||||||
return unwrapException(o::blockingLast);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void blockingAwait(Completable o) throws Exception {
|
|
||||||
Callable<Optional> callable = () -> {
|
|
||||||
o.blockingAwait();
|
|
||||||
return Optional.empty();
|
|
||||||
};
|
|
||||||
unwrapException(callable);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void blockingAwait(Completable o, long timeoutMs) throws Exception {
|
|
||||||
Callable<Optional> callable = () -> {
|
|
||||||
o.blockingAwait();
|
|
||||||
return Optional.empty();
|
|
||||||
};
|
|
||||||
blockingGet(runAsync(callable, timeoutMs));
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> Single<T> timeout(Single<T> o, long timeoutMs) {
|
|
||||||
try {
|
|
||||||
return Single.just(blockingGet(o, timeoutMs));
|
|
||||||
} catch(Exception e) {
|
|
||||||
return Single.error(e);
|
|
||||||
}
|
|
||||||
}/*
|
|
||||||
|
|
||||||
public Completable timeout(Completable o, long timeoutMs) {
|
|
||||||
try {
|
|
||||||
return Completable.fromCallable(() -> {
|
|
||||||
blockingAwait(o, timeoutMs);
|
|
||||||
return Optional.empty();
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
return Completable.error(e);
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
public <T> Single<T> runIOAsync(final Callable<T> callable) {
|
|
||||||
return Single.fromCallable(callable).subscribeOn(Schedulers.io());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable runIOAsyncCompletable(final Action action) {
|
|
||||||
return Completable.fromAction(action).subscribeOn(Schedulers.io());
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T runIO(final Callable<T> callable) throws Exception {
|
|
||||||
return blockingGet(runIOAsync(callable));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void runIO(final Action action) throws Exception {
|
|
||||||
blockingAwait(runIOAsyncCompletable(action));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Completable runAsync(Runnable runnable, long timeoutMs) {
|
|
||||||
Future<?> future = runAsync(() -> {
|
|
||||||
runnable.run();
|
|
||||||
return Optional.empty(); // must return an object for using Completable.fromSingle()
|
|
||||||
});
|
|
||||||
return Completable.fromSingle(Single.fromFuture(future, timeoutMs, TimeUnit.MILLISECONDS));
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> Future<T> runAsync(Callable<T> callable) {
|
|
||||||
// preserve logging context
|
|
||||||
String mdc = mdcAppend("runAsync=" + System.currentTimeMillis());
|
|
||||||
return threadUtil.runAsync(() -> {
|
|
||||||
MDC.put("mdc", mdc);
|
|
||||||
return callable.call();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> Single<T> runAsync(Callable<T> callable, long timeoutMs) {
|
|
||||||
Future<T> future = runAsync(callable);
|
|
||||||
return Single.fromFuture(future, timeoutMs, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String mdcAppend(String info) {
|
|
||||||
String mdc = MDC.get("mdc");
|
|
||||||
if(mdc == null) {
|
|
||||||
mdc = "";
|
|
||||||
} else {
|
|
||||||
mdc += ",";
|
|
||||||
}
|
|
||||||
mdc += info;
|
|
||||||
return mdc;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
public abstract class HttpException extends Exception {
|
|
||||||
|
|
||||||
public HttpException(Exception cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
public class HttpNetworkException extends HttpException {
|
|
||||||
public HttpNetworkException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpNetworkException(Exception cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class HttpProxy {
|
|
||||||
private final HttpProxyProtocol protocol;
|
|
||||||
private final String host;
|
|
||||||
private final int port;
|
|
||||||
|
|
||||||
public HttpProxy(HttpProxyProtocol protocol, String host, int port) {
|
|
||||||
this.protocol = protocol;
|
|
||||||
this.host = host;
|
|
||||||
this.port = port;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean validate(String proxy) {
|
|
||||||
// check protocol
|
|
||||||
String[] protocols = Arrays.stream(HttpProxyProtocol.values()).map(p -> p.name()).toArray(String[]::new);
|
|
||||||
String regex = "^(" + StringUtils.join(protocols, "|").toLowerCase() + ")://(.+?):([0-9]+)";
|
|
||||||
return proxy.trim().toLowerCase().matches(regex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpProxyProtocol getProtocol() {
|
|
||||||
return protocol;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getHost() {
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPort() {
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return protocol + "://" + host + ":" + port;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public enum HttpProxyProtocol {
|
|
||||||
HTTP,
|
|
||||||
SOCKS,
|
|
||||||
SOCKS5;
|
|
||||||
|
|
||||||
public static Optional<HttpProxyProtocol> find(String value) {
|
|
||||||
try {
|
|
||||||
return Optional.of(valueOf(value));
|
|
||||||
} catch(Exception e) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
public class HttpResponseException extends HttpException {
|
|
||||||
private final String responseBody;
|
|
||||||
private final int statusCode;
|
|
||||||
|
|
||||||
public HttpResponseException(Exception cause, String responseBody, int statusCode) {
|
|
||||||
super(cause);
|
|
||||||
this.responseBody = responseBody;
|
|
||||||
this.statusCode = statusCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpResponseException(String message, String responseBody, int statusCode) {
|
|
||||||
super(message);
|
|
||||||
this.responseBody = responseBody;
|
|
||||||
this.statusCode = statusCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpResponseException(String responseBody, int statusCode) {
|
|
||||||
this("response statusCode=" + statusCode, responseBody, statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getResponseBody() {
|
|
||||||
return responseBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getStatusCode() {
|
|
||||||
return statusCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "HttpResponseException{" +
|
|
||||||
"message=" + getMessage() + ", " +
|
|
||||||
"responseBody='" + responseBody + '\'' +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
public class HttpSystemException extends HttpException {
|
|
||||||
public HttpSystemException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpSystemException(Exception cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class HttpUsage {
|
|
||||||
public static final HttpUsage DEFAULT = new HttpUsage("Default");
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
|
|
||||||
public HttpUsage(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if(this == o) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if(o == null || getClass() != o.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
HttpUsage httpUsage = (HttpUsage) o;
|
|
||||||
return Objects.equals(name, httpUsage.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(name);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public interface IBackendClient {
|
|
||||||
<T> T getJson(String url, Class<T> responseType, Map<String, String> headers) throws HttpException;
|
|
||||||
|
|
||||||
<T> T getJson(String url, Class<T> responseType, Map<String, String> headers, boolean async) throws HttpException;
|
|
||||||
|
|
||||||
<T> T postUrlEncoded(String url, Class<T> responseType, Map<String, String> headers, Map<String, String> body) throws Exception;
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
import io.reactivex.Single;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface IHttpClient extends IBackendClient {
|
|
||||||
void connect() throws Exception;
|
|
||||||
|
|
||||||
<T> Single<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body);
|
|
||||||
|
|
||||||
Single<Optional<String>> postString(String urlStr, Map<String, String> headers, String contentType, String content);
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
public interface IHttpClientService {
|
|
||||||
IHttpClient getHttpClient(HttpUsage httpUsage);
|
|
||||||
|
|
||||||
void changeIdentity(); // change Tor identity if any
|
|
||||||
|
|
||||||
void stop();
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface IHttpProxySupplier {
|
|
||||||
Optional<HttpProxy> getHttpProxy(HttpUsage httpUsage);
|
|
||||||
|
|
||||||
void changeIdentity();
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
|
||||||
|
|
||||||
public class JSONUtils {
|
|
||||||
private static JSONUtils instance;
|
|
||||||
|
|
||||||
private ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
public JSONUtils() {
|
|
||||||
objectMapper = new ObjectMapper();
|
|
||||||
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
|
||||||
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final JSONUtils getInstance() {
|
|
||||||
if(instance == null) {
|
|
||||||
instance = new JSONUtils();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectMapper getObjectMapper() {
|
|
||||||
return objectMapper;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,168 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import io.reactivex.Single;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.Callable;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public abstract class JacksonHttpClient implements IHttpClient {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(JacksonHttpClient.class);
|
|
||||||
|
|
||||||
private final Consumer<Exception> onNetworkError;
|
|
||||||
|
|
||||||
public JacksonHttpClient(Consumer<Exception> onNetworkError) {
|
|
||||||
this.onNetworkError = onNetworkError;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract String requestJsonGet(String urlStr, Map<String, String> headers, boolean async) throws HttpException;
|
|
||||||
|
|
||||||
protected abstract String requestJsonPost(String urlStr, Map<String, String> headers, String jsonBody) throws HttpException;
|
|
||||||
|
|
||||||
protected abstract String requestStringPost(String urlStr, Map<String, String> headers, String contentType, String content) throws HttpException;
|
|
||||||
|
|
||||||
protected abstract String requestJsonPostUrlEncoded(String urlStr, Map<String, String> headers, Map<String, String> body) throws HttpException;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T getJson(String urlStr, Class<T> responseType, Map<String, String> headers) throws HttpException {
|
|
||||||
return getJson(urlStr, responseType, headers, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T getJson(String urlStr, Class<T> responseType, Map<String, String> headers, boolean async) throws HttpException {
|
|
||||||
return httpObservableBlockingSingle(() -> { // run on ioThread
|
|
||||||
try {
|
|
||||||
String responseContent = handleNetworkError("getJson " + urlStr, () -> requestJsonGet(urlStr, headers, async));
|
|
||||||
return parseJson(responseContent, responseType, 200);
|
|
||||||
} catch(Exception e) {
|
|
||||||
if(log.isDebugEnabled()) {
|
|
||||||
log.error("getJson failed: " + urlStr + ": " + e.toString());
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> Single<Optional<T>> postJson(final String urlStr, final Class<T> responseType, final Map<String, String> headers, final Object bodyObj) {
|
|
||||||
return httpObservable(
|
|
||||||
() -> {
|
|
||||||
try {
|
|
||||||
String jsonBody = getObjectMapper().writeValueAsString(bodyObj);
|
|
||||||
String responseContent = handleNetworkError("postJson " + urlStr, () -> requestJsonPost(urlStr, headers, jsonBody));
|
|
||||||
return parseJson(responseContent, responseType, 200);
|
|
||||||
} catch(HttpException e) {
|
|
||||||
if(log.isDebugEnabled()) {
|
|
||||||
log.error("postJson failed: " + urlStr + ": " + e);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Single<Optional<String>> postString(String urlStr, Map<String, String> headers, String contentType, String content) {
|
|
||||||
return httpObservable(
|
|
||||||
() -> {
|
|
||||||
try {
|
|
||||||
return handleNetworkError("postString " + urlStr, () -> requestStringPost(urlStr, headers, contentType, content));
|
|
||||||
} catch(HttpException e) {
|
|
||||||
if(log.isDebugEnabled()) {
|
|
||||||
log.error("postJson failed: " + urlStr + ": " + e.toString());
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T postUrlEncoded(String urlStr, Class<T> responseType, Map<String, String> headers, Map<String, String> body) throws HttpException {
|
|
||||||
return httpObservableBlockingSingle(() -> { // run on ioThread
|
|
||||||
try {
|
|
||||||
String responseContent = handleNetworkError("postUrlEncoded " + urlStr, () -> requestJsonPostUrlEncoded(urlStr, headers, body));
|
|
||||||
return parseJson(responseContent, responseType, 200);
|
|
||||||
} catch(Exception e) {
|
|
||||||
if(log.isDebugEnabled()) {
|
|
||||||
log.error("postUrlEncoded failed: " + urlStr + ": " + e);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T> T parseJson(String responseContent, Class<T> responseType, int statusCode) throws HttpException {
|
|
||||||
T result;
|
|
||||||
if(log.isTraceEnabled()) {
|
|
||||||
String responseStr = (responseContent != null ? responseContent : "null");
|
|
||||||
if(responseStr.length() > 500) {
|
|
||||||
responseStr = responseStr.substring(0, 500) + "...";
|
|
||||||
}
|
|
||||||
log.trace("response[" + (responseType != null ? responseType.getCanonicalName() : "null") + "]: " + responseStr);
|
|
||||||
}
|
|
||||||
if(String.class.equals(responseType)) {
|
|
||||||
result = (T) responseContent;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
result = getObjectMapper().readValue(responseContent, responseType);
|
|
||||||
} catch(Exception e) {
|
|
||||||
throw new HttpResponseException(e, responseContent, statusCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String handleNetworkError(String logInfo, Callable<String> doHttpRequest) throws HttpException {
|
|
||||||
try {
|
|
||||||
try {
|
|
||||||
// first attempt
|
|
||||||
return doHttpRequest.call();
|
|
||||||
} catch(HttpNetworkException e) {
|
|
||||||
if(log.isDebugEnabled()) {
|
|
||||||
log.warn("HTTP_ERROR_NETWORK " + logInfo + ", retrying: " + e.getMessage());
|
|
||||||
}
|
|
||||||
// change tor proxy
|
|
||||||
onNetworkError(e);
|
|
||||||
|
|
||||||
// retry second attempt
|
|
||||||
return doHttpRequest.call();
|
|
||||||
}
|
|
||||||
} catch(HttpException e) { // forward
|
|
||||||
throw e;
|
|
||||||
} catch(Exception e) { // should never happen
|
|
||||||
throw new HttpSystemException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onNetworkError(HttpNetworkException e) {
|
|
||||||
if(onNetworkError != null) {
|
|
||||||
synchronized(JacksonHttpClient.class) { // avoid overlapping Tor restarts between httpClients
|
|
||||||
onNetworkError.accept(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected <T> Single<Optional<T>> httpObservable(final Callable<T> supplier) {
|
|
||||||
return Single.fromCallable(() -> Optional.ofNullable(supplier.call())).subscribeOn(Schedulers.io());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected <T> T httpObservableBlockingSingle(final Callable<T> supplier) throws HttpException {
|
|
||||||
try {
|
|
||||||
Optional<T> opt = AsyncUtil.getInstance().blockingGet(httpObservable(supplier));
|
|
||||||
return opt.orElse(null);
|
|
||||||
} catch(HttpException e) { // forward
|
|
||||||
throw e;
|
|
||||||
} catch(Exception e) { // should never happen
|
|
||||||
throw new HttpNetworkException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ObjectMapper getObjectMapper() {
|
|
||||||
return JSONUtils.getInstance().getObjectMapper();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,188 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
|
||||||
import org.eclipse.jetty.client.api.ContentResponse;
|
|
||||||
import org.eclipse.jetty.client.api.Request;
|
|
||||||
import org.eclipse.jetty.client.api.Response;
|
|
||||||
import org.eclipse.jetty.client.util.FormContentProvider;
|
|
||||||
import org.eclipse.jetty.client.util.InputStreamResponseListener;
|
|
||||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
|
||||||
import org.eclipse.jetty.http.HttpMethod;
|
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
|
||||||
import org.eclipse.jetty.util.Fields;
|
|
||||||
import org.eclipse.jetty.util.component.LifeCycle;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Scanner;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public class JettyHttpClient extends JacksonHttpClient {
|
|
||||||
protected static Logger log = LoggerFactory.getLogger(JettyHttpClient.class);
|
|
||||||
public static final String CONTENTTYPE_APPLICATION_JSON = "application/json";
|
|
||||||
|
|
||||||
private final HttpClient httpClient;
|
|
||||||
private final long requestTimeout;
|
|
||||||
private final HttpUsage httpUsage;
|
|
||||||
|
|
||||||
public JettyHttpClient(Consumer<Exception> onNetworkError, HttpClient httpClient, long requestTimeout, HttpUsage httpUsage) {
|
|
||||||
super(onNetworkError);
|
|
||||||
this.httpClient = httpClient;
|
|
||||||
this.requestTimeout = requestTimeout;
|
|
||||||
this.httpUsage = httpUsage;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void connect() throws HttpException {
|
|
||||||
try {
|
|
||||||
if(!httpClient.isRunning()) {
|
|
||||||
httpClient.start();
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
|
||||||
throw new HttpNetworkException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void restart() {
|
|
||||||
try {
|
|
||||||
if(log.isDebugEnabled()) {
|
|
||||||
log.debug("restart");
|
|
||||||
}
|
|
||||||
if(httpClient.isRunning()) {
|
|
||||||
httpClient.stop();
|
|
||||||
}
|
|
||||||
httpClient.start();
|
|
||||||
} catch(Exception e) {
|
|
||||||
log.error("", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
try {
|
|
||||||
if(httpClient.isRunning()) {
|
|
||||||
httpClient.stop();
|
|
||||||
Executor executor = httpClient.getExecutor();
|
|
||||||
if(executor instanceof LifeCycle) {
|
|
||||||
((LifeCycle) executor).stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
|
||||||
log.error("Error stopping client", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String requestJsonGet(String urlStr, Map<String, String> headers, boolean async) throws HttpException {
|
|
||||||
Request req = computeHttpRequest(urlStr, HttpMethod.GET, headers);
|
|
||||||
return makeRequest(req, async);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String requestJsonPost(String urlStr, Map<String, String> headers, String jsonBody) throws HttpException {
|
|
||||||
Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers);
|
|
||||||
req.content(new StringContentProvider(CONTENTTYPE_APPLICATION_JSON, jsonBody, StandardCharsets.UTF_8));
|
|
||||||
return makeRequest(req, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String requestStringPost(String urlStr, Map<String, String> headers, String contentType, String content) throws HttpException {
|
|
||||||
log.debug("POST " + urlStr);
|
|
||||||
Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers);
|
|
||||||
req.content(new StringContentProvider(content), contentType);
|
|
||||||
return makeRequest(req, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String requestJsonPostUrlEncoded(String urlStr, Map<String, String> headers, Map<String, String> body) throws HttpException {
|
|
||||||
Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers);
|
|
||||||
req.content(new FormContentProvider(computeBodyFields(body)));
|
|
||||||
return makeRequest(req, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Fields computeBodyFields(Map<String, String> body) {
|
|
||||||
Fields fields = new Fields();
|
|
||||||
for(Map.Entry<String, String> entry : body.entrySet()) {
|
|
||||||
fields.put(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String makeRequest(Request req, boolean async) throws HttpException {
|
|
||||||
String responseContent;
|
|
||||||
if(async) {
|
|
||||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
|
||||||
req.send(listener);
|
|
||||||
|
|
||||||
// Call to the listener's get() blocks until the headers arrived
|
|
||||||
Response response;
|
|
||||||
try {
|
|
||||||
response = listener.get(requestTimeout, TimeUnit.MILLISECONDS);
|
|
||||||
} catch(Exception e) {
|
|
||||||
throw new HttpNetworkException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read content
|
|
||||||
InputStream is = listener.getInputStream();
|
|
||||||
Scanner s = new Scanner(is).useDelimiter("\\A");
|
|
||||||
responseContent = s.hasNext() ? s.next() : null;
|
|
||||||
|
|
||||||
// check status
|
|
||||||
checkResponseStatus(response.getStatus(), responseContent);
|
|
||||||
} else {
|
|
||||||
ContentResponse response;
|
|
||||||
try {
|
|
||||||
response = req.send();
|
|
||||||
} catch(Exception e) {
|
|
||||||
throw new HttpNetworkException(e);
|
|
||||||
}
|
|
||||||
checkResponseStatus(response.getStatus(), response.getContentAsString());
|
|
||||||
responseContent = response.getContentAsString();
|
|
||||||
}
|
|
||||||
return responseContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkResponseStatus(int status, String responseBody) throws HttpResponseException {
|
|
||||||
if(!HttpStatus.isSuccess(status)) {
|
|
||||||
log.error("Http query failed: status=" + status + ", responseBody=" + responseBody);
|
|
||||||
throw new HttpResponseException(responseBody, status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpClient getJettyHttpClient() throws HttpException {
|
|
||||||
connect();
|
|
||||||
return httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Request computeHttpRequest(String url, HttpMethod method, Map<String, String> headers) throws HttpException {
|
|
||||||
if(url.endsWith("/rpc")) {
|
|
||||||
// log RPC as TRACE
|
|
||||||
if(log.isTraceEnabled()) {
|
|
||||||
String headersStr = headers != null ? " (" + headers.keySet() + ")" : "";
|
|
||||||
log.trace("+" + method + ": " + url + headersStr);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(log.isDebugEnabled()) {
|
|
||||||
String headersStr = headers != null ? " (" + headers.keySet() + ")" : "";
|
|
||||||
log.debug("+" + method + ": " + url + headersStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Request req = getJettyHttpClient().newRequest(url);
|
|
||||||
req.method(method);
|
|
||||||
if(headers != null) {
|
|
||||||
for(Map.Entry<String, String> entry : headers.entrySet()) {
|
|
||||||
req.header(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req.timeout(requestTimeout, TimeUnit.MILLISECONDS);
|
|
||||||
return req;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpUsage getHttpUsage() {
|
|
||||||
return httpUsage;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,167 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
import com.google.common.util.concurrent.RateLimiter;
|
|
||||||
import com.sparrowwallet.sparrow.net.http.client.socks5.Socks5Proxy;
|
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
|
||||||
import org.eclipse.jetty.client.ProxyConfiguration;
|
|
||||||
import org.eclipse.jetty.client.Socks4Proxy;
|
|
||||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
|
||||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
|
||||||
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public class JettyHttpClientService implements IHttpClientService {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(JettyHttpClientService.class);
|
|
||||||
private static final String NAME = "HttpClient";
|
|
||||||
public static final long DEFAULT_TIMEOUT = 30000;
|
|
||||||
|
|
||||||
// limit changing Tor identity on network error every 4 minutes
|
|
||||||
private static final double RATE_CHANGE_IDENTITY_ON_NETWORK_ERROR = 1.0 / 240;
|
|
||||||
|
|
||||||
protected Map<HttpUsage, JettyHttpClient> httpClients; // used by Sparrow
|
|
||||||
private final IHttpProxySupplier httpProxySupplier;
|
|
||||||
private final long requestTimeout;
|
|
||||||
|
|
||||||
public JettyHttpClientService(long requestTimeout, IHttpProxySupplier httpProxySupplier) {
|
|
||||||
this.httpProxySupplier = httpProxySupplier != null ? httpProxySupplier : computeHttpProxySupplierDefault();
|
|
||||||
this.requestTimeout = requestTimeout;
|
|
||||||
this.httpClients = new ConcurrentHashMap<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public JettyHttpClientService(long requestTimeout) {
|
|
||||||
this(requestTimeout, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JettyHttpClientService() {
|
|
||||||
this(DEFAULT_TIMEOUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static IHttpProxySupplier computeHttpProxySupplierDefault() {
|
|
||||||
return new IHttpProxySupplier() {
|
|
||||||
@Override
|
|
||||||
public Optional<HttpProxy> getHttpProxy(HttpUsage httpUsage) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void changeIdentity() {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JettyHttpClient getHttpClient(HttpUsage httpUsage) {
|
|
||||||
JettyHttpClient httpClient = httpClients.get(httpUsage);
|
|
||||||
if(httpClient == null) {
|
|
||||||
if(log.isDebugEnabled()) {
|
|
||||||
log.debug("+httpClient[" + httpUsage + "]");
|
|
||||||
}
|
|
||||||
httpClient = computeHttpClient(httpUsage);
|
|
||||||
httpClients.put(httpUsage, httpClient);
|
|
||||||
}
|
|
||||||
return httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected JettyHttpClient computeHttpClient(HttpUsage httpUsage) {
|
|
||||||
Consumer<Exception> onNetworkError = computeOnNetworkError();
|
|
||||||
HttpClient httpClient = computeJettyClient(httpUsage);
|
|
||||||
return new JettyHttpClient(onNetworkError, httpClient, requestTimeout, httpUsage);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected HttpClient computeJettyClient(HttpUsage httpUsage) {
|
|
||||||
// we use jetty for proxy SOCKS support
|
|
||||||
HttpClient jettyHttpClient = new HttpClient(new SslContextFactory());
|
|
||||||
// jettyHttpClient.setSocketAddressResolver(new MySocketAddressResolver());
|
|
||||||
|
|
||||||
// prevent user-agent tracking
|
|
||||||
jettyHttpClient.setUserAgentField(null);
|
|
||||||
|
|
||||||
// configure
|
|
||||||
configureProxy(jettyHttpClient, httpUsage);
|
|
||||||
configureThread(jettyHttpClient, httpUsage);
|
|
||||||
|
|
||||||
return jettyHttpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Consumer<Exception> computeOnNetworkError() {
|
|
||||||
RateLimiter rateLimiter = RateLimiter.create(RATE_CHANGE_IDENTITY_ON_NETWORK_ERROR);
|
|
||||||
return e -> {
|
|
||||||
if(!rateLimiter.tryAcquire()) {
|
|
||||||
if(log.isDebugEnabled()) {
|
|
||||||
log.debug("onNetworkError: not changing Tor identity (too many recent attempts)");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// change Tor identity on network error
|
|
||||||
httpProxySupplier.changeIdentity();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void configureProxy(HttpClient jettyHttpClient, HttpUsage httpUsage) {
|
|
||||||
Optional<HttpProxy> httpProxyOptional = httpProxySupplier.getHttpProxy(httpUsage);
|
|
||||||
if(httpProxyOptional != null && httpProxyOptional.isPresent()) {
|
|
||||||
HttpProxy httpProxy = httpProxyOptional.get();
|
|
||||||
if(log.isDebugEnabled()) {
|
|
||||||
log.debug("+httpClient: proxy=" + httpProxy);
|
|
||||||
}
|
|
||||||
ProxyConfiguration.Proxy jettyProxy = computeJettyProxy(httpProxy);
|
|
||||||
jettyHttpClient.getProxyConfiguration().getProxies().add(jettyProxy);
|
|
||||||
} else {
|
|
||||||
if(log.isDebugEnabled()) {
|
|
||||||
log.debug("+httpClient: no proxy");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void configureThread(HttpClient jettyHttpClient, HttpUsage httpUsage) {
|
|
||||||
String name = NAME + "-" + httpUsage.toString();
|
|
||||||
|
|
||||||
QueuedThreadPool threadPool = new QueuedThreadPool();
|
|
||||||
threadPool.setName(name);
|
|
||||||
threadPool.setDaemon(true);
|
|
||||||
jettyHttpClient.setExecutor(threadPool);
|
|
||||||
jettyHttpClient.setScheduler(new ScheduledExecutorScheduler(name + "-scheduler", true));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ProxyConfiguration.Proxy computeJettyProxy(HttpProxy httpProxy) {
|
|
||||||
ProxyConfiguration.Proxy jettyProxy = null;
|
|
||||||
switch(httpProxy.getProtocol()) {
|
|
||||||
case SOCKS:
|
|
||||||
jettyProxy = new Socks4Proxy(httpProxy.getHost(), httpProxy.getPort());
|
|
||||||
break;
|
|
||||||
case SOCKS5:
|
|
||||||
jettyProxy = new Socks5Proxy(httpProxy.getHost(), httpProxy.getPort());
|
|
||||||
break;
|
|
||||||
case HTTP:
|
|
||||||
jettyProxy = new org.eclipse.jetty.client.HttpProxy(httpProxy.getHost(), httpProxy.getPort());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return jettyProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void stop() {
|
|
||||||
for(JettyHttpClient httpClient : httpClients.values()) {
|
|
||||||
httpClient.stop();
|
|
||||||
}
|
|
||||||
httpClients.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void changeIdentity() {
|
|
||||||
stop();
|
|
||||||
httpProxySupplier.changeIdentity();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IHttpProxySupplier getHttpProxySupplier() {
|
|
||||||
return httpProxySupplier;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
package com.sparrowwallet.sparrow.net.http.client;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.concurrent.*;
|
|
||||||
|
|
||||||
public class ThreadUtil {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ThreadUtil.class);
|
|
||||||
|
|
||||||
private static ThreadUtil instance;
|
|
||||||
|
|
||||||
private ExecutorService executorService;
|
|
||||||
|
|
||||||
protected ThreadUtil() {
|
|
||||||
this.executorService = computeExecutorService();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ThreadUtil getInstance() {
|
|
||||||
if(instance == null) {
|
|
||||||
instance = new ThreadUtil();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ExecutorService computeExecutorService() {
|
|
||||||
return Executors.newFixedThreadPool(5,
|
|
||||||
r -> {
|
|
||||||
Thread t = Executors.defaultThreadFactory().newThread(r);
|
|
||||||
t.setDaemon(true);
|
|
||||||
return t;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setExecutorService(ScheduledExecutorService executorService) {
|
|
||||||
this.executorService = executorService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> Future<T> runAsync(Callable<T> callable) {
|
|
||||||
return executorService.submit(callable);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,222 +0,0 @@
|
||||||
/**
|
|
||||||
* Socks5 backported from Jetty12 - we still use Jetty9 for JDK8 compatibility.
|
|
||||||
*/
|
|
||||||
|
|
||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under the
|
|
||||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
|
||||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package com.sparrowwallet.sparrow.net.http.client.socks5;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.io.EndPoint;
|
|
||||||
import org.eclipse.jetty.util.BufferUtil;
|
|
||||||
import org.eclipse.jetty.util.Callback;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.channels.ClosedChannelException;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class for SOCKS5 proxying.
|
|
||||||
*
|
|
||||||
* @see Socks5Proxy
|
|
||||||
*/
|
|
||||||
public class Socks5 {
|
|
||||||
/** The SOCKS protocol version: {@value}. */
|
|
||||||
public static final byte VERSION = 0x05;
|
|
||||||
|
|
||||||
/** The SOCKS5 {@code CONNECT} command used in SOCKS5 connect requests. */
|
|
||||||
public static final byte COMMAND_CONNECT = 0x01;
|
|
||||||
|
|
||||||
/** The reserved byte value: {@value}. */
|
|
||||||
public static final byte RESERVED = 0x00;
|
|
||||||
|
|
||||||
/** The address type for IPv4 used in SOCKS5 connect requests and responses. */
|
|
||||||
public static final byte ADDRESS_TYPE_IPV4 = 0x01;
|
|
||||||
|
|
||||||
/** The address type for domain names used in SOCKS5 connect requests and responses. */
|
|
||||||
public static final byte ADDRESS_TYPE_DOMAIN = 0x03;
|
|
||||||
|
|
||||||
/** The address type for IPv6 used in SOCKS5 connect requests and responses. */
|
|
||||||
public static final byte ADDRESS_TYPE_IPV6 = 0x04;
|
|
||||||
|
|
||||||
private Socks5() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A SOCKS5 authentication method.
|
|
||||||
*
|
|
||||||
* <p>Implementations should send and receive the bytes that are specific to the particular
|
|
||||||
* authentication method.
|
|
||||||
*/
|
|
||||||
public interface Authentication {
|
|
||||||
/**
|
|
||||||
* Performs the authentication send and receive bytes exchanges specific for this {@link
|
|
||||||
* Authentication}.
|
|
||||||
*
|
|
||||||
* @param endPoint the {@link EndPoint} to send to and receive from the SOCKS5 server
|
|
||||||
* @param callback the callback to complete when the authentication is complete
|
|
||||||
*/
|
|
||||||
void authenticate(EndPoint endPoint, Callback callback);
|
|
||||||
|
|
||||||
/** A factory for {@link Authentication}s. */
|
|
||||||
interface Factory {
|
|
||||||
/**
|
|
||||||
* @return the authentication method defined by RFC 1928
|
|
||||||
*/
|
|
||||||
byte getMethod();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return a new {@link Authentication}
|
|
||||||
*/
|
|
||||||
Authentication newAuthentication();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The implementation of the {@code NO AUTH} authentication method defined in <a
|
|
||||||
* href="https://datatracker.ietf.org/doc/html/rfc1928">RFC 1928</a>.
|
|
||||||
*/
|
|
||||||
public static class NoAuthenticationFactory implements Authentication.Factory {
|
|
||||||
public static final byte METHOD = 0x00;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte getMethod() {
|
|
||||||
return METHOD;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Authentication newAuthentication() {
|
|
||||||
return (endPoint, callback) -> callback.succeeded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The implementation of the {@code USERNAME/PASSWORD} authentication method defined in <a
|
|
||||||
* href="https://datatracker.ietf.org/doc/html/rfc1929">RFC 1929</a>.
|
|
||||||
*/
|
|
||||||
public static class UsernamePasswordAuthenticationFactory implements Authentication.Factory {
|
|
||||||
public static final byte METHOD = 0x02;
|
|
||||||
public static final byte VERSION = 0x01;
|
|
||||||
private static final Logger LOG =
|
|
||||||
LoggerFactory.getLogger(UsernamePasswordAuthenticationFactory.class);
|
|
||||||
|
|
||||||
private final String userName;
|
|
||||||
private final String password;
|
|
||||||
private final Charset charset;
|
|
||||||
|
|
||||||
public UsernamePasswordAuthenticationFactory(String userName, String password) {
|
|
||||||
this(userName, password, StandardCharsets.US_ASCII);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UsernamePasswordAuthenticationFactory(
|
|
||||||
String userName, String password, Charset charset) {
|
|
||||||
this.userName = Objects.requireNonNull(userName);
|
|
||||||
this.password = Objects.requireNonNull(password);
|
|
||||||
this.charset = Objects.requireNonNull(charset);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte getMethod() {
|
|
||||||
return METHOD;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Authentication newAuthentication() {
|
|
||||||
return new UsernamePasswordAuthentication(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class UsernamePasswordAuthentication implements Authentication, Callback {
|
|
||||||
private final ByteBuffer byteBuffer = BufferUtil.allocate(2);
|
|
||||||
private final UsernamePasswordAuthenticationFactory factory;
|
|
||||||
private EndPoint endPoint;
|
|
||||||
private Callback callback;
|
|
||||||
|
|
||||||
private UsernamePasswordAuthentication(UsernamePasswordAuthenticationFactory factory) {
|
|
||||||
this.factory = factory;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void authenticate(EndPoint endPoint, Callback callback) {
|
|
||||||
this.endPoint = endPoint;
|
|
||||||
this.callback = callback;
|
|
||||||
|
|
||||||
byte[] userNameBytes = factory.userName.getBytes(factory.charset);
|
|
||||||
byte[] passwordBytes = factory.password.getBytes(factory.charset);
|
|
||||||
ByteBuffer byteBuffer =
|
|
||||||
(ByteBuffer)
|
|
||||||
ByteBuffer.allocate(3 + userNameBytes.length + passwordBytes.length)
|
|
||||||
.put(VERSION)
|
|
||||||
.put((byte) userNameBytes.length)
|
|
||||||
.put(userNameBytes)
|
|
||||||
.put((byte) passwordBytes.length)
|
|
||||||
.put(passwordBytes)
|
|
||||||
.flip();
|
|
||||||
endPoint.write(Callback.from(this::authenticationSent, this::failed), byteBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void authenticationSent() {
|
|
||||||
if(LOG.isDebugEnabled()) {
|
|
||||||
LOG.debug("Written SOCKS5 username/password authentication request");
|
|
||||||
}
|
|
||||||
endPoint.fillInterested(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void succeeded() {
|
|
||||||
try {
|
|
||||||
int filled = endPoint.fill(byteBuffer);
|
|
||||||
if(filled < 0) {
|
|
||||||
throw new ClosedChannelException();
|
|
||||||
}
|
|
||||||
if(byteBuffer.remaining() < 2) {
|
|
||||||
endPoint.fillInterested(this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(LOG.isDebugEnabled()) {
|
|
||||||
LOG.debug("Received SOCKS5 username/password authentication response");
|
|
||||||
}
|
|
||||||
byte version = byteBuffer.get();
|
|
||||||
if(version != VERSION) {
|
|
||||||
throw new IOException(
|
|
||||||
"Unsupported username/password authentication version: " + version);
|
|
||||||
}
|
|
||||||
byte status = byteBuffer.get();
|
|
||||||
if(status != 0) {
|
|
||||||
throw new IOException("SOCK5 username/password authentication failure");
|
|
||||||
}
|
|
||||||
if(LOG.isDebugEnabled()) {
|
|
||||||
LOG.debug("SOCKS5 username/password authentication succeeded");
|
|
||||||
}
|
|
||||||
callback.succeeded();
|
|
||||||
} catch(Throwable x) {
|
|
||||||
failed(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void failed(Throwable x) {
|
|
||||||
callback.failed(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InvocationType getInvocationType() {
|
|
||||||
return InvocationType.NON_BLOCKING;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,419 +0,0 @@
|
||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under the
|
|
||||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
|
||||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package com.sparrowwallet.sparrow.net.http.client.socks5;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
|
||||||
import org.eclipse.jetty.client.HttpClientTransport;
|
|
||||||
import org.eclipse.jetty.client.HttpDestination;
|
|
||||||
import org.eclipse.jetty.client.Origin;
|
|
||||||
import org.eclipse.jetty.client.ProxyConfiguration.Proxy;
|
|
||||||
import org.eclipse.jetty.io.AbstractConnection;
|
|
||||||
import org.eclipse.jetty.io.ClientConnectionFactory;
|
|
||||||
import org.eclipse.jetty.io.Connection;
|
|
||||||
import org.eclipse.jetty.io.EndPoint;
|
|
||||||
import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
|
|
||||||
import org.eclipse.jetty.util.BufferUtil;
|
|
||||||
import org.eclipse.jetty.util.Callback;
|
|
||||||
import org.eclipse.jetty.util.Promise;
|
|
||||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.channels.ClosedChannelException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client-side proxy configuration for SOCKS5, defined by <a
|
|
||||||
* href="https://datatracker.ietf.org/doc/html/rfc1928">RFC 1928</a>.
|
|
||||||
*
|
|
||||||
* <p>Multiple authentication methods are supported via {@link
|
|
||||||
* #putAuthenticationFactory(Socks5.Authentication.Factory)}. By default only the {@link
|
|
||||||
* Socks5.NoAuthenticationFactory NO AUTH} authentication method is configured. The {@link
|
|
||||||
* Socks5.UsernamePasswordAuthenticationFactory USERNAME/PASSWORD} is available to applications but
|
|
||||||
* must be explicitly configured and added.
|
|
||||||
*/
|
|
||||||
public class Socks5Proxy extends Proxy {
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(Socks5Proxy.class);
|
|
||||||
|
|
||||||
private final Map<Byte, Socks5.Authentication.Factory> authentications = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new instance with the given SOCKS5 proxy host and port.
|
|
||||||
*
|
|
||||||
* @param host the SOCKS5 proxy host name
|
|
||||||
* @param port the SOCKS5 proxy port
|
|
||||||
*/
|
|
||||||
public Socks5Proxy(String host, int port) {
|
|
||||||
this(new Origin.Address(host, port), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new instance with the given SOCKS5 proxy address.
|
|
||||||
*
|
|
||||||
* <p>When {@code secure=true} the communication between the client and the proxy will be
|
|
||||||
* encrypted (using this proxy {@link #getSslContextFactory()} which typically defaults to that of
|
|
||||||
* {@link HttpClient}.
|
|
||||||
*
|
|
||||||
* @param address the SOCKS5 proxy address (host and port)
|
|
||||||
* @param secure whether the communication between the client and the SOCKS5 proxy should be
|
|
||||||
* secure
|
|
||||||
*/
|
|
||||||
public Socks5Proxy(Origin.Address address, boolean secure) {
|
|
||||||
super(address, secure);
|
|
||||||
putAuthenticationFactory(new Socks5.NoAuthenticationFactory());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static ClientConnectionFactory newSslClientConnectionFactory(HttpClient httpClient, SslContextFactory sslContextFactory, ClientConnectionFactory connectionFactory) {
|
|
||||||
if(sslContextFactory == null) {
|
|
||||||
sslContextFactory = httpClient.getSslContextFactory();
|
|
||||||
}
|
|
||||||
return new SslClientConnectionFactory(sslContextFactory, httpClient.getByteBufferPool(), httpClient.getExecutor(), connectionFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides this class with the given SOCKS5 authentication method.
|
|
||||||
*
|
|
||||||
* @param authenticationFactory the SOCKS5 authentication factory
|
|
||||||
* @return the previous authentication method of the same type, or {@code null} if there was none
|
|
||||||
* of that type already present
|
|
||||||
*/
|
|
||||||
public Socks5.Authentication.Factory putAuthenticationFactory(Socks5.Authentication.Factory authenticationFactory) {
|
|
||||||
return authentications.put(authenticationFactory.getMethod(), authenticationFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the authentication of the given {@code method}.
|
|
||||||
*
|
|
||||||
* @param method the authentication method to remove
|
|
||||||
*/
|
|
||||||
public Socks5.Authentication.Factory removeAuthenticationFactory(byte method) {
|
|
||||||
return authentications.remove(method);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ClientConnectionFactory newClientConnectionFactory(ClientConnectionFactory connectionFactory) {
|
|
||||||
return new Socks5ProxyClientConnectionFactory(connectionFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Socks5ProxyConnection extends AbstractConnection implements Connection.UpgradeFrom {
|
|
||||||
private static final Pattern IPv4_PATTERN = Pattern.compile("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})");
|
|
||||||
|
|
||||||
// SOCKS5 response max length is 262 bytes.
|
|
||||||
private final ByteBuffer byteBuffer = BufferUtil.allocate(512);
|
|
||||||
private final ClientConnectionFactory connectionFactory;
|
|
||||||
private final Map<String, Object> context;
|
|
||||||
private final Map<Byte, Socks5.Authentication.Factory> authentications;
|
|
||||||
private State state = State.HANDSHAKE;
|
|
||||||
|
|
||||||
private Socks5ProxyConnection(EndPoint endPoint, Executor executor, ClientConnectionFactory connectionFactory, Map<String, Object> context, Map<Byte, Socks5.Authentication.Factory> authentications) {
|
|
||||||
super(endPoint, executor);
|
|
||||||
this.connectionFactory = connectionFactory;
|
|
||||||
this.context = context;
|
|
||||||
this.authentications = new LinkedHashMap<>(authentications);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ByteBuffer onUpgradeFrom() {
|
|
||||||
return BufferUtil.copy(byteBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOpen() {
|
|
||||||
super.onOpen();
|
|
||||||
sendHandshake();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendHandshake() {
|
|
||||||
try {
|
|
||||||
// +-------------+--------------------+------------------+
|
|
||||||
// | version (1) | num of methods (1) | methods (1..255) |
|
|
||||||
// +-------------+--------------------+------------------+
|
|
||||||
int size = authentications.size();
|
|
||||||
ByteBuffer byteBuffer =
|
|
||||||
ByteBuffer.allocate(1 + 1 + size).put(Socks5.VERSION).put((byte) size);
|
|
||||||
authentications.keySet().forEach(byteBuffer::put);
|
|
||||||
byteBuffer.flip();
|
|
||||||
getEndPoint().write(Callback.from(this::handshakeSent, this::fail), byteBuffer);
|
|
||||||
} catch(Throwable x) {
|
|
||||||
fail(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handshakeSent() {
|
|
||||||
if(LOG.isDebugEnabled()) {
|
|
||||||
LOG.debug("Written SOCKS5 handshake request");
|
|
||||||
}
|
|
||||||
state = State.HANDSHAKE;
|
|
||||||
fillInterested();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fail(Throwable x) {
|
|
||||||
if(LOG.isDebugEnabled()) {
|
|
||||||
LOG.debug("SOCKS5 failure", x);
|
|
||||||
}
|
|
||||||
getEndPoint().close();
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Promise<Connection> promise =
|
|
||||||
(Promise<Connection>)
|
|
||||||
this.context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
|
|
||||||
promise.failed(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFillable() {
|
|
||||||
try {
|
|
||||||
switch(state) {
|
|
||||||
case HANDSHAKE:
|
|
||||||
receiveHandshake();
|
|
||||||
break;
|
|
||||||
case CONNECT:
|
|
||||||
receiveConnect();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
} catch(Throwable x) {
|
|
||||||
fail(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void receiveHandshake() throws IOException {
|
|
||||||
// +-------------+------------+
|
|
||||||
// | version (1) | method (1) |
|
|
||||||
// +-------------+------------+
|
|
||||||
int filled = getEndPoint().fill(byteBuffer);
|
|
||||||
if(filled < 0) {
|
|
||||||
throw new ClosedChannelException();
|
|
||||||
}
|
|
||||||
if(byteBuffer.remaining() < 2) {
|
|
||||||
fillInterested();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(LOG.isDebugEnabled()) {
|
|
||||||
LOG.debug("Received SOCKS5 handshake response {}", BufferUtil.toDetailString(byteBuffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
byte version = byteBuffer.get();
|
|
||||||
if(version != Socks5.VERSION) {
|
|
||||||
throw new IOException("Unsupported SOCKS5 version: " + version);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte method = byteBuffer.get();
|
|
||||||
if(method == -1) {
|
|
||||||
throw new IOException("Unacceptable SOCKS5 authentication methods");
|
|
||||||
}
|
|
||||||
|
|
||||||
Socks5.Authentication.Factory factory = authentications.get(method);
|
|
||||||
if(factory == null) {
|
|
||||||
throw new IOException("Unknown SOCKS5 authentication method: " + method);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory
|
|
||||||
.newAuthentication()
|
|
||||||
.authenticate(getEndPoint(), Callback.from(this::sendConnect, this::fail));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendConnect() {
|
|
||||||
try {
|
|
||||||
// +-------------+-------------+--------------+------------------+------------------------+----------+
|
|
||||||
// | version (1) | command (1) | reserved (1) | address type (1) | address bytes (4..255) |
|
|
||||||
// port (2) |
|
|
||||||
// +-------------+-------------+--------------+------------------+------------------------+----------+
|
|
||||||
HttpDestination destination = (HttpDestination) context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
|
||||||
Origin.Address address = destination.getOrigin().getAddress();
|
|
||||||
String host = address.getHost();
|
|
||||||
short port = (short) address.getPort();
|
|
||||||
|
|
||||||
ByteBuffer byteBuffer;
|
|
||||||
Matcher matcher = IPv4_PATTERN.matcher(host);
|
|
||||||
if(matcher.matches()) {
|
|
||||||
byteBuffer =
|
|
||||||
ByteBuffer.allocate(10)
|
|
||||||
.put(Socks5.VERSION)
|
|
||||||
.put(Socks5.COMMAND_CONNECT)
|
|
||||||
.put(Socks5.RESERVED)
|
|
||||||
.put(Socks5.ADDRESS_TYPE_IPV4);
|
|
||||||
for(int i = 1; i <= 4; ++i) {
|
|
||||||
byteBuffer.put(Byte.parseByte(matcher.group(i)));
|
|
||||||
}
|
|
||||||
byteBuffer.putShort(port).flip();
|
|
||||||
} else if(true /*URIUtil.isValidHostRegisteredName(host)*/) {
|
|
||||||
byte[] bytes = host.getBytes(StandardCharsets.US_ASCII);
|
|
||||||
if(bytes.length > 255) {
|
|
||||||
throw new IOException("Invalid host name: " + host);
|
|
||||||
}
|
|
||||||
byteBuffer =
|
|
||||||
(ByteBuffer)
|
|
||||||
ByteBuffer.allocate(7 + bytes.length)
|
|
||||||
.put(Socks5.VERSION)
|
|
||||||
.put(Socks5.COMMAND_CONNECT)
|
|
||||||
.put(Socks5.RESERVED)
|
|
||||||
.put(Socks5.ADDRESS_TYPE_DOMAIN)
|
|
||||||
.put((byte) bytes.length)
|
|
||||||
.put(bytes)
|
|
||||||
.putShort(port)
|
|
||||||
.flip();
|
|
||||||
} else {
|
|
||||||
// Assume IPv6.
|
|
||||||
byte[] bytes = InetAddress.getByName(host).getAddress();
|
|
||||||
byteBuffer =
|
|
||||||
(ByteBuffer)
|
|
||||||
ByteBuffer.allocate(22)
|
|
||||||
.put(Socks5.VERSION)
|
|
||||||
.put(Socks5.COMMAND_CONNECT)
|
|
||||||
.put(Socks5.RESERVED)
|
|
||||||
.put(Socks5.ADDRESS_TYPE_IPV6)
|
|
||||||
.put(bytes)
|
|
||||||
.putShort(port)
|
|
||||||
.flip();
|
|
||||||
}
|
|
||||||
|
|
||||||
getEndPoint().write(Callback.from(this::connectSent, this::fail), byteBuffer);
|
|
||||||
} catch(Throwable x) {
|
|
||||||
fail(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void connectSent() {
|
|
||||||
if(LOG.isDebugEnabled()) {
|
|
||||||
LOG.debug("Written SOCKS5 connect request");
|
|
||||||
}
|
|
||||||
state = State.CONNECT;
|
|
||||||
fillInterested();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void receiveConnect() throws IOException {
|
|
||||||
// +-------------+-----------+--------------+------------------+------------------------+----------+
|
|
||||||
// | version (1) | reply (1) | reserved (1) | address type (1) | address bytes (4..255) | port
|
|
||||||
// (2) |
|
|
||||||
// +-------------+-----------+--------------+------------------+------------------------+----------+
|
|
||||||
int filled = getEndPoint().fill(byteBuffer);
|
|
||||||
if(filled < 0) {
|
|
||||||
throw new ClosedChannelException();
|
|
||||||
}
|
|
||||||
if(byteBuffer.remaining() < 5) {
|
|
||||||
fillInterested();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
byte addressType = byteBuffer.get(3);
|
|
||||||
int length = 6;
|
|
||||||
if(addressType == Socks5.ADDRESS_TYPE_IPV4) {
|
|
||||||
length += 4;
|
|
||||||
} else if(addressType == Socks5.ADDRESS_TYPE_DOMAIN) {
|
|
||||||
length += 1 + (byteBuffer.get(4) & 0xFF);
|
|
||||||
} else if(addressType == Socks5.ADDRESS_TYPE_IPV6) {
|
|
||||||
length += 16;
|
|
||||||
} else {
|
|
||||||
throw new IOException("Invalid SOCKS5 address type: " + addressType);
|
|
||||||
}
|
|
||||||
if(byteBuffer.remaining() < length) {
|
|
||||||
fillInterested();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(LOG.isDebugEnabled()) {
|
|
||||||
LOG.debug("Received SOCKS5 connect response {}", BufferUtil.toDetailString(byteBuffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have all the SOCKS5 bytes.
|
|
||||||
byte version = byteBuffer.get();
|
|
||||||
if(version != Socks5.VERSION) {
|
|
||||||
throw new IOException("Unsupported SOCKS5 version: " + version);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte status = byteBuffer.get();
|
|
||||||
switch(status) {
|
|
||||||
case 0: {
|
|
||||||
// Consume the buffer before upgrading to the tunnel.
|
|
||||||
byteBuffer.position(length);
|
|
||||||
tunnel();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 1:
|
|
||||||
throw new IOException("SOCKS5 general failure");
|
|
||||||
case 2:
|
|
||||||
throw new IOException("SOCKS5 connection not allowed");
|
|
||||||
case 3:
|
|
||||||
throw new IOException("SOCKS5 network unreachable");
|
|
||||||
case 4:
|
|
||||||
throw new IOException("SOCKS5 host unreachable");
|
|
||||||
case 5:
|
|
||||||
throw new IOException("SOCKS5 connection refused");
|
|
||||||
case 6:
|
|
||||||
throw new IOException("SOCKS5 timeout expired");
|
|
||||||
case 7:
|
|
||||||
throw new IOException("SOCKS5 unsupported command");
|
|
||||||
case 8:
|
|
||||||
throw new IOException("SOCKS5 unsupported address");
|
|
||||||
default:
|
|
||||||
throw new IOException("SOCKS5 unknown status: " + status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void tunnel() {
|
|
||||||
try {
|
|
||||||
HttpDestination destination =
|
|
||||||
(HttpDestination) context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
|
||||||
this.context.put("ssl.peer.host", destination.getHost());
|
|
||||||
this.context.put("ssl.peer.port", destination.getPort());
|
|
||||||
// Origin.Address address = destination.getOrigin().getAddress();
|
|
||||||
// Don't want to do DNS resolution here.
|
|
||||||
// InetSocketAddress inet = InetSocketAddress.createUnresolved(address.getHost(),
|
|
||||||
// address.getPort());
|
|
||||||
// context.put(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, inet);
|
|
||||||
ClientConnectionFactory connectionFactory = this.connectionFactory;
|
|
||||||
if(destination.isSecure()) {
|
|
||||||
connectionFactory =
|
|
||||||
newSslClientConnectionFactory(destination.getHttpClient(), null, connectionFactory);
|
|
||||||
}
|
|
||||||
Connection newConnection = connectionFactory.newConnection(getEndPoint(), context);
|
|
||||||
getEndPoint().upgrade(newConnection);
|
|
||||||
if(LOG.isDebugEnabled()) {
|
|
||||||
LOG.debug("SOCKS5 tunnel established: {} over {}", this, newConnection);
|
|
||||||
}
|
|
||||||
} catch(Throwable x) {
|
|
||||||
fail(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum State {
|
|
||||||
HANDSHAKE,
|
|
||||||
CONNECT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Socks5ProxyClientConnectionFactory implements ClientConnectionFactory {
|
|
||||||
private final ClientConnectionFactory connectionFactory;
|
|
||||||
|
|
||||||
private Socks5ProxyClientConnectionFactory(ClientConnectionFactory connectionFactory) {
|
|
||||||
this.connectionFactory = connectionFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Connection newConnection(EndPoint endPoint, Map<String, Object> context) {
|
|
||||||
HttpDestination destination = (HttpDestination) context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
|
||||||
Executor executor = destination.getHttpClient().getExecutor();
|
|
||||||
Socks5ProxyConnection connection = new Socks5ProxyConnection(endPoint, executor, connectionFactory, context, authentications);
|
|
||||||
return customize(connection, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,7 +16,7 @@ import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.net.HttpClientService;
|
import com.sparrowwallet.sparrow.net.HttpClientService;
|
||||||
import com.sparrowwallet.sparrow.net.Protocol;
|
import com.sparrowwallet.sparrow.net.Protocol;
|
||||||
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
|
import com.sparrowwallet.tern.http.client.HttpResponseException;
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
|
@ -64,8 +64,5 @@ open module com.sparrowwallet.sparrow {
|
||||||
requires com.sparrowwallet.bokmakierie;
|
requires com.sparrowwallet.bokmakierie;
|
||||||
requires java.smartcardio;
|
requires java.smartcardio;
|
||||||
requires com.jcraft.jzlib;
|
requires com.jcraft.jzlib;
|
||||||
requires org.eclipse.jetty.client;
|
requires com.sparrowwallet.tern;
|
||||||
requires org.eclipse.jetty.http;
|
|
||||||
requires org.eclipse.jetty.util;
|
|
||||||
requires org.eclipse.jetty.io;
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue