statistical defects fixed
[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_SESSION_FULL_URI:
345                         HashMap<String, Object> payloadData = mCbor
346                                 .parsePayloadFromCbor(response.getPayload(),
347                                         HashMap.class);
348
349                         if (response.getStatus() != ResponseStatus.CHANGED) {
350                             throw new UnAuthorizedException();
351                         }
352
353                         if (payloadData == null) {
354                             throw new BadRequestException("payload is empty");
355                         }
356                         int remainTime = (int) payloadData
357                                 .get(Constants.EXPIRES_IN);
358
359                         Device device = ctx.channel().attr(keyDevice).get();
360                         ((CoapDevice) device).setExpiredPolicy(remainTime);
361
362                         // Remove current auth handler and replace to
363                         // LifeCycleHandle
364                         ctx.channel().pipeline().replace(this,
365                                 "LifeCycleHandler", mLifeCycleHandler);
366
367                         // Raise event that we have Authenticated device
368                         ctx.fireChannelActive();
369
370                         break;
371                 }
372
373                 ctx.writeAndFlush(msg);
374
375             } catch (Throwable t) {
376                 Log.f(ctx.channel(), t);
377                 ctx.writeAndFlush(msg);
378                 ctx.close();
379             }
380         }
381
382         @Override
383         public void channelRead(ChannelHandlerContext ctx, Object msg) {
384             try {
385                 if (!(msg instanceof CoapRequest)) {
386                     throw new BadRequestException(
387                             "this msg type is not CoapRequest");
388                 }
389
390                 // And check first response is VALID then add or cut
391                 CoapRequest request = (CoapRequest) msg;
392
393                 String urlPath = request.getUriPath();
394
395                 if (urlPath == null) {
396                     throw new InternalServerErrorException(
397                             "request uriPath is null");
398                 }
399
400                 switch (urlPath) {
401                     // Check whether request is about account
402                     case OICConstants.ACCOUNT_FULL_URI:
403                     case OICConstants.ACCOUNT_TOKENREFRESH_FULL_URI:
404
405                         if (ctx.channel().attr(keyDevice).get() == null) {
406                             // Create device first and pass to upperlayer
407                             Device device = new CoapDevice(ctx);
408                             ctx.channel().attr(keyDevice).set(device);
409                         }
410
411                         break;
412
413                     case OICConstants.ACCOUNT_SESSION_FULL_URI:
414
415                         HashMap<String, Object> authPayload = mCbor
416                                 .parsePayloadFromCbor(request.getPayload(),
417                                         HashMap.class);
418
419                         Device device = ctx.channel().attr(keyDevice).get();
420
421                         if (device == null) {
422                             device = new CoapDevice(ctx);
423                             ctx.channel().attr(keyDevice).set(device);
424                         }
425
426                         if (authPayload == null) {
427                             throw new BadRequestException("payload is empty");
428                         }
429
430                         ((CoapDevice) device).updateDevice(
431                                 (String) authPayload.get(Constants.DEVICE_ID),
432                                 (String) authPayload.get(Constants.USER_ID),
433                                 (String) authPayload
434                                         .get(Constants.ACCESS_TOKEN));
435
436                         break;
437
438                     case OICConstants.KEEP_ALIVE_FULL_URI:
439                         // TODO: Pass ping request to upper layer
440                         break;
441
442                     default:
443                         throw new UnAuthorizedException(
444                                 "authentication required first");
445                 }
446
447                 ctx.fireChannelRead(msg);
448
449             } catch (Throwable t) {
450                 ResponseStatus responseStatus = t instanceof ServerException
451                         ? ((ServerException) t).getErrorResponse()
452                         : ResponseStatus.UNAUTHORIZED;
453                 ctx.writeAndFlush(MessageBuilder
454                         .createResponse((CoapRequest) msg, responseStatus));
455                 Log.f(ctx.channel(), t);
456             }
457         }
458     }
459
460     @Sharable
461     static class HttpAuthHandler extends ChannelDuplexHandler {
462         @Override
463         public void channelActive(ChannelHandlerContext ctx) throws Exception {
464             // After current channel authenticated, raise to upper layer
465         }
466     }
467
468     @Sharable
469     class CoapSignalingHandler extends ChannelInboundHandlerAdapter {
470
471         @Override
472         public void channelInactive(ChannelHandlerContext ctx)
473                 throws Exception {
474             // delete csm information from the map
475             mCsmMap.remove(ctx);
476             ctx.fireChannelInactive();
477         }
478
479         @Override
480         public void channelRead(ChannelHandlerContext ctx, Object msg) {
481             try {
482                 if (msg instanceof CoapSignaling) {
483                     if (mCsmMap.get(ctx) == null) {
484                         // In the server, the CSM message is sent to the device
485                         // once
486                         CoapSignaling inicialCsm = (CoapSignaling) MessageBuilder
487                                 .createSignaling(SignalingMethod.CSM);
488                         inicialCsm.setCsmMaxMessageSize(4294967295L);
489                         ctx.writeAndFlush(inicialCsm);
490                     }
491                     CoapSignaling signaling = (CoapSignaling) msg;
492                     switch (signaling.getSignalingMethod()) {
493                         case CSM:
494                             // get existing CSM from the map
495                             CoapSignaling existingCsm = mCsmMap.get(ctx);
496                             if (existingCsm == null) {
497                                 existingCsm = signaling;
498                             } else {
499                                 // replace and cumulate CSM options
500                                 existingCsm.setCsmBlockWiseTransfer(
501                                         signaling.getCsmBlockWiseTransfer());
502                                 existingCsm.setCsmMaxMessageSize(
503                                         signaling.getCsmMaxMessageSize());
504                                 existingCsm.setCsmServerName(
505                                         signaling.getCsmServerName());
506                             }
507                             mCsmMap.put(ctx, existingCsm);
508                             break;
509                         case PING:
510                             // TODO process PING signaling option
511                             break;
512                         case PONG:
513                             // TODO process PONG signaling option
514                             break;
515                         case RELEASE:
516                         case ABORT:
517                             mCsmMap.remove(ctx);
518                             ctx.close();
519                             break;
520                         default:
521                             throw new BadOptionException(
522                                     "unsupported CoAP Signaling option");
523                     }
524
525                     ctx.fireChannelRead(msg);
526                 } else {
527                     ctx.fireChannelRead(msg);
528                     // TODO annotated codes must be removed to follow
529                     // the CSM specification of draft-ietf-core-coap-tcp-tls-05
530
531                     // if (mCsmMap.get(ctx) != null) {
532                     // ctx.fireChannelRead(msg);
533                     // } else {
534                     // // send ABORT signaling and close the connection
535                     // ctx.writeAndFlush(MessageBuilder.createSignaling(
536                     // SignalingMethod.ABORT,
537                     // new String(
538                     // "Capability and Settings message (CSM) is not received
539                     // yet")
540                     // .getBytes()));
541                     // ctx.close();
542                     // }
543                 }
544             } catch (Throwable t) {
545                 ResponseStatus responseStatus = t instanceof ServerException
546                         ? ((ServerException) t).getErrorResponse()
547                         : ResponseStatus.BAD_OPTION;
548                 if (msg instanceof CoapRequest) {
549                     ctx.writeAndFlush(MessageBuilder
550                             .createResponse((CoapRequest) msg, responseStatus));
551                 } else if (msg instanceof CoapSignaling) {
552                     ctx.writeAndFlush(MessageBuilder.createSignalingResponse(
553                             (CoapSignaling) msg, responseStatus));
554                 }
555                 Log.f(ctx.channel(), t);
556             }
557         }
558
559     }
560
561     @Override
562     public void addServer(Server server) {
563         if (server instanceof CoapServer) {
564             server.addHandler(new CoapSignalingHandler());
565             server.addHandler(new CoapAuthHandler());
566         }
567
568         if (server instanceof WebSocketServer) {
569             server.addHandler(new CoapAuthHandler());
570         }
571
572         if (server instanceof HttpServer) {
573             server.addHandler(new HttpAuthHandler());
574         }
575
576         super.addServer(server);
577     }
578
579     public CoapDevicePool getDevicePool() {
580         return mDevicePool;
581     }
582 }