fixed interval time in keepalive resource and logic to disconnect after sign up
[iotivity.git] / cloud / interface / src / main / java / org / iotivity / cloud / ciserver / DeviceServerSystem.java
1 /*
2  * //******************************************************************
3  * //
4  * // Copyright 2016 Samsung Electronics All Rights Reserved.
5  * //
6  * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
7  * //
8  * // Licensed under the Apache License, Version 2.0 (the "License");
9  * // you may not use this file except in compliance with the License.
10  * // You may obtain a copy of the License at
11  * //
12  * //      http://www.apache.org/licenses/LICENSE-2.0
13  * //
14  * // Unless required by applicable law or agreed to in writing, software
15  * // distributed under the License is distributed on an "AS IS" BASIS,
16  * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * // See the License for the specific language governing permissions and
18  * // limitations under the License.
19  * //
20  * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
21  */
22 package org.iotivity.cloud.ciserver;
23
24 import java.util.HashMap;
25 import java.util.Iterator;
26 import java.util.List;
27
28 import org.iotivity.cloud.base.OICConstants;
29 import org.iotivity.cloud.base.ServerSystem;
30 import org.iotivity.cloud.base.connector.ConnectorPool;
31 import org.iotivity.cloud.base.device.CoapDevice;
32 import org.iotivity.cloud.base.device.Device;
33 import org.iotivity.cloud.base.device.IRequestChannel;
34 import org.iotivity.cloud.base.exception.ClientException;
35 import org.iotivity.cloud.base.exception.ServerException;
36 import org.iotivity.cloud.base.exception.ServerException.BadOptionException;
37 import org.iotivity.cloud.base.exception.ServerException.BadRequestException;
38 import org.iotivity.cloud.base.exception.ServerException.InternalServerErrorException;
39 import org.iotivity.cloud.base.exception.ServerException.UnAuthorizedException;
40 import org.iotivity.cloud.base.protocols.MessageBuilder;
41 import org.iotivity.cloud.base.protocols.coap.CoapRequest;
42 import org.iotivity.cloud.base.protocols.coap.CoapResponse;
43 import org.iotivity.cloud.base.protocols.coap.CoapSignaling;
44 import org.iotivity.cloud.base.protocols.enums.ContentFormat;
45 import org.iotivity.cloud.base.protocols.enums.RequestMethod;
46 import org.iotivity.cloud.base.protocols.enums.ResponseStatus;
47 import org.iotivity.cloud.base.protocols.enums.SignalingMethod;
48 import org.iotivity.cloud.base.server.CoapServer;
49 import org.iotivity.cloud.base.server.HttpServer;
50 import org.iotivity.cloud.base.server.Server;
51 import org.iotivity.cloud.base.server.WebSocketServer;
52 import org.iotivity.cloud.util.Bytes;
53 import org.iotivity.cloud.util.Cbor;
54 import org.iotivity.cloud.util.Log;
55
56 import io.netty.channel.ChannelDuplexHandler;
57 import io.netty.channel.ChannelHandler.Sharable;
58 import io.netty.channel.ChannelHandlerContext;
59 import io.netty.channel.ChannelInboundHandlerAdapter;
60 import io.netty.channel.ChannelPromise;
61
62 /**
63  *
64  * This class provides a set of APIs to manage all of request
65  *
66  */
67
68 public class DeviceServerSystem extends ServerSystem {
69
70     private Cbor<HashMap<String, Object>>                 mCbor     = new Cbor<HashMap<String, Object>>();
71     private HashMap<ChannelHandlerContext, CoapSignaling> mCsmMap   = new HashMap<>();
72
73     IRequestChannel                                       mRDServer = null;
74
75     public DeviceServerSystem() {
76         mRDServer = ConnectorPool.getConnection("rd");
77     }
78
79     /**
80      *
81      * This class provides a set of APIs to manage device pool.
82      *
83      */
84     public static class CoapDevicePool {
85         HashMap<String, Device> mMapDevice = new HashMap<>();
86
87         /**
88          * API for adding device information into pool.
89          * 
90          * @param device
91          *            device to be added
92          */
93         public void addDevice(Device device) {
94             String deviceId = ((CoapDevice) device).getDeviceId();
95             synchronized (mMapDevice) {
96                 mMapDevice.put(deviceId, device);
97             }
98         }
99
100         /**
101          * API for removing device information into pool.
102          * 
103          * @param device
104          *            device to be removed
105          */
106         public void removeDevice(Device device) throws ClientException {
107             String deviceId = ((CoapDevice) device).getDeviceId();
108             synchronized (mMapDevice) {
109                 if (mMapDevice.get(deviceId) == device) {
110                     mMapDevice.remove(deviceId);
111                 }
112             }
113             removeObserveDevice(device);
114         }
115
116         private void removeObserveDevice(Device device) throws ClientException {
117             synchronized (mMapDevice) {
118                 Iterator<String> iterator = mMapDevice.keySet().iterator();
119                 while (iterator.hasNext()) {
120                     String deviceId = iterator.next();
121                     CoapDevice getDevice = (CoapDevice) queryDevice(deviceId);
122                     getDevice.removeObserveChannel(
123                             ((CoapDevice) device).getRequestChannel());
124                 }
125             }
126         }
127
128         /**
129          * API for getting device information.
130          * 
131          * @param deviceId
132          *            device id to get device
133          */
134         public Device queryDevice(String deviceId) {
135             Device device = null;
136             synchronized (mMapDevice) {
137                 device = mMapDevice.get(deviceId);
138             }
139             return device;
140         }
141     }
142
143     CoapDevicePool mDevicePool = new CoapDevicePool();
144
145     /**
146      *
147      * This class provides a set of APIs to manage life cycle of coap message.
148      *
149      */
150     @Sharable
151     class CoapLifecycleHandler extends ChannelDuplexHandler {
152         @Override
153         public void channelRead(ChannelHandlerContext ctx, Object msg) {
154
155             if (msg instanceof CoapRequest) {
156                 try {
157                     CoapDevice coapDevice = (CoapDevice) ctx.channel()
158                             .attr(keyDevice).get();
159
160                     if (coapDevice.isExpiredTime()) {
161                         throw new UnAuthorizedException("token is expired");
162                     }
163
164                     CoapRequest coapRequest = (CoapRequest) msg;
165                     IRequestChannel targetChannel = null;
166                     String urlPath = coapRequest.getUriPath();
167
168                     if (urlPath == null) {
169                         throw new InternalServerErrorException(
170                                 "request uriPath is null");
171                     }
172
173                     if (urlPath.contains(Constants.ROUTE_FULL_URI)) {
174
175                         int RouteResourcePathSize = Constants.ROUTE_FULL_URI
176                                 .split("/").length;
177                         List<String> uriPath = coapRequest.getUriPathSegments();
178                         if (uriPath != null && !uriPath.isEmpty()) {
179                             CoapDevice targetDevice = (CoapDevice) mDevicePool
180                                     .queryDevice(uriPath
181                                             .get(RouteResourcePathSize - 1));
182                             targetChannel = targetDevice.getRequestChannel();
183                         }
184
185                         switch (coapRequest.getObserve()) {
186                             case SUBSCRIBE:
187                                 coapDevice.addObserveRequest(
188                                         Bytes.bytesToLong(coapRequest.getToken()),
189                                         coapRequest);
190                                 coapDevice.addObserveChannel(targetChannel);
191                                 break;
192                             case UNSUBSCRIBE:
193                                 coapDevice.removeObserveChannel(targetChannel);
194                                 coapDevice.removeObserveRequest(
195                                         Bytes.bytesToLong(coapRequest.getToken()));
196                                 break;
197                             default:
198                                 break;
199                         }
200                     }
201                 } catch (Throwable t) {
202                     Log.f(ctx.channel(), t);
203                     ResponseStatus responseStatus = t instanceof ServerException
204                             ? ((ServerException) t).getErrorResponse()
205                             : ResponseStatus.INTERNAL_SERVER_ERROR;
206                     ctx.writeAndFlush(MessageBuilder
207                             .createResponse((CoapRequest) msg, responseStatus));
208                     ctx.close();
209                 }
210             }
211             ctx.fireChannelRead(msg);
212         }
213
214         @Override
215         public void write(ChannelHandlerContext ctx, Object msg,
216                 ChannelPromise promise) throws Exception {
217
218             boolean bCloseConnection = false;
219
220             if (msg instanceof CoapResponse) {
221                 // This is CoapResponse
222                 // Once the response is valid, add this to deviceList
223                 CoapResponse response = (CoapResponse) msg;
224
225                 String urlPath = response.getUriPath();
226
227                 if (urlPath == null) {
228                     throw new InternalServerErrorException(
229                             "request uriPath is null");
230                 }
231
232                 switch (urlPath) {
233                     case OICConstants.ACCOUNT_SESSION_FULL_URI:
234                         if (response.getStatus() != ResponseStatus.CHANGED) {
235                             bCloseConnection = true;
236                         }
237                         break;
238                     case OICConstants.ACCOUNT_FULL_URI:
239                         if (response.getStatus() == ResponseStatus.DELETED) {
240                             bCloseConnection = true;
241                         }
242                         break;
243                 }
244             }
245
246             ctx.writeAndFlush(msg);
247
248             if (bCloseConnection == true) {
249                 ctx.close();
250             }
251         }
252
253         @Override
254         public void channelActive(ChannelHandlerContext ctx) {
255             Device device = ctx.channel().attr(keyDevice).get();
256             // Authenticated device connected
257
258             sendDevicePresence(device.getDeviceId(), "on");
259             mDevicePool.addDevice(device);
260
261             device.onConnected();
262         }
263
264         @Override
265         public void channelInactive(ChannelHandlerContext ctx)
266                 throws ClientException {
267             Device device = ctx.channel().attr(keyDevice).get();
268
269             // Some cases, this event occurs after new device connected using
270             // same di.
271             // So compare actual value, and remove if same.
272             if (device != null) {
273                 sendDevicePresence(device.getDeviceId(), "off");
274
275                 device.onDisconnected();
276
277                 mDevicePool.removeDevice(device);
278                 ctx.channel().attr(keyDevice).remove();
279
280             }
281         }
282
283         /**
284          * API for sending state to resource directory
285          * 
286          * @param deviceId
287          *            device id to be sent to resource directory
288          * @param state
289          *            device state to be sent to resource directory
290          */
291         public void sendDevicePresence(String deviceId, String state) {
292
293             Cbor<HashMap<String, Object>> cbor = new Cbor<>();
294             HashMap<String, Object> payload = new HashMap<String, Object>();
295             payload.put(Constants.REQ_DEVICE_ID, deviceId);
296             payload.put(Constants.PRESENCE_STATE, state);
297             StringBuffer uriPath = new StringBuffer();
298             uriPath.append("/" + Constants.PREFIX_OIC);
299             uriPath.append("/" + Constants.DEVICE_PRESENCE_URI);
300             mRDServer.sendRequest(MessageBuilder.createRequest(
301                     RequestMethod.POST, uriPath.toString(), null,
302                     ContentFormat.APPLICATION_CBOR,
303                     cbor.encodingPayloadToCbor(payload)), null);
304         }
305     }
306
307     CoapLifecycleHandler mLifeCycleHandler = new CoapLifecycleHandler();
308
309     @Sharable
310     class CoapAuthHandler extends ChannelDuplexHandler {
311
312         @Override
313         public void channelActive(ChannelHandlerContext ctx) {
314             // Actual channel active should decided after authentication.
315         }
316
317         @Override
318         public void write(ChannelHandlerContext ctx, Object msg,
319                 ChannelPromise promise) {
320             try {
321
322                 if (!(msg instanceof CoapResponse)) {
323                     // throw new BadRequestException(
324                     // "this msg type is not CoapResponse");
325
326                     // TODO check websocket handshake response
327                     ctx.writeAndFlush(msg);
328                     return;
329                 }
330                 // This is CoapResponse
331                 // Once the response is valid, add this to deviceList
332
333                 CoapResponse response = (CoapResponse) msg;
334
335                 String urlPath = response.getUriPath();
336
337                 if (urlPath == null) {
338                     throw new InternalServerErrorException(
339                             "request uriPath is null");
340                 }
341
342                 switch (urlPath) {
343
344                     case OICConstants.ACCOUNT_FULL_URI:
345                         ctx.writeAndFlush(msg);
346                         ctx.close();
347                         return;
348
349                     case OICConstants.ACCOUNT_SESSION_FULL_URI:
350                         HashMap<String, Object> payloadData = mCbor
351                                 .parsePayloadFromCbor(response.getPayload(),
352                                         HashMap.class);
353
354                         if (response.getStatus() != ResponseStatus.CHANGED) {
355                             throw new UnAuthorizedException();
356                         }
357
358                         if (payloadData == null) {
359                             throw new BadRequestException("payload is empty");
360                         }
361                         int remainTime = (int) payloadData
362                                 .get(Constants.EXPIRES_IN);
363
364                         Device device = ctx.channel().attr(keyDevice).get();
365                         ((CoapDevice) device).setExpiredPolicy(remainTime);
366
367                         // Remove current auth handler and replace to
368                         // LifeCycleHandle
369                         ctx.channel().pipeline().replace(this,
370                                 "LifeCycleHandler", mLifeCycleHandler);
371
372                         // Raise event that we have Authenticated device
373                         ctx.fireChannelActive();
374
375                         break;
376                 }
377
378                 ctx.writeAndFlush(msg);
379
380             } catch (Throwable t) {
381                 Log.f(ctx.channel(), t);
382                 ctx.writeAndFlush(msg);
383                 ctx.close();
384             }
385         }
386
387         @Override
388         public void channelRead(ChannelHandlerContext ctx, Object msg) {
389             try {
390                 if (!(msg instanceof CoapRequest)) {
391                     throw new BadRequestException(
392                             "this msg type is not CoapRequest");
393                 }
394
395                 // And check first response is VALID then add or cut
396                 CoapRequest request = (CoapRequest) msg;
397
398                 String urlPath = request.getUriPath();
399
400                 if (urlPath == null) {
401                     throw new InternalServerErrorException(
402                             "request uriPath is null");
403                 }
404
405                 switch (urlPath) {
406                     // Check whether request is about account
407                     case OICConstants.ACCOUNT_FULL_URI:
408                     case OICConstants.ACCOUNT_TOKENREFRESH_FULL_URI:
409
410                         if (ctx.channel().attr(keyDevice).get() == null) {
411                             // Create device first and pass to upperlayer
412                             Device device = new CoapDevice(ctx);
413                             ctx.channel().attr(keyDevice).set(device);
414                         }
415
416                         break;
417
418                     case OICConstants.ACCOUNT_SESSION_FULL_URI:
419
420                         HashMap<String, Object> authPayload = mCbor
421                                 .parsePayloadFromCbor(request.getPayload(),
422                                         HashMap.class);
423
424                         Device device = ctx.channel().attr(keyDevice).get();
425
426                         if (device == null) {
427                             device = new CoapDevice(ctx);
428                             ctx.channel().attr(keyDevice).set(device);
429                         }
430
431                         if (authPayload == null) {
432                             throw new BadRequestException("payload is empty");
433                         }
434
435                         ((CoapDevice) device).updateDevice(
436                                 (String) authPayload.get(Constants.DEVICE_ID),
437                                 (String) authPayload.get(Constants.USER_ID),
438                                 (String) authPayload
439                                         .get(Constants.ACCESS_TOKEN));
440
441                         break;
442
443                     case OICConstants.KEEP_ALIVE_FULL_URI:
444                         // TODO: Pass ping request to upper layer
445                         break;
446
447                     default:
448                         throw new UnAuthorizedException(
449                                 "authentication required first");
450                 }
451
452                 ctx.fireChannelRead(msg);
453
454             } catch (Throwable t) {
455                 ResponseStatus responseStatus = t instanceof ServerException
456                         ? ((ServerException) t).getErrorResponse()
457                         : ResponseStatus.UNAUTHORIZED;
458                 ctx.writeAndFlush(MessageBuilder
459                         .createResponse((CoapRequest) msg, responseStatus));
460                 Log.f(ctx.channel(), t);
461             }
462         }
463     }
464
465     @Sharable
466     static class HttpAuthHandler extends ChannelDuplexHandler {
467         @Override
468         public void channelActive(ChannelHandlerContext ctx) throws Exception {
469             // After current channel authenticated, raise to upper layer
470         }
471     }
472
473     @Sharable
474     class CoapSignalingHandler extends ChannelInboundHandlerAdapter {
475
476         @Override
477         public void channelInactive(ChannelHandlerContext ctx)
478                 throws Exception {
479             // delete csm information from the map
480             mCsmMap.remove(ctx);
481             ctx.fireChannelInactive();
482         }
483
484         @Override
485         public void channelRead(ChannelHandlerContext ctx, Object msg) {
486             try {
487                 if (msg instanceof CoapSignaling) {
488                     if (mCsmMap.get(ctx) == null) {
489                         // In the server, the CSM message is sent to the device
490                         // once
491                         CoapSignaling inicialCsm = (CoapSignaling) MessageBuilder
492                                 .createSignaling(SignalingMethod.CSM);
493                         inicialCsm.setCsmMaxMessageSize(4294967295L);
494                         ctx.writeAndFlush(inicialCsm);
495                     }
496                     CoapSignaling signaling = (CoapSignaling) msg;
497                     switch (signaling.getSignalingMethod()) {
498                         case CSM:
499                             // get existing CSM from the map
500                             CoapSignaling existingCsm = mCsmMap.get(ctx);
501                             if (existingCsm == null) {
502                                 existingCsm = signaling;
503                             } else {
504                                 // replace and cumulate CSM options
505                                 existingCsm.setCsmBlockWiseTransfer(
506                                         signaling.getCsmBlockWiseTransfer());
507                                 existingCsm.setCsmMaxMessageSize(
508                                         signaling.getCsmMaxMessageSize());
509                                 existingCsm.setCsmServerName(
510                                         signaling.getCsmServerName());
511                             }
512                             mCsmMap.put(ctx, existingCsm);
513                             break;
514                         case PING:
515                             // TODO process PING signaling option
516                             break;
517                         case PONG:
518                             // TODO process PONG signaling option
519                             break;
520                         case RELEASE:
521                         case ABORT:
522                             mCsmMap.remove(ctx);
523                             ctx.close();
524                             break;
525                         default:
526                             throw new BadOptionException(
527                                     "unsupported CoAP Signaling option");
528                     }
529
530                     ctx.fireChannelRead(msg);
531                 } else {
532                     ctx.fireChannelRead(msg);
533                     // TODO annotated codes must be removed to follow
534                     // the CSM specification of draft-ietf-core-coap-tcp-tls-05
535
536                     // if (mCsmMap.get(ctx) != null) {
537                     // ctx.fireChannelRead(msg);
538                     // } else {
539                     // // send ABORT signaling and close the connection
540                     // ctx.writeAndFlush(MessageBuilder.createSignaling(
541                     // SignalingMethod.ABORT,
542                     // new String(
543                     // "Capability and Settings message (CSM) is not received
544                     // yet")
545                     // .getBytes()));
546                     // ctx.close();
547                     // }
548                 }
549             } catch (Throwable t) {
550                 ResponseStatus responseStatus = t instanceof ServerException
551                         ? ((ServerException) t).getErrorResponse()
552                         : ResponseStatus.BAD_OPTION;
553                 if (msg instanceof CoapRequest) {
554                     ctx.writeAndFlush(MessageBuilder
555                             .createResponse((CoapRequest) msg, responseStatus));
556                 } else if (msg instanceof CoapSignaling) {
557                     ctx.writeAndFlush(MessageBuilder.createSignalingResponse(
558                             (CoapSignaling) msg, responseStatus));
559                 }
560                 Log.f(ctx.channel(), t);
561             }
562         }
563
564     }
565
566     @Override
567     public void addServer(Server server) {
568         if (server instanceof CoapServer) {
569             server.addHandler(new CoapSignalingHandler());
570             server.addHandler(new CoapAuthHandler());
571         }
572
573         if (server instanceof WebSocketServer) {
574             server.addHandler(new CoapAuthHandler());
575         }
576
577         if (server instanceof HttpServer) {
578             server.addHandler(new HttpAuthHandler());
579         }
580
581         super.addServer(server);
582     }
583
584     public CoapDevicePool getDevicePool() {
585         return mDevicePool;
586     }
587 }