Revert cloud sign-up response behavior
[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     /**
74      *
75      * This class provides a set of APIs to manage device pool.
76      *
77      */
78     public static class CoapDevicePool {
79         HashMap<String, Device> mMapDevice = new HashMap<>();
80
81         /**
82          * API for adding device information into pool.
83          * 
84          * @param device
85          *            device to be added
86          */
87         public void addDevice(Device device) {
88             String deviceId = ((CoapDevice) device).getDeviceId();
89             synchronized (mMapDevice) {
90                 mMapDevice.put(deviceId, device);
91             }
92         }
93
94         /**
95          * API for removing device information into pool.
96          * 
97          * @param device
98          *            device to be removed
99          */
100         public void removeDevice(Device device) throws ClientException {
101             String deviceId = ((CoapDevice) device).getDeviceId();
102             synchronized (mMapDevice) {
103                 if (mMapDevice.get(deviceId) == device) {
104                     mMapDevice.remove(deviceId);
105                 }
106             }
107             removeObserveDevice(device);
108         }
109
110         private void removeObserveDevice(Device device) throws ClientException {
111             synchronized (mMapDevice) {
112                 Iterator<String> iterator = mMapDevice.keySet().iterator();
113                 while (iterator.hasNext()) {
114                     String deviceId = iterator.next();
115                     CoapDevice getDevice = (CoapDevice) queryDevice(deviceId);
116                     getDevice.removeObserveChannel(
117                             ((CoapDevice) device).getRequestChannel());
118                 }
119             }
120         }
121
122         /**
123          * API for getting device information.
124          * 
125          * @param deviceId
126          *            device id to get device
127          */
128         public Device queryDevice(String deviceId) {
129             Device device = null;
130             synchronized (mMapDevice) {
131                 device = mMapDevice.get(deviceId);
132             }
133             return device;
134         }
135     }
136
137     CoapDevicePool mDevicePool = new CoapDevicePool();
138
139     /**
140      *
141      * This class provides a set of APIs to manage life cycle of coap message.
142      *
143      */
144     @Sharable
145     class CoapLifecycleHandler extends ChannelDuplexHandler {
146         @Override
147         public void channelRead(ChannelHandlerContext ctx, Object msg) {
148
149             if (msg instanceof CoapRequest) {
150                 try {
151                     CoapDevice coapDevice = (CoapDevice) ctx.channel()
152                             .attr(keyDevice).get();
153
154                     if (coapDevice.isExpiredTime()) {
155                         throw new UnAuthorizedException("token is expired");
156                     }
157
158                     CoapRequest coapRequest = (CoapRequest) msg;
159                     IRequestChannel targetChannel = null;
160                     String urlPath = coapRequest.getUriPath();
161
162                     if (urlPath == null) {
163                         throw new InternalServerErrorException(
164                                 "request uriPath is null");
165                     }
166
167                     if (urlPath.contains(Constants.ROUTE_FULL_URI)) {
168
169                         int RouteResourcePathSize = Constants.ROUTE_FULL_URI
170                                 .split("/").length;
171                         List<String> uriPath = coapRequest.getUriPathSegments();
172                         if (uriPath != null && !uriPath.isEmpty()) {
173                             CoapDevice targetDevice = (CoapDevice) mDevicePool
174                                     .queryDevice(uriPath
175                                             .get(RouteResourcePathSize - 1));
176                             targetChannel = targetDevice.getRequestChannel();
177                         }
178
179                         switch (coapRequest.getObserve()) {
180                             case SUBSCRIBE:
181                                 coapDevice.addObserveRequest(
182                                         Bytes.bytesToLong(
183                                                 coapRequest.getToken()),
184                                         coapRequest);
185                                 coapDevice.addObserveChannel(targetChannel);
186                                 break;
187                             case UNSUBSCRIBE:
188                                 coapDevice.removeObserveChannel(targetChannel);
189                                 coapDevice.removeObserveRequest(Bytes
190                                         .bytesToLong(coapRequest.getToken()));
191                                 break;
192                             default:
193                                 break;
194                         }
195                     }
196                 } catch (Throwable t) {
197                     Log.f(ctx.channel(), t);
198                     ResponseStatus responseStatus = t instanceof ServerException
199                             ? ((ServerException) t).getErrorResponse()
200                             : ResponseStatus.INTERNAL_SERVER_ERROR;
201                     ctx.writeAndFlush(MessageBuilder
202                             .createResponse((CoapRequest) msg, responseStatus));
203                     ctx.close();
204                 }
205             }
206             ctx.fireChannelRead(msg);
207         }
208
209         @Override
210         public void write(ChannelHandlerContext ctx, Object msg,
211                 ChannelPromise promise) throws Exception {
212
213             boolean bCloseConnection = false;
214
215             if (msg instanceof CoapResponse) {
216                 // This is CoapResponse
217                 // Once the response is valid, add this to deviceList
218                 CoapResponse response = (CoapResponse) msg;
219
220                 String urlPath = response.getUriPath();
221
222                 if (urlPath == null) {
223                     throw new InternalServerErrorException(
224                             "request uriPath is null");
225                 }
226
227                 switch (urlPath) {
228                     case OICConstants.ACCOUNT_SESSION_FULL_URI:
229                         if (response.getStatus() != ResponseStatus.CHANGED) {
230                             bCloseConnection = true;
231                         }
232                         break;
233                     case OICConstants.ACCOUNT_FULL_URI:
234                         if (response.getStatus() == ResponseStatus.DELETED) {
235                             bCloseConnection = true;
236                         }
237                         break;
238                 }
239             }
240
241             ctx.writeAndFlush(msg);
242
243             if (bCloseConnection == true) {
244                 ctx.close();
245             }
246         }
247
248         @Override
249         public void channelActive(ChannelHandlerContext ctx) {
250             Device device = ctx.channel().attr(keyDevice).get();
251             // Authenticated device connected
252
253             sendDevicePresence(device.getDeviceId(), "on");
254             mDevicePool.addDevice(device);
255
256             device.onConnected();
257         }
258
259         @Override
260         public void channelInactive(ChannelHandlerContext ctx)
261                 throws ClientException {
262             Device device = ctx.channel().attr(keyDevice).get();
263
264             // Some cases, this event occurs after new device connected using
265             // same di.
266             // So compare actual value, and remove if same.
267             if (device != null) {
268                 sendDevicePresence(device.getDeviceId(), "off");
269
270                 device.onDisconnected();
271
272                 mDevicePool.removeDevice(device);
273                 ctx.channel().attr(keyDevice).remove();
274
275             }
276         }
277
278         /**
279          * API for sending state to resource directory
280          * 
281          * @param deviceId
282          *            device id to be sent to resource directory
283          * @param state
284          *            device state to be sent to resource directory
285          */
286         public void sendDevicePresence(String deviceId, String state) {
287
288             Cbor<HashMap<String, Object>> cbor = new Cbor<>();
289             HashMap<String, Object> payload = new HashMap<String, Object>();
290             payload.put(Constants.REQ_DEVICE_ID, deviceId);
291             payload.put(Constants.PRESENCE_STATE, state);
292             StringBuffer uriPath = new StringBuffer();
293             uriPath.append("/" + Constants.PREFIX_OIC);
294             uriPath.append("/" + Constants.DEVICE_PRESENCE_URI);
295             ConnectorPool.getConnection("rd")
296                     .sendRequest(MessageBuilder.createRequest(
297                             RequestMethod.POST, uriPath.toString(), null,
298                             ContentFormat.APPLICATION_CBOR,
299                             cbor.encodingPayloadToCbor(payload)), null);
300         }
301     }
302
303     CoapLifecycleHandler mLifeCycleHandler = new CoapLifecycleHandler();
304
305     @Sharable
306     class CoapAuthHandler extends ChannelDuplexHandler {
307
308         @Override
309         public void channelActive(ChannelHandlerContext ctx) {
310             // Actual channel active should decided after authentication.
311         }
312
313         @Override
314         public void write(ChannelHandlerContext ctx, Object msg,
315                 ChannelPromise promise) {
316             try {
317
318                 if (!(msg instanceof CoapResponse)) {
319                     // throw new BadRequestException(
320                     // "this msg type is not CoapResponse");
321
322                     // TODO check websocket handshake response
323                     ctx.writeAndFlush(msg);
324                     return;
325                 }
326                 // This is CoapResponse
327                 // Once the response is valid, add this to deviceList
328
329                 CoapResponse response = (CoapResponse) msg;
330
331                 String urlPath = response.getUriPath();
332
333                 if (urlPath == null) {
334                     throw new InternalServerErrorException(
335                             "request uriPath is null");
336                 }
337
338                 switch (urlPath) {
339                     /*
340                      * case OICConstants.ACCOUNT_FULL_URI:
341                      * ctx.writeAndFlush(msg); ctx.close(); return;
342                      */
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 }