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 }