diff --git a/build.gradle b/build.gradle index 8b4eff38..3cd4937a 100644 --- a/build.gradle +++ b/build.gradle @@ -122,7 +122,7 @@ dependencies { exclude group: 'org.slf4j' } 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:rxjavafx:2.2.2') implementation('org.apache.commons:commons-lang3:3.7') @@ -158,7 +158,7 @@ processResources { test { 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 { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java b/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java index 1ed220d9..a5b586d7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java @@ -6,7 +6,7 @@ import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Transaction; 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.LoggerFactory; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java index 04078fa5..c1847209 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java @@ -2,7 +2,7 @@ package com.sparrowwallet.sparrow.net; import com.sparrowwallet.sparrow.AppServices; 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.Service; import javafx.concurrent.Task; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/HttpClientService.java b/src/main/java/com/sparrowwallet/sparrow/net/HttpClientService.java index 4171d319..e886f0b2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/HttpClientService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/HttpClientService.java @@ -1,50 +1,12 @@ package com.sparrowwallet.sparrow.net; 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.Task; -import java.util.Map; -import java.util.Optional; - -public class HttpClientService extends JettyHttpClientService { - private static final int REQUEST_TIMEOUT = 120000; - +public class HttpClientService extends com.sparrowwallet.tern.http.client.HttpClientService { public HttpClientService(HostAndPort torProxy) { - super(REQUEST_TIMEOUT, new HttpProxySupplier(torProxy)); - } - - public T requestJson(String url, Class responseType, Map headers) throws Exception { - return getHttpClient(HttpUsage.DEFAULT).getJson(url, responseType, headers); - } - - public Observable> postJson(String url, Class responseType, Map headers, Object body) { - return getHttpClient(HttpUsage.DEFAULT).postJson(url, responseType, headers, body).toObservable(); - } - - public String postString(String url, Map 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(); + super(new HttpProxySupplier(torProxy)); } public static class ShutdownService extends Service { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/HttpProxySupplier.java b/src/main/java/com/sparrowwallet/sparrow/net/HttpProxySupplier.java index 41509f52..1aa59770 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/HttpProxySupplier.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/HttpProxySupplier.java @@ -1,44 +1,10 @@ package com.sparrowwallet.sparrow.net; 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) { - this.torProxy = 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 getHttpProxy(HttpUsage httpUsage) { - return Optional.ofNullable(httpProxy); + super(torProxy); } @Override diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/AsyncUtil.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/AsyncUtil.java deleted file mode 100644 index 8449a1ee..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/AsyncUtil.java +++ /dev/null @@ -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 unwrapException(Callable 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 blockingGet(Single 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 blockingGet(Single o, long timeoutMs) throws Exception { - Callable callable = () -> blockingGet(o); - return blockingGet(runAsync(callable, timeoutMs)); - } - - public T blockingGet(Future o, long timeoutMs) throws Exception { - return o.get(timeoutMs, TimeUnit.MILLISECONDS); - } - - public T blockingLast(Observable o) throws Exception { - return unwrapException(o::blockingLast); - } - - public void blockingAwait(Completable o) throws Exception { - Callable callable = () -> { - o.blockingAwait(); - return Optional.empty(); - }; - unwrapException(callable); - } - - public void blockingAwait(Completable o, long timeoutMs) throws Exception { - Callable callable = () -> { - o.blockingAwait(); - return Optional.empty(); - }; - blockingGet(runAsync(callable, timeoutMs)); - } - - public Single timeout(Single 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 Single runIOAsync(final Callable callable) { - return Single.fromCallable(callable).subscribeOn(Schedulers.io()); - } - - public Completable runIOAsyncCompletable(final Action action) { - return Completable.fromAction(action).subscribeOn(Schedulers.io()); - } - - public T runIO(final Callable 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 Future runAsync(Callable callable) { - // preserve logging context - String mdc = mdcAppend("runAsync=" + System.currentTimeMillis()); - return threadUtil.runAsync(() -> { - MDC.put("mdc", mdc); - return callable.call(); - }); - } - - public Single runAsync(Callable callable, long timeoutMs) { - Future 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; - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpException.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpException.java deleted file mode 100644 index 5fb0d057..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpException.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpNetworkException.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpNetworkException.java deleted file mode 100644 index ca7be862..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpNetworkException.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpProxy.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpProxy.java deleted file mode 100644 index 7d1f3793..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpProxy.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpProxyProtocol.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpProxyProtocol.java deleted file mode 100644 index 7e8f131a..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpProxyProtocol.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.sparrowwallet.sparrow.net.http.client; - -import java.util.Optional; - -public enum HttpProxyProtocol { - HTTP, - SOCKS, - SOCKS5; - - public static Optional find(String value) { - try { - return Optional.of(valueOf(value)); - } catch(Exception e) { - return Optional.empty(); - } - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpResponseException.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpResponseException.java deleted file mode 100644 index 990fdc1b..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpResponseException.java +++ /dev/null @@ -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 + '\'' + - '}'; - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpSystemException.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpSystemException.java deleted file mode 100644 index 1ac3adf8..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpSystemException.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpUsage.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpUsage.java deleted file mode 100644 index 3e5dbb97..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/HttpUsage.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/IBackendClient.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/IBackendClient.java deleted file mode 100644 index 0ec438bc..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/IBackendClient.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.sparrowwallet.sparrow.net.http.client; - -import java.util.Map; - -public interface IBackendClient { - T getJson(String url, Class responseType, Map headers) throws HttpException; - - T getJson(String url, Class responseType, Map headers, boolean async) throws HttpException; - - T postUrlEncoded(String url, Class responseType, Map headers, Map body) throws Exception; -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/IHttpClient.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/IHttpClient.java deleted file mode 100644 index 88e2925d..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/IHttpClient.java +++ /dev/null @@ -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; - - Single> postJson(String url, Class responseType, Map headers, Object body); - - Single> postString(String urlStr, Map headers, String contentType, String content); -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/IHttpClientService.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/IHttpClientService.java deleted file mode 100644 index ed01db5c..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/IHttpClientService.java +++ /dev/null @@ -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(); -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/IHttpProxySupplier.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/IHttpProxySupplier.java deleted file mode 100644 index 0839699f..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/IHttpProxySupplier.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.sparrowwallet.sparrow.net.http.client; - -import java.util.Optional; - -public interface IHttpProxySupplier { - Optional getHttpProxy(HttpUsage httpUsage); - - void changeIdentity(); -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/JSONUtils.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/JSONUtils.java deleted file mode 100644 index 0d3b1679..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/JSONUtils.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/JacksonHttpClient.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/JacksonHttpClient.java deleted file mode 100644 index 55ee2cd9..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/JacksonHttpClient.java +++ /dev/null @@ -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 onNetworkError; - - public JacksonHttpClient(Consumer onNetworkError) { - this.onNetworkError = onNetworkError; - } - - protected abstract String requestJsonGet(String urlStr, Map headers, boolean async) throws HttpException; - - protected abstract String requestJsonPost(String urlStr, Map headers, String jsonBody) throws HttpException; - - protected abstract String requestStringPost(String urlStr, Map headers, String contentType, String content) throws HttpException; - - protected abstract String requestJsonPostUrlEncoded(String urlStr, Map headers, Map body) throws HttpException; - - @Override - public T getJson(String urlStr, Class responseType, Map headers) throws HttpException { - return getJson(urlStr, responseType, headers, false); - } - - @Override - public T getJson(String urlStr, Class responseType, Map 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 Single> postJson(final String urlStr, final Class responseType, final Map 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> postString(String urlStr, Map 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 postUrlEncoded(String urlStr, Class responseType, Map headers, Map 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 parseJson(String responseContent, Class 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 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 Single> httpObservable(final Callable supplier) { - return Single.fromCallable(() -> Optional.ofNullable(supplier.call())).subscribeOn(Schedulers.io()); - } - - protected T httpObservableBlockingSingle(final Callable supplier) throws HttpException { - try { - Optional 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(); - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/JettyHttpClient.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/JettyHttpClient.java deleted file mode 100644 index bfe8da17..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/JettyHttpClient.java +++ /dev/null @@ -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 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 headers, boolean async) throws HttpException { - Request req = computeHttpRequest(urlStr, HttpMethod.GET, headers); - return makeRequest(req, async); - } - - @Override - protected String requestJsonPost(String urlStr, Map 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 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 headers, Map body) throws HttpException { - Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers); - req.content(new FormContentProvider(computeBodyFields(body))); - return makeRequest(req, false); - } - - private Fields computeBodyFields(Map body) { - Fields fields = new Fields(); - for(Map.Entry 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 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 entry : headers.entrySet()) { - req.header(entry.getKey(), entry.getValue()); - } - } - req.timeout(requestTimeout, TimeUnit.MILLISECONDS); - return req; - } - - public HttpUsage getHttpUsage() { - return httpUsage; - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/JettyHttpClientService.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/JettyHttpClientService.java deleted file mode 100644 index 43036a79..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/JettyHttpClientService.java +++ /dev/null @@ -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 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 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 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 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 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; - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/ThreadUtil.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/ThreadUtil.java deleted file mode 100644 index 88ab9f5c..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/ThreadUtil.java +++ /dev/null @@ -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 Future runAsync(Callable callable) { - return executorService.submit(callable); - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/socks5/Socks5.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/socks5/Socks5.java deleted file mode 100644 index 3e91181b..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/socks5/Socks5.java +++ /dev/null @@ -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. - * - *

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 RFC 1928. - */ - 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 RFC 1929. - */ - 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; - } - } - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/http/client/socks5/Socks5Proxy.java b/src/main/java/com/sparrowwallet/sparrow/net/http/client/socks5/Socks5Proxy.java deleted file mode 100644 index 0c43903a..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/net/http/client/socks5/Socks5Proxy.java +++ /dev/null @@ -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 RFC 1928. - * - *

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

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 context; - private final Map authentications; - private State state = State.HANDSHAKE; - - private Socks5ProxyConnection(EndPoint endPoint, Executor executor, ClientConnectionFactory connectionFactory, Map context, Map 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 promise = - (Promise) - 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 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); - } - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java b/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java index 0c673d28..231ac3b1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java +++ b/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java @@ -16,7 +16,7 @@ import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.net.HttpClientService; 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.Task; import org.slf4j.Logger; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index f1a77617..b4b0cafc 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -64,8 +64,5 @@ open module com.sparrowwallet.sparrow { requires com.sparrowwallet.bokmakierie; requires java.smartcardio; requires com.jcraft.jzlib; - requires org.eclipse.jetty.client; - requires org.eclipse.jetty.http; - requires org.eclipse.jetty.util; - requires org.eclipse.jetty.io; + requires com.sparrowwallet.tern; } \ No newline at end of file