/*
Copyright 2014 Groupon, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.groupon.odo.proxylib;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.groupon.odo.proxylib.models.Client;
import com.groupon.odo.proxylib.models.EndpointOverride;
import com.groupon.odo.proxylib.models.Group;
import com.groupon.odo.proxylib.models.Method;
import com.groupon.odo.proxylib.models.Script;
import com.groupon.odo.proxylib.models.ServerGroup;
import com.groupon.odo.proxylib.models.ServerRedirect;
import com.groupon.odo.proxylib.models.backup.Backup;
import com.groupon.odo.proxylib.models.backup.ConfigAndProfileBackup;
import com.groupon.odo.proxylib.models.backup.PathOverride;
import com.groupon.odo.proxylib.models.backup.Profile;
import com.groupon.odo.proxylib.models.backup.SingleProfileBackup;
import java.io.InputStream;
import java.net.InetAddress;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
import javax.net.ssl.HostnameVerifier;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@SuppressWarnings("deprecation")
public class BackupService {
private static final Logger logger = LoggerFactory.getLogger(BackupService.class);
private static BackupService serviceInstance = null;
public BackupService() {
}
public static BackupService getInstance() {
if (serviceInstance == null) {
serviceInstance = new BackupService();
}
return serviceInstance;
}
/**
* Get all Groups
*
* @return
* @throws Exception
*/
private List<Group> getGroups() throws Exception {
List<Group> groups = new ArrayList<Group>();
List<Group> sourceGroups = PathOverrideService.getInstance().findAllGroups();
// loop through the groups
for (Group sourceGroup : sourceGroups) {
Group group = new Group();
// add all methods
ArrayList<Method> methods = new ArrayList<Method>();
for (Method sourceMethod : EditService.getInstance().getMethodsFromGroupId(sourceGroup.getId(), null)) {
Method method = new Method();
method.setClassName(sourceMethod.getClassName());
method.setMethodName(sourceMethod.getMethodName());
methods.add(method);
}
group.setMethods(methods);
group.setName(sourceGroup.getName());
groups.add(group);
}
return groups;
}
private List<Profile> getProfiles() throws Exception {
ArrayList<Profile> profiles = new ArrayList<Profile>();
for (com.groupon.odo.proxylib.models.Profile sourceProfile : ProfileService.getInstance().findAllProfiles()) {
Profile profile = new Profile();
profile.setName(sourceProfile.getName());
// get paths
profile.setPaths(PathOverrideService.getInstance().getPaths(sourceProfile.getId(), Constants.PROFILE_CLIENT_DEFAULT_ID, null));
// get default servers
profile.setServers(ServerRedirectService.getInstance().tableServers(sourceProfile.getId(), 0));
//get server groups
profile.setServerGroups(ServerRedirectService.getInstance().tableServerGroups(sourceProfile.getId()));
// set active
profile.setActive(ProfileService.getInstance().isActive(sourceProfile.getId()));
profiles.add(profile);
}
return profiles;
}
/**
* Get the active overrides with parameters and the active server group for a client
*
* @param profileID Id of profile to get configuration for
* @param clientUUID Client Id to export configuration
* @return SingleProfileBackup containing active overrides and active server group
* @throws Exception exception
*/
public SingleProfileBackup getProfileBackupData(int profileID, String clientUUID) throws Exception {
SingleProfileBackup singleProfileBackup = new SingleProfileBackup();
List<PathOverride> enabledPaths = new ArrayList<>();
List<EndpointOverride> paths = PathOverrideService.getInstance().getPaths(profileID, clientUUID, null);
for (EndpointOverride override : paths) {
if (override.getRequestEnabled() || override.getResponseEnabled()) {
PathOverride pathOverride = new PathOverride();
pathOverride.setPathName(override.getPathName());
if (override.getRequestEnabled()) {
pathOverride.setRequestEnabled(true);
}
if (override.getResponseEnabled()) {
pathOverride.setResponseEnabled(true);
}
pathOverride.setEnabledEndpoints(override.getEnabledEndpoints());
enabledPaths.add(pathOverride);
}
}
singleProfileBackup.setEnabledPaths(enabledPaths);
Client backupClient = ClientService.getInstance().findClient(clientUUID, profileID);
ServerGroup activeServerGroup = ServerRedirectService.getInstance().getServerGroup(backupClient.getActiveServerGroup(), profileID);
singleProfileBackup.setActiveServerGroup(activeServerGroup);
return singleProfileBackup;
}
/**
* Get the single profile backup (active overrides and active server group) for a client
* and the full odo backup
*
* @param profileID Id of profile to get configuration for
* @param clientUUID Client Id to export configuration
* @return Odo backup and client backup
* @throws Exception exception
*/
public ConfigAndProfileBackup getConfigAndProfileData(int profileID, String clientUUID) throws Exception {
SingleProfileBackup singleProfileBackup = getProfileBackupData(profileID, clientUUID);
Backup backup = getBackupData();
ConfigAndProfileBackup configAndProfileBackup = new ConfigAndProfileBackup();
configAndProfileBackup.setOdoBackup(backup);
configAndProfileBackup.setProfileBackup(singleProfileBackup);
return configAndProfileBackup;
}
/**
* Return the structured backup data
*
* @return Backup of current configuration
* @throws Exception exception
*/
public Backup getBackupData() throws Exception {
Backup backupData = new Backup();
backupData.setGroups(getGroups());
backupData.setProfiles(getProfiles());
ArrayList<Script> scripts = new ArrayList<Script>();
Collections.addAll(scripts, ScriptService.getInstance().getScripts());
backupData.setScripts(scripts);
return backupData;
}
/**
* Restore configuration from backup data
*
* @param streamData InputStream for configuration to restore
* @return true if succeeded, false if operation failed
*/
public boolean restoreBackupData(InputStream streamData) {
// convert stream to string
java.util.Scanner s = new java.util.Scanner(streamData).useDelimiter("\\A");
String data = s.hasNext() ? s.next() : "";
// parse JSON
ObjectMapper mapper = new ObjectMapper();
Backup backupData = null;
try {
backupData = mapper.readValue(data, Backup.class);
} catch (Exception e) {
logger.error("Could not parse input data: {}, {}", e.getClass(), e.getMessage());
return false;
}
// TODO: validate json against a schema for safety
// GROUPS
try {
logger.info("Number of groups: {}", backupData.getGroups().size());
for (Group group : backupData.getGroups()) {
// determine if group already exists.. if not then add it
Integer groupId = PathOverrideService.getInstance().getGroupIdFromName(group.getName());
if (groupId == null) {
groupId = PathOverrideService.getInstance().addGroup(group.getName());
}
// get all methods from the group.. we are going to remove ones that don't exist in the new configuration
List<Method> originalMethods = EditService.getInstance().getMethodsFromGroupId(groupId, null);
for (Method originalMethod : originalMethods) {
Boolean matchInImportGroup = false;
int importCount = 0;
for (Method importMethod : group.getMethods()) {
if (originalMethod.getClassName().equals(importMethod.getClassName()) &&
originalMethod.getMethodName().equals(importMethod.getMethodName())) {
matchInImportGroup = true;
break;
}
importCount++;
}
if (!matchInImportGroup) {
// remove it from current database since it is a delta to the current import
PathOverrideService.getInstance().removeOverride(originalMethod.getId());
} else {
// remove from import list since it already exists
group.getMethods().remove(importCount);
}
}
// add methods to groups
for (Method method : group.getMethods()) {
PathOverrideService.getInstance().createOverride(groupId, method.getMethodName(), method.getClassName());
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// PROFILES
try {
logger.info("Number of profiles: {}", backupData.getProfiles().size());
// remove all servers
// don't care about deltas here.. we'll just recreate them all
// removed default servers (belong to group id=0)
ServerRedirectService.getInstance().deleteServerGroup(0);
for (com.groupon.odo.proxylib.models.backup.Profile profile : backupData.getProfiles()) {
// see if a profile with this name already exists
Integer profileId = ProfileService.getInstance().getIdFromName(profile.getName());
com.groupon.odo.proxylib.models.Profile newProfile;
if (profileId == null) {
// create new profile
newProfile = ProfileService.getInstance().add(profile.getName());
} else {
// get the existing profile
newProfile = ProfileService.getInstance().findProfile(profileId);
}
// add new servers
if (profile.getServers() != null) {
for (ServerRedirect server : profile.getServers()) {
ServerRedirectService.getInstance().addServerRedirect(server.getRegion(), server.getSrcUrl(), server.getDestUrl(), server.getHostHeader(), newProfile.getId(), 0);
}
}
// remove all server groups
for (ServerGroup group : ServerRedirectService.getInstance().tableServerGroups(newProfile.getId())) {
ServerRedirectService.getInstance().deleteServerGroup(group.getId());
}
// add new server groups
if (profile.getServerGroups() != null) {
for (ServerGroup group : profile.getServerGroups()) {
int groupId = ServerRedirectService.getInstance().addServerGroup(group.getName(), newProfile.getId());
for (ServerRedirect server : group.getServers()) {
ServerRedirectService.getInstance().addServerRedirect(server.getRegion(), server.getSrcUrl(), server.getDestUrl(), server.getHostHeader(), newProfile.getId(), groupId);
}
}
}
// remove all paths
// don't care about deltas here.. we'll just recreate them all
for (EndpointOverride path : PathOverrideService.getInstance().getPaths(newProfile.getId(), Constants.PROFILE_CLIENT_DEFAULT_ID, null)) {
PathOverrideService.getInstance().removePath(path.getPathId());
}
// add new paths
if (profile.getPaths() != null) {
for (EndpointOverride path : profile.getPaths()) {
int pathId = PathOverrideService.getInstance().addPathnameToProfile(newProfile.getId(), path.getPathName(), path.getPath());
PathOverrideService.getInstance().setContentType(pathId, path.getContentType());
PathOverrideService.getInstance().setRequestType(pathId, path.getRequestType());
PathOverrideService.getInstance().setGlobal(pathId, path.getGlobal());
// add groups to path
for (String groupName : path.getGroupNames()) {
int groupId = PathOverrideService.getInstance().getGroupIdFromName(groupName);
PathOverrideService.getInstance().AddGroupByNumber(newProfile.getId(), pathId, groupId);
}
}
}
// set active
ClientService.getInstance().updateActive(newProfile.getId(), Constants.PROFILE_CLIENT_DEFAULT_ID, profile.getActive());
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// SCRIPTS
try {
// delete all scripts
for (Script script : ScriptService.getInstance().getScripts()) {
ScriptService.getInstance().removeScript(script.getId());
}
// add scripts
for (Script script : backupData.getScripts()) {
ScriptService.getInstance().addScript(script.getName(), script.getScript());
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// tell http/https proxies to reload plugins
try {
org.apache.http.conn.ssl.SSLSocketFactory sslsf = new org.apache.http.conn.ssl.SSLSocketFactory(new TrustStrategy() {
@Override
public boolean isTrusted(
final X509Certificate[] chain, String authType) throws CertificateException {
// ignore SSL cert issues
return true;
}
});
HostnameVerifier hostnameVerifier = org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;
sslsf.setHostnameVerifier((X509HostnameVerifier) hostnameVerifier);
for (String connectstring : getConnectorStrings("https_proxy")) {
HttpGet request = new HttpGet(connectstring + "/proxy/reload");
HttpClient httpClient = new org.apache.http.impl.client.DefaultHttpClient();
String[] parts = connectstring.split(":");
httpClient.getConnectionManager().getSchemeRegistry().register(new org.apache.http.conn.scheme.Scheme("https", Integer.parseInt(parts[parts.length - 1]), sslsf));
HttpResponse response = httpClient.execute(request);
}
} catch (Exception e) {
e.printStackTrace();
logger.info("Exception caught during proxy reload. Things may be in an inconsistent state.");
}
// restart plugin service for this process
PluginManager.destroy();
return true;
}
/**
* Returns an MBeanServer with the specified name
*
* @param name
* @return
*/
private MBeanServer getServerForName(String name) {
try {
MBeanServer mbeanServer = null;
final ObjectName objectNameQuery = new ObjectName(name + ":type=Service,*");
for (final MBeanServer server : MBeanServerFactory.findMBeanServer(null)) {
if (server.queryNames(objectNameQuery, null).size() > 0) {
mbeanServer = server;
// we found it, bail out
break;
}
}
return mbeanServer;
} catch (Exception e) {
}
return null;
}
/**
* Get a list of strings(scheme + host + port) that the specified connector is running on
*
* @param name
* @return
*/
private ArrayList<String> getConnectorStrings(String name) {
ArrayList<String> connectorStrings = new ArrayList<String>();
try {
MBeanServer mbeanServer = getServerForName(name);
Set<ObjectName> objs = mbeanServer.queryNames(new ObjectName("*:type=Connector,*"), null);
String hostname = InetAddress.getLocalHost().getHostName();
InetAddress[] addresses = InetAddress.getAllByName(hostname);
for (Iterator<ObjectName> i = objs.iterator(); i.hasNext(); ) {
ObjectName obj = i.next();
String scheme = mbeanServer.getAttribute(obj, "scheme").toString();
String port = obj.getKeyProperty("port");
connectorStrings.add(scheme + "://localhost:" + port);
logger.info("Adding: {}", scheme + "://localhost:" + port);
}
} catch (Exception e) {
}
return connectorStrings;
}
/**
* @param groupName Name of server group to get ID for
* @param profileId Profile ID server group is in
* @return ID of group
*/
private int getServerIdFromName(String groupName, int profileId) {
List<ServerGroup> serverGroups = ServerRedirectService.getInstance().tableServerGroups(profileId);
if (groupName.equals("Default")) {
return 0;
}
for (ServerGroup group : serverGroups) {
if (groupName.compareTo(group.getName()) == 0) {
return group.getId();
}
}
return -1;
}
/**
* 1. Resets profile to get fresh slate
* 2. Updates active server group to one from json
* 3. For each path in json, sets request/response enabled
* 4. Adds active overrides to each path
* 5. Update arguments and repeat count for each override
*
* @param profileBackup JSON containing server configuration and overrides to activate
* @param profileId Profile to update
* @param clientUUID Client UUID to apply update to
* @throws Exception Array of errors for things that could not be imported
*/
public void setProfileFromBackup(JSONObject profileBackup, int profileId, String clientUUID) throws Exception {
// Reset the profile before applying changes
ClientService clientService = ClientService.getInstance();
clientService.reset(profileId, clientUUID);
clientService.updateActive(profileId, clientUUID, true);
JSONArray errors = new JSONArray();
// Change to correct server group
JSONObject activeServerGroup = profileBackup.getJSONObject(Constants.BACKUP_ACTIVE_SERVER_GROUP);
int activeServerId = getServerIdFromName(activeServerGroup.getString(Constants.NAME), profileId);
if (activeServerId == -1) {
errors.put(formErrorJson("Server Error", "Cannot change to '" + activeServerGroup.getString(Constants.NAME) + "' - Check Server Group Exists"));
} else {
Client clientToUpdate = ClientService.getInstance().findClient(clientUUID, profileId);
ServerRedirectService.getInstance().activateServerGroup(activeServerId, clientToUpdate.getId());
}
JSONArray enabledPaths = profileBackup.getJSONArray(Constants.ENABLED_PATHS);
PathOverrideService pathOverrideService = PathOverrideService.getInstance();
OverrideService overrideService = OverrideService.getInstance();
for (int i = 0; i < enabledPaths.length(); i++) {
JSONObject path = enabledPaths.getJSONObject(i);
int pathId = pathOverrideService.getPathId(path.getString(Constants.PATH_NAME), profileId);
// Set path to have request/response enabled as necessary
try {
if (path.getBoolean(Constants.REQUEST_ENABLED)) {
pathOverrideService.setRequestEnabled(pathId, true, clientUUID);
}
if (path.getBoolean(Constants.RESPONSE_ENABLED)) {
pathOverrideService.setResponseEnabled(pathId, true, clientUUID);
}
} catch (Exception e) {
errors.put(formErrorJson("Path Error", "Cannot update path: '" + path.getString(Constants.PATH_NAME) + "' - Check Path Exists"));
continue;
}
JSONArray enabledOverrides = path.getJSONArray(Constants.ENABLED_ENDPOINTS);
/**
* 2 for loops to ensure overrides are added with correct priority
* 1st loop is priority currently adding override to
* 2nd loop is to find the override with matching priority in profile json
*/
for (int j = 0; j < enabledOverrides.length(); j++) {
for (int k = 0; k < enabledOverrides.length(); k++) {
JSONObject override = enabledOverrides.getJSONObject(k);
if (override.getInt(Constants.PRIORITY) != j) {
continue;
}
int overrideId;
// Name of method that can be used by error message as necessary later
String overrideNameForError = "";
// Get the Id of the override
try {
// If method information is null, then the override is a default override
if (override.get(Constants.METHOD_INFORMATION) != JSONObject.NULL) {
JSONObject methodInformation = override.getJSONObject(Constants.METHOD_INFORMATION);
overrideNameForError = methodInformation.getString(Constants.METHOD_NAME);
overrideId = overrideService.getOverrideIdForMethod(methodInformation.getString(Constants.CLASS_NAME),
methodInformation.getString(Constants.METHOD_NAME));
} else {
overrideNameForError = "Default Override";
overrideId = override.getInt(Constants.OVERRIDE_ID);
}
// Enable override and set repeat number and arguments
overrideService.enableOverride(overrideId, pathId, clientUUID);
int overrideOrdinal = overrideService.getCurrentMethodOrdinal(overrideId, pathId, clientUUID, null);
overrideService.updateRepeatNumber(overrideId, pathId, overrideOrdinal, override.getInt(Constants.REPEAT_NUMBER), clientUUID);
overrideService.updateArguments(overrideId, pathId, overrideOrdinal, override.getString(Constants.ARGUMENTS), clientUUID);
// If it has responseCode then update the override with the responseCode. Otherwise, use 200 as response code
try {
overrideService.updateResponseCode(overrideId, pathId, overrideOrdinal, override.getString(Constants.RESPONSE_CODE), clientUUID);
} catch (JSONException e) {
overrideService.updateResponseCode(overrideId, pathId, overrideOrdinal, "200", clientUUID);
}
} catch (Exception e) {
errors.put(formErrorJson("Override Error", "Cannot add/update override: '" + overrideNameForError + "' - Check Override Exists"));
continue;
}
}
}
}
// Throw exception if any errors occured
if (errors.length() > 0) {
throw new Exception(errors.toString());
}
}
private JSONObject formErrorJson(String type, String errorMessage) throws Exception {
JSONObject error = new JSONObject();
error.put("type", type);
error.put("error", errorMessage);
return error;
}
}