package lu.circl.mispbump.auxiliary; import android.annotation.SuppressLint; import androidx.annotation.NonNull; import java.net.NoRouteToHostException; import java.security.cert.CertificateException; import java.util.List; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import lu.circl.mispbump.interfaces.MispService; import lu.circl.mispbump.models.restModels.MispOrganisation; import lu.circl.mispbump.models.restModels.MispRole; import lu.circl.mispbump.models.restModels.MispServer; import lu.circl.mispbump.models.restModels.MispUser; import lu.circl.mispbump.models.restModels.Organisation; import lu.circl.mispbump.models.restModels.Role; import lu.circl.mispbump.models.restModels.Server; import lu.circl.mispbump.models.restModels.User; import lu.circl.mispbump.models.restModels.Version; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; /** * Implementation of the RetroFit2 Misp client. * In order to conveniently use this api some wrapper interfaces are implemented to return the requested API endpoint as java object. */ public class MispRestClient { private static MispRestClient instance; private MispService mispService; public static MispRestClient getInstance(String url, String authkey) { if (instance == null) { instance = new MispRestClient(); } instance.initMispRestInterface(url, authkey); return instance; } private void initMispRestInterface(String url, String authkey) { try { Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) .addConverterFactory(GsonConverterFactory.create()) .client(getCustomClient(true, true, authkey)) .build(); mispService = retrofit.create(MispService.class); } catch (IllegalArgumentException e) { throw new RuntimeException(e); } } public MispService getService() { return mispService; } /** * @param unsafe whether to accept all certificates or only trusted ones * @param logging whether to log Retrofit calls (for debugging) * @return {@link OkHttpClient} */ private OkHttpClient getCustomClient(boolean unsafe, boolean logging, final String authkey) { try { OkHttpClient.Builder builder = new OkHttpClient.Builder(); if (unsafe) { // Create a trust manager that does not validate certificate chains final TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { @SuppressLint("TrustAllX509TrustManager") @Override public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) { // nothing to do } @SuppressLint("TrustAllX509TrustManager") @Override public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) { // nothing to do } @Override public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new java.security.cert.X509Certificate[]{}; } } }; // Install the all-trusting trust manager final SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); // Create an ssl socket factory with our all-trusting manager final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]); builder.hostnameVerifier((hostname, session) -> true); } if (logging) { HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); builder.addInterceptor(interceptor); } // create interceptor builder.addInterceptor(chain -> { Request.Builder ongoing = chain.request().newBuilder(); ongoing.addHeader("Accept", "application/json"); ongoing.addHeader("Content-Type", "application/json"); ongoing.addHeader("Authorization", authkey); return chain.proceed(ongoing.build()); }); return builder.build(); } catch (Exception e) { throw new RuntimeException(e); } } /** * Check via pyMispRoute if server is available * * @param callback {@link AvailableCallback} */ public void isAvailable(final AvailableCallback callback) { Call call = mispService.pyMispVersion(); call.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (!response.isSuccessful()) { if (response.code() == 403) { callback.available(); return; } callback.unavailable(extractError(response)); } else { callback.available(); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { callback.unavailable(extractError(t)); } }); } public void getRoles(final AllRolesCallback callback) { Call> call = mispService.getRoles(); call.enqueue(new Callback>() { @Override public void onResponse(@NonNull Call> call, @NonNull Response> response) { if (!response.isSuccessful()) { callback.failure(extractError(response)); return; } List mispRoles = response.body(); assert mispRoles != null; Role[] roles = new Role[mispRoles.size()]; for (int i = 0; i < roles.length; i++) { roles[i] = mispRoles.get(i).role; } callback.success(roles); } @Override public void onFailure(@NonNull Call> call, @NonNull Throwable t) { callback.failure(extractError(t)); } }); } /** * Fetches information about the user that is associated with saved auth key. * * @param callback {@link UserCallback} wrapper to return user directly */ public void getMyUser(final UserCallback callback) { Call call = mispService.getMyUserInformation(); call.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { if (response.body() != null) { callback.success(response.body().getUser()); } else { callback.failure("response body was null"); } } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { t.printStackTrace(); callback.failure(t.getMessage()); } }); } /** * Get an user with specific ID. * * @param userId user identifier * @param callback {@link UserCallback} wrapper to return user directly */ public void getUser(int userId, final UserCallback callback) { Call call = mispService.getUser(userId); call.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { if (response.body() != null) { callback.success(response.body().getUser()); } else { callback.failure("response body was null"); } } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { t.printStackTrace(); callback.failure(t.getMessage()); } }); } public void getUser(final String emailAddress, final UserCallback callback) { getAllUsers(new AllUsersCallback() { @Override public void success(User[] users) { for (User user : users) { if (user.getEmail().equals(emailAddress)) { callback.success(user); return; } } callback.failure("Could not find user with email address {" + emailAddress + "}"); } @Override public void failure(String error) { callback.failure(error); } }); } public void getAllUsers(final AllMispUsersCallback callback) { Call> call = mispService.getAllUsers(); call.enqueue(new Callback>() { @Override public void onResponse(@NonNull Call> call, @NonNull Response> response) { if (!response.isSuccessful()) { callback.failure("Failed onResponse"); return; } callback.success(response.body()); } @Override public void onFailure(@NonNull Call> call, @NonNull Throwable t) { callback.failure(extractError(t)); } }); } public void getAllUsers(final AllUsersCallback callback) { Call> call = mispService.getAllUsers(); call.enqueue(new Callback>() { @Override public void onResponse(@NonNull Call> call, @NonNull Response> response) { if (!response.isSuccessful()) { callback.failure("Failed onResponse"); return; } List mispUsers = response.body(); assert mispUsers != null; User[] users = new User[mispUsers.size()]; for (int i = 0; i < users.length; i++) { users[i] = mispUsers.get(i).getUser(); } callback.success(users); } @Override public void onFailure(@NonNull Call> call, @NonNull Throwable t) { callback.failure(extractError(t)); } }); } /** * Add a given user to the MISP instance referenced by url in preferences. * * @param user user to add * @param callback {@link UserCallback} wrapper to return the created user directly */ public void addUser(User user, final UserCallback callback) { Call call = mispService.addUser(user); call.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { assert response.body() != null; callback.success(response.body().getUser()); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { callback.failure(t.getMessage()); } }); } // --- organisation routes --- /** * Get an organisation by a given organisation id. * * @param orgId organisation identifier * @param callback {@link OrganisationCallback} wrapper to return a organisation directly */ public void getOrganisation(int orgId, final OrganisationCallback callback) { Call call = mispService.getOrganisation(orgId); call.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { if (response.body() != null) { callback.success(response.body().organisation); } else { callback.failure("Response was empty"); } } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { callback.failure(t.getMessage()); } }); } public void getOrganisation(final String uuid, final OrganisationCallback callback) { getAllOrganisations(new AllOrganisationsCallback() { @Override public void success(Organisation[] organisations) { for (Organisation organisation : organisations) { if (organisation.getUuid().equals(uuid)) { callback.success(organisation); return; } } callback.failure("Could not find organisation with UUID {" + uuid + "}"); } @Override public void failure(String error) { callback.failure(error); } }); } public void getAllOrganisations(final AllOrganisationsCallback callback) { Call> call = mispService.getAllOrganisations(); call.enqueue(new Callback>() { @Override public void onResponse(@NonNull Call> call, @NonNull Response> response) { if (!response.isSuccessful()) { return; } List mispOrganisations = response.body(); assert mispOrganisations != null; Organisation[] organisations = new Organisation[mispOrganisations.size()]; for (int i = 0; i < mispOrganisations.size(); i++) { organisations[i] = mispOrganisations.get(i).organisation; } callback.success(organisations); } @Override public void onFailure(@NonNull Call> call, @NonNull Throwable t) { callback.failure(extractError(t)); } }); } /** * Add a given organisation to the MISP instance referenced by url in preferences. * * @param organisation organisation to add * @param callback {@link OrganisationCallback} wrapper to return the created organisation directly */ public void addOrganisation(Organisation organisation, final OrganisationCallback callback) { Call call = mispService.addOrganisation(organisation); call.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { assert response.body() != null; callback.success(response.body().organisation); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { callback.failure(t.getMessage()); } }); } // --- server routes --- /** * Get all servers on MISP instance. * * @param callback {@link OrganisationCallback} wrapper to return a list of servers directly */ public void getAllServers(final AllServersCallback callback) { Call> call = mispService.getAllServers(); call.enqueue(new Callback>() { @Override public void onResponse(@NonNull Call> call, @NonNull Response> response) { if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { List mispServers = response.body(); assert mispServers != null; Server[] servers = new Server[mispServers.size()]; for (int i = 0; i < servers.length; i++) { servers[i] = mispServers.get(i).getServer(); } callback.success(servers); } } @Override public void onFailure(@NonNull Call> call, @NonNull Throwable t) { callback.failure(t.getMessage()); } }); } public void getAllServers(final AllRawServersCallback callback) { Call> call = mispService.getAllServers(); call.enqueue(new Callback>() { @Override public void onResponse(@NonNull Call> call, @NonNull Response> response) { if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { callback.success(response.body()); } } @Override public void onFailure(@NonNull Call> call, @NonNull Throwable t) { callback.failure(t.getMessage()); } }); } /** * Add a server to the MISP instance * * @param server the server to create * @param callback {@link ServerCallback} wrapper to return the created server directly */ public void addServer(Server server, final ServerCallback callback) { Call call = mispService.addServer(server); call.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (!response.isSuccessful()) { callback.failure(extractError(response)); } else { callback.success(response.body()); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { callback.failure(t.getMessage()); throw new RuntimeException(t); } }); } // --- error parsing --- /** * Converts error {@link Response}s to human readable info. * * @param response erroneous response * @param type of response * @return human readable String that describes the error */ private String extractError(Response response) { switch (response.code()) { // bad request (malformed) case 400: return "Bad request"; // unauthorized case 401: return "Unauthorized"; // forbidden case 403: return "Authentification failed"; // not found case 404: return "Not found"; // No permission (method not allowed) case 405: return "No admin permission"; default: return response.message(); } } /** * Converts a {@link Throwable} to a human readable error message. * * @param t throwable * @return human readable String that describes the error. */ private String extractError(Throwable t) { if (t.getCause() instanceof CertificateException) { return "Trust anchor for certification path not found.\nSelf signed certificates are not supported."; } if (t instanceof SSLHandshakeException) { return "SSL Handshake Error"; } if (t instanceof NoRouteToHostException) { return "Server is not available (no route to host)"; } try { throw new Exception(t); } catch (Exception e) { e.printStackTrace(); } return t.getMessage(); } // interfaces public interface AvailableCallback { void available(); void unavailable(String error); } public interface UserCallback { void success(User user); void failure(String error); } public interface AllUsersCallback { void success(User[] users); void failure(String error); } public interface AllMispUsersCallback { void success(List users); void failure(String error); } public interface OrganisationCallback { void success(Organisation organisation); void failure(String error); } public interface AllOrganisationsCallback { void success(Organisation[] organisations); void failure(String error); } public interface ServerCallback { void success(Server server); void failure(String error); } public interface AllServersCallback { void success(Server[] servers); void failure(String error); } public interface AllRawServersCallback { void success(List mispServers); void failure(String error); } public interface AllRolesCallback { void success(Role[] roles); void failure(String error); } }