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