package com.nutomic.syncthingandroid.http;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.util.Log;
import com.android.volley.AuthFailureError;
import com.android.volley.RequestQueue;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HurlStack;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public abstract class ApiRequest {
private static final String TAG = "ApiRequest";
/**
* The name of the HTTP header used for the syncthing API key.
*/
private static final String HEADER_API_KEY = "X-API-Key";
public interface OnSuccessListener {
public void onSuccess(String result);
}
public interface OnErrorListener {
public void onError(VolleyError error);
}
private static RequestQueue sVolleyQueue;
private RequestQueue getVolleyQueue() {
if (sVolleyQueue == null) {
Context context = mContext.getApplicationContext();
sVolleyQueue = Volley.newRequestQueue(context, new NetworkStack());
}
return sVolleyQueue;
}
private final Context mContext;
private final URL mUrl;
protected final String mPath;
private final String mHttpsCertPath;
private final String mApiKey;
public ApiRequest(Context context, URL url, String path, String httpsCertPath, String apiKey) {
mContext = context;
mUrl = url;
mPath = path;
mHttpsCertPath = httpsCertPath;
mApiKey = apiKey;
}
protected Uri buildUri(Map<String, String> params) {
Uri.Builder uriBuilder = Uri.parse(mUrl.toString())
.buildUpon()
.path(mPath);
for (Map.Entry<String, String> entry : params.entrySet()) {
uriBuilder.appendQueryParameter(entry.getKey(), entry.getValue());
}
return uriBuilder.build();
}
/**
* Opens the connection, then returns success status and response string.
*/
protected void connect(int requestMethod, Uri uri, @Nullable String requestBody,
@Nullable OnSuccessListener listener, @Nullable OnErrorListener errorListener) {
StringRequest request = new StringRequest(requestMethod, uri.toString(), reply -> {
if (listener != null)
listener.onSuccess(reply);
}, error -> {
if (errorListener != null)
errorListener.onError(error);
Log.w(TAG, "Request to " + uri + " failed: " + error.getMessage());
}) {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
return ImmutableMap.of(HEADER_API_KEY, mApiKey);
}
@Override
public byte[] getBody() throws AuthFailureError {
return Optional.fromNullable(requestBody).transform(String::getBytes).orNull();
}
};
getVolleyQueue().add(request);
}
/**
* Extends {@link HurlStack}, uses {@link #getSslSocketFactory()} and disables hostname
* verification.
*/
private class NetworkStack extends HurlStack {
public NetworkStack() {
super(null, getSslSocketFactory());
}
@Override
protected HttpURLConnection createConnection(URL url) throws IOException {
HttpsURLConnection connection = (HttpsURLConnection) super.createConnection(url);
connection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
return connection;
}
}
private SSLSocketFactory getSslSocketFactory() {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{new SyncthingTrustManager()},
new SecureRandom());
return sslContext.getSocketFactory();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
Log.w(TAG, e);
return null;
}
}
/*
* TrustManager checking against the local Syncthing instance's https public key.
*
* Based on http://stackoverflow.com/questions/16719959#16759793
*/
private class SyncthingTrustManager implements X509TrustManager {
private static final String TAG = "SyncthingTrustManager";
@Override
@SuppressLint("TrustAllX509TrustManager")
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
/**
* Verifies certs against public key of the local syncthing instance
*/
@Override
public void checkServerTrusted(X509Certificate[] certs,
String authType) throws CertificateException {
InputStream is = null;
try {
is = new FileInputStream(mHttpsCertPath);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate ca = (X509Certificate) cf.generateCertificate(is);
for (X509Certificate cert : certs) {
cert.verify(ca.getPublicKey());
}
} catch (FileNotFoundException | NoSuchAlgorithmException | InvalidKeyException |
NoSuchProviderException | SignatureException e) {
throw new CertificateException("Untrusted Certificate!", e);
} finally {
try {
if (is != null)
is.close();
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
}