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