package org.aarboard.nextcloud.api.utils; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.concurrent.CompletableFuture; import javax.net.ssl.SSLContext; import org.aarboard.nextcloud.api.ServerConfig; import org.aarboard.nextcloud.api.exception.NextcloudApiException; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.HttpVersion; import org.apache.http.NameValuePair; import org.apache.http.StatusLine; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.AuthCache; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.client.utils.URIBuilder; import org.apache.http.concurrent.FutureCallback; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.TrustAllStrategy; import org.apache.http.entity.ContentType; import org.apache.http.impl.auth.BasicScheme; import org.apache.http.impl.client.BasicAuthCache; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.impl.nio.client.HttpAsyncClients; import org.apache.http.ssl.SSLContexts; public class ConnectorCommon { private final ServerConfig serverConfig; public ConnectorCommon(ServerConfig serverConfig) { this.serverConfig = serverConfig; } public <R> CompletableFuture<R> executeGet(String part, List<NameValuePair> queryParams, ResponseParser<R> parser) { try { URI url= buildUrl(part, queryParams); HttpRequestBase request = new HttpGet(url.toString()); return executeRequest(parser, request); } catch (IOException e) { throw new NextcloudApiException(e); } } public <R> CompletableFuture<R> executePost(String part, List<NameValuePair> postParams, ResponseParser<R> parser) { try { URI url= buildUrl(part, postParams); HttpRequestBase request = new HttpPost(url.toString()); return executeRequest(parser, request); } catch (IOException e) { throw new NextcloudApiException(e); } } public <R> CompletableFuture<R> executePut(String part1, String part2, List<NameValuePair> putParams, ResponseParser<R> parser) { try { URI url= buildUrl(part1 + "/" + part2, putParams); HttpRequestBase request = new HttpPut(url.toString()); return executeRequest(parser, request); } catch (IOException e) { throw new NextcloudApiException(e); } } public <R> CompletableFuture<R> executeDelete(String part1, String part2, List<NameValuePair> deleteParams, ResponseParser<R> parser) { try { URI url= buildUrl(part1 + "/" + part2, deleteParams); HttpRequestBase request = new HttpDelete(url.toString()); return executeRequest(parser, request); } catch (IOException e) { throw new NextcloudApiException(e); } } private URI buildUrl(String subPath, List<NameValuePair> queryParams) { if(serverConfig.getSubpathPrefix()!=null) { subPath = serverConfig.getSubpathPrefix()+"/"+subPath; } URIBuilder uB= new URIBuilder() .setScheme(serverConfig.isUseHTTPS() ? "https" : "http") .setHost(serverConfig.getServerName()) .setPort(serverConfig.getPort()) .setUserInfo(serverConfig.getUserName(), serverConfig.getPassword()) .setPath(subPath); if (queryParams != null) { uB.addParameters(queryParams); } try { return uB.build(); } catch (URISyntaxException e) { throw new NextcloudApiException(e); } } private <R> CompletableFuture<R> executeRequest(final ResponseParser<R> parser, HttpRequestBase request) throws IOException, ClientProtocolException { // https://docs.nextcloud.com/server/14/developer_manual/core/ocs-share-api.html request.addHeader("OCS-APIRequest", "true"); request.addHeader("Content-Type", "application/x-www-form-urlencoded"); request.setProtocolVersion(HttpVersion.HTTP_1_1); HttpClientContext context = prepareContext(); CompletableFuture<R> futureResponse = new CompletableFuture<>(); HttpAsyncClientSingleton.getInstance(serverConfig).execute(request, context, new ResponseCallback<>(parser, futureResponse)); return futureResponse; } private HttpClientContext prepareContext() { HttpHost targetHost = new HttpHost(serverConfig.getServerName(), serverConfig.getPort(), serverConfig.isUseHTTPS() ? "https" : "http"); AuthCache authCache = new BasicAuthCache(); authCache.put(targetHost, new BasicScheme()); CredentialsProvider credsProvider = new BasicCredentialsProvider(); UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(serverConfig.getUserName(), serverConfig.getPassword()); credsProvider.setCredentials(AuthScope.ANY, credentials); // Add AuthCache to the execution context HttpClientContext context = HttpClientContext.create(); context.setCredentialsProvider(credsProvider); context.setAuthCache(authCache); return context; } private final class ResponseCallback<R> implements FutureCallback<HttpResponse> { private final ResponseParser<R> parser; private final CompletableFuture<R> futureResponse; private ResponseCallback(ResponseParser<R> parser, CompletableFuture<R> futureResponse) { this.parser = parser; this.futureResponse = futureResponse; } @Override public void completed(HttpResponse response) { try { R result = handleResponse(parser, response); futureResponse.complete(result); } catch(Exception ex) { futureResponse.completeExceptionally(ex); } } private R handleResponse(ResponseParser<R> parser, HttpResponse response) throws IOException { StatusLine statusLine= response.getStatusLine(); if (statusLine.getStatusCode() == HttpStatus.SC_OK) { HttpEntity entity = response.getEntity(); if (entity != null) { Charset charset = ContentType.getOrDefault(entity).getCharset(); Reader reader = new InputStreamReader(entity.getContent(), charset); return parser.parseResponse(reader); } throw new NextcloudApiException("Empty response received"); } throw new NextcloudApiException(String.format("Request failed with %d %s", statusLine.getStatusCode(), statusLine.getReasonPhrase())); } @Override public void failed(Exception ex) { futureResponse.completeExceptionally(ex); } @Override public void cancelled() { futureResponse.cancel(true); } } private static class HttpAsyncClientSingleton { private static CloseableHttpAsyncClient HTTPC_CLIENT; private HttpAsyncClientSingleton(){} public static CloseableHttpAsyncClient getInstance(ServerConfig serverConfig) throws IOException{ if (HTTPC_CLIENT == null) { if (serverConfig.isTrustAllCertificates()) { try { SSLContext sslContext = SSLContexts.custom() .loadTrustMaterial(null, TrustAllStrategy.INSTANCE).build(); HTTPC_CLIENT = HttpAsyncClients.custom() .setSSLHostnameVerifier((NoopHostnameVerifier.INSTANCE)) .setSSLContext(sslContext) .build(); } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) { throw new IOException(e); } } else { HTTPC_CLIENT = HttpAsyncClients.createDefault(); } HTTPC_CLIENT.start(); } return HTTPC_CLIENT; } } public interface ResponseParser<R> { public R parseResponse(Reader reader); } /** * Close the http client. Required for clean shutdown. * @throws IOException */ public static void shutdown() throws IOException{ if(HttpAsyncClientSingleton.HTTPC_CLIENT != null) { HttpAsyncClientSingleton.getInstance(null).close(); } } }