repackage http client as tern library dependency

This commit is contained in:
Craig Raw 2024-11-25 13:17:39 +02:00
parent d49d5967b2
commit 46034b8f11
26 changed files with 10 additions and 1681 deletions

View file

@ -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 {

View file

@ -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;

View file

@ -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;

View file

@ -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> {

View file

@ -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

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -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 + '\'' +
'}';
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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;
} }