001package com.thetransactioncompany.jsonrpc2.server;
002
003
004import java.net.InetAddress;
005import java.net.URLConnection;
006import java.security.Principal;
007import java.security.cert.X509Certificate;
008
009import javax.net.ssl.HttpsURLConnection;
010import javax.servlet.http.HttpServletRequest;
011
012
013/**
014 * Context information about JSON-RPC 2.0 request and notification messages.
015 * This class is immutable.
016 *
017 * <ul>
018 *     <li>The client's host name.
019 *     <li>The client's IP address.
020 *     <li>Whether the request / notification was transmitted securely (e.g. 
021 *         via HTTPS).
022 *     <li>The client principal(s) (user), if authenticated.
023 * </ul>
024 *
025 * @author Vladimir Dzhuvinov
026 */
027public class MessageContext {
028
029
030        /** 
031         * The client hostname, {@code null} if none was specified.
032         */
033        private String clientHostName = null;
034
035        
036        /** 
037         * The client IP address, {@code null} if none was specified.
038         */
039        private String clientInetAddress = null;
040
041        
042        /** 
043         * Indicates whether the request was received over a secure channel
044         * (typically HTTPS). 
045         */
046        private boolean secure = false;
047        
048        
049        /**
050         * The authenticated client principals, {@code null} if none were 
051         * specified.
052         */
053        private Principal[] principals = null;
054        
055        
056        /**
057         * Minimal implementation of the {@link java.security.Principal} 
058         * interface.
059         */
060        public class BasicPrincipal implements Principal {
061        
062                /**
063                 * The principal name.
064                 */
065                private String name;
066        
067        
068                /**
069                 * Creates a new principal.
070                 *
071                 * @param name The principal name, must not be {@code null} or
072                 *             empty string.
073                 *
074                 * @throws IllegalArgumentException On a {@code null} or empty 
075                 *                                  principal name.
076                 */
077                public BasicPrincipal(final String name) {
078                
079                        if (name == null || name.trim().isEmpty())
080                                throw new IllegalArgumentException("The principal name must be defined");
081                
082                        this.name = name;
083                }
084        
085        
086                /**
087                 * Checks for equality.
088                 *
089                 * @param another The object to compare to.
090                 */
091                public boolean equals(final Object another) {
092                
093                        return another != null &&
094                               another instanceof Principal && 
095                               ((Principal)another).getName().equals(this.getName());
096                }
097                
098                
099                /**
100                 * Returns a hash code for this principal.
101                 *
102                 * @return The hash code.
103                 */
104                public int hashCode() {
105                
106                        return getName().hashCode();
107                }
108                
109                
110                /**
111                 * Returns the principal name.
112                 *
113                 * @return The principal name.
114                 */
115                public String getName() {
116                        
117                        return name;
118                }
119        }
120        
121        
122        /**
123         * Creates a new JSON-RPC 2.0 request / notification context.
124         *
125         * @param clientHostName    The client host name, {@code null} if 
126         *                          unknown.
127         * @param clientInetAddress The client IP address, {@code null} if 
128         *                          unknown.
129         * @param secure            Specifies a request received over HTTPS.
130         * @param principalName     Specifies the authenticated client principle
131         *                          name, {@code null} if unknown. The name must
132         *                          not be an empty or blank string.
133         */
134        public MessageContext(final String clientHostName, 
135                              final String clientInetAddress, 
136                              final boolean secure, 
137                              final String principalName) {
138        
139                this.clientHostName = clientHostName;
140                this.clientInetAddress = clientInetAddress;
141                this.secure = secure;
142                
143                if (principalName != null) {
144                        principals = new Principal[1];
145                        principals[0] = new BasicPrincipal(principalName);
146                }
147        }
148        
149        
150        /**
151         * Creates a new JSON-RPC 2.0 request / notification context.
152         *
153         * @param clientHostName    The client host name, {@code null} if 
154         *                          unknown.
155         * @param clientInetAddress The client IP address, {@code null} if 
156         *                          unknown.
157         * @param secure            Specifies a request received over HTTPS.
158         * @param principalNames    Specifies the authenticated client principle
159         *                          names, {@code null} if unknown. The names
160         *                          must not be an empty or blank string.
161         */
162        public MessageContext(final String clientHostName, 
163                              final String clientInetAddress, 
164                              final boolean secure, 
165                              final String[] principalNames) {
166        
167                this.clientHostName = clientHostName;
168                this.clientInetAddress = clientInetAddress;
169                this.secure = secure;
170                
171                if (principalNames != null) {
172                        principals = new Principal[principalNames.length];
173                        
174                        for (int i=0; i < principals.length; i++)
175                                principals[0] = new BasicPrincipal(principalNames[i]);
176                }
177        }
178        
179        
180        /**
181         * Creates a new JSON-RPC 2.0 request / notification context. No 
182         * authenticated client principal is specified.
183         *
184         * @param clientHostName    The client host name, {@code null} if 
185         *                          unknown.
186         * @param clientInetAddress The client IP address, {@code null} if 
187         *                          unknown.
188         * @param secure            Specifies a request received over HTTPS.
189         */
190        public MessageContext(final String clientHostName, 
191                              final String clientInetAddress, 
192                              final boolean secure) {
193        
194                this.clientHostName = clientHostName;
195                this.clientInetAddress = clientInetAddress;
196                this.secure = secure;
197        }
198        
199        
200        /**
201         * Creates a new JSON-RPC 2.0 request / notification context. Indicates 
202         * an insecure transport (plain HTTP) and no authenticated client 
203         * principal.
204         *
205         * @param clientHostName    The client host name, {@code null} if 
206         *                          unknown.
207         * @param clientInetAddress The client IP address, {@code null} if 
208         *                          unknown.
209         */
210        public MessageContext(final String clientHostName, 
211                              final String clientInetAddress) {
212        
213                this.clientHostName = clientHostName;
214                this.clientInetAddress = clientInetAddress;
215                this.secure = false;
216        }
217        
218        
219        /**
220         * Creates a new JSON-RPC 2.0 request / notification context. Indicates 
221         * an insecure transport (plain HTTP) and no authenticated client 
222         * principal. Not client host name / IP is specified.
223         */
224        public MessageContext() {
225        
226                this.secure = false;
227        }
228        
229        
230        /**
231         * Creates a new JSON-RPC 2.0 request / notification context from the
232         * specified HTTP request.
233         *
234         * @param httpRequest The HTTP request.
235         */
236        public MessageContext(final HttpServletRequest httpRequest) {
237        
238                clientInetAddress = httpRequest.getRemoteAddr();
239        
240                clientHostName = httpRequest.getRemoteHost();
241                
242                if (clientHostName != null && clientHostName.equals(clientInetAddress))
243                        clientHostName = null; // not resolved actually
244                
245                secure = httpRequest.isSecure();
246
247                X509Certificate[] certs = (X509Certificate[])httpRequest.getAttribute("javax.servlet.request.X509Certificate");
248
249                if (certs != null && certs.length > 0) {     
250                        
251                        principals = new Principal[certs.length];
252                        
253                        for (int i=0; i < principals.length; i++)
254                                principals[i] = certs[i].getSubjectX500Principal();
255                }
256        }
257
258
259        /**
260         * Creates a new JSON-RPC 2.0 request / notification context from the
261         * specified URL connection. Use this constructor in cases when the 
262         * HTTP server is the origin of the JSON-RPC 2.0 requests / 
263         * notifications. If the IP address of the HTTP server cannot be 
264         * resolved {@link #getClientInetAddress} will return {@code null}.
265         *
266         * @param connection The URL connection, must be established and not
267         *                   {@code null}.
268         */
269        public MessageContext(final URLConnection connection) {
270
271                clientHostName = connection.getURL().getHost();
272
273                InetAddress ip = null;
274
275                if (clientHostName != null) {
276
277                        try {
278                                ip = InetAddress.getByName(clientHostName);
279
280                        } catch (Exception e) {
281
282                                // UnknownHostException, SecurityException 
283                                // ignore
284                        }
285                }
286
287                if (ip != null)
288                        clientInetAddress = ip.getHostAddress();
289
290
291                if (connection instanceof HttpsURLConnection) {
292
293                        secure = true;
294
295                        HttpsURLConnection httpsConnection = (HttpsURLConnection)connection;
296
297                        Principal prn = null;
298
299                        try {
300                                prn = httpsConnection.getPeerPrincipal();
301
302                        } catch (Exception e) {
303
304                                // SSLPeerUnverifiedException, IllegalStateException 
305                                // ignore
306                        }
307
308                        if (prn != null) {
309
310                                principals = new Principal[1];
311                                principals[0] = prn;
312                        }
313                }
314        }
315        
316        
317        /**
318         * Gets the host name of the client that sent the request / 
319         * notification.
320         *
321         * @return The client host name, {@code null} if unknown.
322         */
323        public String getClientHostName() {
324        
325                return clientHostName;
326        }
327        
328        
329        /**
330         * Gets the IP address of the client that sent the request /
331         * notification.
332         *
333         * @return The client IP address, {@code null} if unknown.
334         */
335        public String getClientInetAddress() {
336                
337                return clientInetAddress;
338        }
339         
340
341        /**
342         * Indicates whether the request / notification was received over a 
343         * secure HTTPS connection.
344         *
345         * @return {@code true} If the request was received over HTTPS, 
346         *         {@code false} if it was received over plain HTTP.
347         */
348        public boolean isSecure() {
349        
350                return secure;
351        }
352        
353        
354        /**
355         * Returns the first authenticated client principal, {@code null} if 
356         * none.
357         *
358         * @return The first client principal, {@code null} if none.
359         */
360        public Principal getPrincipal() {
361        
362                if (principals != null)
363                        return principals[0];
364                else
365                        return null;
366        }
367        
368        
369        /**
370         * Returns the authenticated client principals, {@code null} if 
371         * none.
372         *
373         * @return The client principals, {@code null} if none.
374         */
375        public Principal[] getPrincipals() {
376        
377                return principals;
378        }
379        
380        
381        /**
382         * Returns the first authenticated client principal name, {@code null} 
383         * if none.
384         *
385         * @return The first client principal name, {@code null} if none.
386         */
387        public String getPrincipalName() {
388        
389                if (principals != null)
390                        return principals[0].getName();
391                else
392                        return null;
393        }
394        
395        
396        /**
397         * Returns the authenticated client principal names, {@code null} 
398         * if none.
399         *
400         * @return The client principal names, {@code null} if none.
401         */
402        public String[] getPrincipalNames() {
403        
404                String[] names = new String[principals.length];
405                
406                for (int i=0; i < names.length; i++)
407                        names[i] = principals[i].getName();
408                
409                return names;
410        }
411
412
413        @Override
414        public String toString() {
415
416                String s = "[host=" + clientHostName + " hostIP=" + clientInetAddress + " secure=" + secure;
417
418                if (principals != null) {
419
420                        int i = 0;
421
422                        for (Principal p: principals)
423                                s += " principal[" + (i++) + "]=" + p;
424                }
425
426                return s + "]";
427        }
428}