/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.federation;
import com.google.common.base.Optional;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.bouncycastle.openssl.PEMReader;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.RequestEntityProcessing;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AccountCount;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import io.dropwizard.client.JerseyClientBuilder;
import io.dropwizard.client.JerseyClientConfiguration;
import io.dropwizard.setup.Environment;
public class FederatedClient {
private final Logger logger = LoggerFactory.getLogger(FederatedClient.class);
private static final String USER_COUNT_PATH = "/v1/federation/user_count";
private static final String USER_TOKENS_PATH = "/v1/federation/user_tokens/%d";
private static final String RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%d/%s";
private static final String PREKEY_PATH_DEVICE_V1 = "/v1/federation/key/%s/%s";
private static final String PREKEY_PATH_DEVICE_V2 = "/v2/federation/key/%s/%s";
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d/%s/%d";
private final FederatedPeer peer;
private final Client client;
public FederatedClient(Environment environment, JerseyClientConfiguration configuration,
String federationName, FederatedPeer peer)
throws IOException
{
try {
this.client = createClient(environment, configuration, federationName, peer);
this.peer = peer;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (KeyStoreException | KeyManagementException | CertificateException e) {
throw new IOException(e);
}
}
public URL getSignedAttachmentUri(long attachmentId) throws IOException {
try {
AttachmentUri response = client.target(peer.getUrl())
.path(String.format(ATTACHMENT_URI_PATH, attachmentId))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(AttachmentUri.class);
return response.getLocation();
} catch (ProcessingException e) {
logger.warn("Bad URI", e);
throw new IOException(e);
}
}
public Optional<PreKeyResponse> getKeysV2(String destination, String device) {
try {
PreKeyResponse response = client.target(peer.getUrl())
.path(String.format(PREKEY_PATH_DEVICE_V2, destination, device))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(PreKeyResponse.class);
return Optional.of(response);
} catch (ProcessingException e) {
logger.warn("PreKey", e);
return Optional.absent();
}
}
public int getUserCount() {
try {
AccountCount count = client.target(peer.getUrl())
.path(USER_COUNT_PATH)
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(AccountCount.class);
return count.getCount();
} catch (ProcessingException e) {
logger.warn("User Count", e);
return 0;
}
}
public List<ClientContact> getUserTokens(int offset) {
try {
ClientContacts contacts = client.target(peer.getUrl())
.path(String.format(USER_TOKENS_PATH, offset))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(ClientContacts.class);
return contacts.getContacts();
} catch (ProcessingException e) {
logger.warn("User Tokens", e);
return null;
}
}
public void sendMessages(String source, long sourceDeviceId, String destination, IncomingMessageList messages)
throws IOException
{
Response response = null;
try {
response = client.target(peer.getUrl())
.path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination))
.request()
.put(Entity.json(messages));
if (response.getStatus() != 200 && response.getStatus() != 204) {
if (response.getStatus() == 411) throw new WebApplicationException(Response.status(413).build());
else throw new WebApplicationException(Response.status(response.getStatusInfo()).build());
}
} catch (ProcessingException e) {
logger.warn("sendMessage", e);
throw new IOException(e);
} finally {
if (response != null) response.close();
}
}
public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId)
throws IOException
{
Response response = null;
try {
response = client.target(peer.getUrl())
.path(String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId))
.request()
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
.put(Entity.entity("", MediaType.APPLICATION_JSON_TYPE));
if (response.getStatus() != 200 && response.getStatus() != 204) {
if (response.getStatus() == 411) throw new WebApplicationException(Response.status(413).build());
else throw new WebApplicationException(Response.status(response.getStatusInfo()).build());
}
} catch (ProcessingException e) {
logger.warn("sendMessage", e);
throw new IOException(e);
} finally {
if (response != null) response.close();
}
}
private Client createClient(Environment environment, JerseyClientConfiguration configuration,
String federationName, FederatedPeer peer)
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, CertificateException
{
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
trustManagerFactory.init(initializeTrustStore(peer.getName(), peer.getCertificate()));
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier());
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register("https", sslConnectionSocketFactory).build();
Client client = new JerseyClientBuilder(environment).using(configuration)
.using(registry)
.build("FederatedClient");
client.property(ClientProperties.CONNECT_TIMEOUT, 5000);
client.property(ClientProperties.READ_TIMEOUT, 10000);
client.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED);
client.register(HttpAuthenticationFeature.basic(federationName, peer.getAuthenticationToken()));
return client;
}
private KeyStore initializeTrustStore(String name, String pemCertificate)
throws CertificateException
{
try {
PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemCertificate.getBytes())));
X509Certificate certificate = (X509Certificate) reader.readObject();
if (certificate == null) {
throw new CertificateException("No certificate found in parsing!");
}
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
keyStore.setCertificateEntry(name, certificate);
return keyStore;
} catch (IOException | KeyStoreException e) {
throw new CertificateException(e);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public String getPeerName() {
return peer.getName();
}
}