001 package org.cumulus4j.keymanager.api.internal.remote; 002 003 import java.io.IOException; 004 import java.io.InputStreamReader; 005 import java.io.Reader; 006 import java.nio.charset.Charset; 007 import java.util.Collections; 008 import java.util.HashMap; 009 import java.util.Map; 010 011 import javax.ws.rs.core.MediaType; 012 import javax.ws.rs.core.Response.Status; 013 014 import org.cumulus4j.keymanager.api.AuthenticationException; 015 import org.cumulus4j.keymanager.api.CannotDeleteLastUserException; 016 import org.cumulus4j.keymanager.api.CryptoSession; 017 import org.cumulus4j.keymanager.api.DateDependentKeyStrategyInitParam; 018 import org.cumulus4j.keymanager.api.DateDependentKeyStrategyInitResult; 019 import org.cumulus4j.keymanager.api.KeyManagerAPIConfiguration; 020 import org.cumulus4j.keymanager.api.KeyManagerAPIInstantiationException; 021 import org.cumulus4j.keymanager.api.KeyStoreNotEmptyException; 022 import org.cumulus4j.keymanager.api.internal.AbstractKeyManagerAPI; 023 import org.cumulus4j.keymanager.front.shared.AcquireCryptoSessionResponse; 024 import org.cumulus4j.keymanager.front.shared.AppServer; 025 import org.cumulus4j.keymanager.front.shared.Error; 026 import org.cumulus4j.keymanager.front.shared.PutAppServerResponse; 027 import org.cumulus4j.keymanager.front.shared.UserWithPassword; 028 029 import com.sun.jersey.api.client.Client; 030 import com.sun.jersey.api.client.ClientHandlerException; 031 import com.sun.jersey.api.client.ClientResponse; 032 import com.sun.jersey.api.client.UniformInterfaceException; 033 import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter; 034 035 /** 036 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de 037 */ 038 public class RemoteKeyManagerAPI extends AbstractKeyManagerAPI 039 { 040 private Map<String, AppServer> appServerBaseURL2appServer = Collections.synchronizedMap(new HashMap<String, AppServer>()); 041 042 public RemoteKeyManagerAPI() 043 throws KeyManagerAPIInstantiationException 044 { 045 // We test here, whether the AcquireCryptoSessionResponse and some other classes are accessible. If they are not, it means the remote stuff is not deployed 046 // and it should not be possible to instantiate a RemoteKeyManagerAPI. 047 try { 048 AcquireCryptoSessionResponse.class.getConstructors(); 049 } catch (NoClassDefFoundError x) { 050 throw new KeyManagerAPIInstantiationException("The RemoteKeyManagerAPI could not be instantiated! If you really want to use a key-server, make sure all required libs are deployed. If you want to use a local key-store instead of a key-server, you must specify different arguments. It seems, the module 'org.cumulus4j.keymanager.front.shared' is missing! " + x, x); 051 } 052 053 try { 054 com.sun.jersey.core.header.AcceptableMediaType.class.getConstructors(); 055 } catch (NoClassDefFoundError x) { 056 throw new KeyManagerAPIInstantiationException("The RemoteKeyManagerAPI could not be instantiated! If you really want to use a key-server, make sure all required libs are deployed. If you want to use a local key-store instead of a key-server, you must specify different arguments. It seems, the module 'jersey-core' is missing! " + x, x); 057 } 058 059 try { 060 Client.class.getConstructors(); 061 } catch (NoClassDefFoundError x) { 062 throw new KeyManagerAPIInstantiationException("The RemoteKeyManagerAPI could not be instantiated! If you really want to use a key-server, make sure all required libs are deployed. If you want to use a local key-store instead of a key-server, you must specify different arguments. It seems, the module 'jersey-client' is missing! " + x, x); 063 } 064 } 065 066 @Override 067 public void setConfiguration(KeyManagerAPIConfiguration configuration) throws IllegalArgumentException, KeyManagerAPIInstantiationException 068 { 069 super.setConfiguration(configuration); 070 appServerBaseURL2appServer.clear(); 071 } 072 073 protected static final String appendFinalSlash(String url) 074 { 075 if (url.endsWith("/")) 076 return url; 077 else 078 return url + '/'; 079 } 080 081 private Client client; 082 083 protected synchronized Client getClient() 084 { 085 // A client is thread-safe except for configuration changes (but we don't change the configuration of the returned client anymore). 086 if (client == null) { 087 Client client = new Client(); 088 client.addFilter( 089 new HTTPBasicAuthFilter(getAuthUserName(), new String(getAuthPassword())) 090 ); 091 this.client = client; 092 } 093 return client; 094 } 095 096 @Override 097 public DateDependentKeyStrategyInitResult initDateDependentKeyStrategy(DateDependentKeyStrategyInitParam param) throws KeyStoreNotEmptyException, IOException 098 { 099 org.cumulus4j.keymanager.front.shared.DateDependentKeyStrategyInitParam ksInitParam = new org.cumulus4j.keymanager.front.shared.DateDependentKeyStrategyInitParam(); 100 ksInitParam.setKeyActivityPeriodMSec(param.getKeyActivityPeriodMSec()); 101 ksInitParam.setKeyStorePeriodMSec(param.getKeyStorePeriodMSec()); 102 103 org.cumulus4j.keymanager.front.shared.DateDependentKeyStrategyInitResult r; 104 try { 105 r = getClient().resource(appendFinalSlash(getKeyManagerBaseURL()) + "DateDependentKeyStrategy/" + getKeyStoreID() + "/init") 106 .type(MediaType.APPLICATION_XML_TYPE) 107 .post(org.cumulus4j.keymanager.front.shared.DateDependentKeyStrategyInitResult.class, ksInitParam); 108 } catch (UniformInterfaceException x) { 109 RemoteKeyManagerAPI.throwUniformInterfaceExceptionAsKeyStoreNotEmptyException(x); 110 RemoteKeyManagerAPI.throwUniformInterfaceExceptionAsIOException(x); 111 throw new IOException(x); 112 } 113 114 DateDependentKeyStrategyInitResult result = new DateDependentKeyStrategyInitResult(); 115 result.setGeneratedKeyCount(r.getGeneratedKeyCount()); 116 return result; 117 } 118 119 @Override 120 public void putUser(String userName, char[] password) throws AuthenticationException, IOException 121 { 122 try { 123 UserWithPassword userWithPassword = new UserWithPassword(); 124 userWithPassword.setUserName(userName); 125 userWithPassword.setPassword(password.toString()); 126 127 getClient().resource(appendFinalSlash(getKeyManagerBaseURL()) + "User/" + getKeyStoreID()) 128 .type(MediaType.APPLICATION_XML_TYPE) 129 .put(userWithPassword); 130 } catch (UniformInterfaceException x) { 131 RemoteKeyManagerAPI.throwUniformInterfaceExceptionAsAuthenticationException(x); 132 RemoteKeyManagerAPI.throwUniformInterfaceExceptionAsIOException(x); 133 throw new IOException(x); 134 // } catch (IOException x) { 135 // throw x; 136 } 137 138 // If we changed the current user's password, we automatically re-configure this API instance. 139 KeyManagerAPIConfiguration conf = getConf(); 140 if (conf.getAuthUserName() != null && conf.getAuthUserName().equals(userName)) { 141 KeyManagerAPIConfiguration newConf = new KeyManagerAPIConfiguration(conf); 142 newConf.setAuthPassword(password); 143 try { 144 setConfiguration(newConf); 145 } catch (KeyManagerAPIInstantiationException e) { 146 throw new RuntimeException(e); // Shouldn't happen, because we copied the old configuration. 147 } 148 } 149 } 150 151 @Override 152 public void deleteUser(String userName) throws AuthenticationException, CannotDeleteLastUserException, IOException 153 { 154 try { 155 getClient().resource(appendFinalSlash(getKeyManagerBaseURL()) + "User/" + getKeyStoreID() + '/' + userName) 156 .type(MediaType.APPLICATION_XML_TYPE) 157 .delete(); 158 } catch (UniformInterfaceException x) { 159 RemoteKeyManagerAPI.throwUniformInterfaceExceptionAsAuthenticationException(x); 160 RemoteKeyManagerAPI.throwUniformInterfaceExceptionAsIOException(x); 161 throw new IOException(x); 162 // } catch (IOException x) { 163 // throw x; 164 } 165 } 166 167 @Override 168 public CryptoSession getCryptoSession(String appServerBaseURL) throws IOException, AuthenticationException 169 { 170 try { 171 AppServer appServer = appServerBaseURL2appServer.get(appServerBaseURL); 172 if (appServer == null) { 173 // Even if multiple threads run into this clause, the key-server will return 174 // the same appServerID for all of them. 175 appServer = new AppServer(); 176 appServer.setAppServerBaseURL(appServerBaseURL); 177 178 PutAppServerResponse putAppServerResponse = getClient().resource(appendFinalSlash(getKeyManagerBaseURL()) + "AppServer/" + getKeyStoreID()) 179 .accept(MediaType.APPLICATION_XML_TYPE) 180 .type(MediaType.APPLICATION_XML_TYPE) 181 .put(PutAppServerResponse.class, appServer); 182 183 if (putAppServerResponse == null) 184 throw new IOException("Key server returned null instead of a PutAppServerResponse when putting an AppServer instance!"); 185 186 if (putAppServerResponse.getAppServerID() == null) 187 throw new IOException("Key server returned a PutAppServerResponse with property appServerID being null!"); 188 189 appServer.setAppServerID(putAppServerResponse.getAppServerID()); 190 appServerBaseURL2appServer.put(appServerBaseURL, appServer); 191 } 192 193 RemoteCryptoSession session = new RemoteCryptoSession(this, appServer); 194 195 // // Try to open the session already now, so that we know already here, whether this works (but lock it immediately, again). 196 // session.acquire(); 197 // session.release(); 198 199 return session; 200 } catch (UniformInterfaceException x) { 201 RemoteKeyManagerAPI.throwUniformInterfaceExceptionAsAuthenticationException(x); 202 RemoteKeyManagerAPI.throwUniformInterfaceExceptionAsIOException(x); 203 throw new IOException(x); 204 } catch (IOException x) { 205 throw x; 206 } 207 } 208 209 protected static void throwUniformInterfaceExceptionAsAuthenticationException(UniformInterfaceException x) 210 throws AuthenticationException 211 { 212 if (x.getResponse().getStatus() != Status.FORBIDDEN.getStatusCode()) 213 return; 214 215 x.getResponse().bufferEntity(); 216 if (x.getResponse().hasEntity()) 217 { 218 try { 219 Error error = x.getResponse().getEntity(Error.class); 220 if ( 221 AuthenticationException.class.getName().equals(error.getType()) || 222 org.cumulus4j.keystore.AuthenticationException.class.getName().equals(error.getType()) 223 ) 224 throw new AuthenticationException(error.getMessage()); 225 } catch (ClientHandlerException e) { 226 //parsing the result failed => returning it as a String 227 String message = getClientResponseEntityAsString(x.getResponse()); 228 throw new AuthenticationException("URL=\"" + x.getResponse().getLocation() + "\": Server replied with error code " + x.getResponse().getStatus() + " and message: " + message); 229 } 230 } 231 232 throw new AuthenticationException("URL=\"" + x.getResponse().getLocation() + "\": Server replied with error code " + x.getResponse().getStatus() + "!"); 233 } 234 235 private static String getClientResponseEntityAsString(ClientResponse response) 236 { 237 Reader reader = new InputStreamReader(response.getEntityInputStream(), Charset.forName("UTF-8")); 238 StringBuilder sb = new StringBuilder(); 239 char[] cb = new char[1024]; 240 int bytesRead; 241 try { 242 while (0 <= (bytesRead = reader.read(cb))) { 243 sb.append(cb, 0, bytesRead); 244 } 245 } catch (IOException x) { // this comes from the Reader API and should be safe to ignore, because we buffer the entity and should thus not encounter any socket-read-error here. 246 throw new RuntimeException(x); 247 } 248 return sb.toString(); 249 } 250 251 protected static void throwUniformInterfaceExceptionAsIOException(UniformInterfaceException x) 252 throws IOException 253 { 254 x.getResponse().bufferEntity(); 255 if (x.getResponse().hasEntity()) { 256 try { 257 Error error = x.getResponse().getEntity(Error.class); 258 throw new IOException(error.getMessage()); 259 } catch (ClientHandlerException e) { 260 //parsing the result failed => returning it as a String 261 String message = getClientResponseEntityAsString(x.getResponse()); 262 throw new IOException("URL=\"" + x.getResponse().getLocation() + "\": Server replied with error code " + x.getResponse().getStatus() + " and message: " + message); 263 } 264 } 265 266 if (x.getResponse().getStatus() >= 400) 267 throw new IOException("URL=\"" + x.getResponse().getLocation() + "\": Server replied with error code " + x.getResponse().getStatus() + "!"); 268 } 269 270 private static void throwUniformInterfaceExceptionAsKeyStoreNotEmptyException(UniformInterfaceException x) 271 throws KeyStoreNotEmptyException 272 { 273 if (x.getResponse().getStatus() < 400) // Every code < 400 means success => return without trying to read an Error. 274 return; 275 276 x.getResponse().bufferEntity(); 277 if (x.getResponse().hasEntity()) 278 { 279 try { 280 Error error = x.getResponse().getEntity(Error.class); 281 if (org.cumulus4j.keystore.KeyStoreNotEmptyException.class.getName().equals(error.getType())) 282 throw new KeyStoreNotEmptyException(error.getMessage()); 283 } catch (ClientHandlerException e) { 284 //parsing the result failed => ignore it silently 285 doNothing(); 286 } 287 } 288 } 289 290 private static final void doNothing() { } 291 }