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    }