Repository: eclipse/leshan
Pull request #669 (2019-03-22 14:55:19)

Add a way to custom BootstrapHandler behavior at server side
This PR allows to customize bootstrap server behavior like this:

```java
builder.setBootstrapHandlerFactory(new BootstrapHandlerFactory() {
    
    @Override
    public BootstrapHandler create(BootstrapStore store, LwM2mBootstrapRequestSender sender,
            BootstrapSessionManager sessionManager) {
        return new MyBootstrapHandler(store, sender, sessionManager);
    }
})
```


Patch:
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanBootstrapServerBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanBootstrapServerBuilder.java
index e170b7a0..145d6af6 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanBootstrapServerBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanBootstrapServerBuilder.java
@@ -35,8 +35,12 @@ import org.eclipse.leshan.core.californium.EndpointFactory;
 import org.eclipse.leshan.core.californium.Lwm2mEndpointContextMatcher;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectLoader;
+import org.eclipse.leshan.server.bootstrap.BootstrapHandler;
+import org.eclipse.leshan.server.bootstrap.BootstrapHandlerFactory;
 import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager;
 import org.eclipse.leshan.server.bootstrap.BootstrapStore;
+import org.eclipse.leshan.server.bootstrap.DefaultBootstrapHandler;
+import org.eclipse.leshan.server.bootstrap.LwM2mBootstrapRequestSender;
 import org.eclipse.leshan.server.bootstrap.LwM2mBootstrapServer;
 import org.eclipse.leshan.server.californium.impl.LeshanBootstrapServer;
 import org.eclipse.leshan.server.californium.impl.LwM2mBootstrapPskStore;
@@ -59,6 +63,8 @@ public class LeshanBootstrapServerBuilder {
     private BootstrapStore configStore;
     private BootstrapSecurityStore securityStore;
     private BootstrapSessionManager sessionManager;
+    private BootstrapHandlerFactory bootstrapHandlerFactory;
+
     private LwM2mModel model;
     private NetworkConfig coapConfig;
     private Builder dtlsConfigBuilder;
@@ -192,6 +198,11 @@ public class LeshanBootstrapServerBuilder {
         return this;
     }
 
+    public LeshanBootstrapServerBuilder setBootstrapHandlerFactory(BootstrapHandlerFactory bootstrapHandlerFactory) {
+        this.bootstrapHandlerFactory = bootstrapHandlerFactory;
+        return this;
+    }
+
     public LeshanBootstrapServerBuilder setModel(LwM2mModel model) {
         this.model = model;
         return this;
@@ -254,6 +265,14 @@ public class LeshanBootstrapServerBuilder {
 
         if (sessionManager == null)
             sessionManager = new DefaultBootstrapSessionManager(securityStore);
+        if (bootstrapHandlerFactory == null)
+            bootstrapHandlerFactory = new BootstrapHandlerFactory() {
+                @Override
+                public BootstrapHandler create(BootstrapStore store, LwM2mBootstrapRequestSender sender,
+                        BootstrapSessionManager sessionManager) {
+                    return new DefaultBootstrapHandler(store, sender, sessionManager);
+                }
+            };
         if (model == null)
             model = new LwM2mModel(ObjectLoader.loadDefault());
         if (coapConfig == null) {
@@ -395,6 +414,6 @@ public class LeshanBootstrapServerBuilder {
         }
 
         return new LeshanBootstrapServer(unsecuredEndpoint, securedEndpoint, configStore, securityStore, sessionManager,
-                model, coapConfig);
+                bootstrapHandlerFactory, model, coapConfig);
     }
 }
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanBootstrapServer.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanBootstrapServer.java
index 95733c8d..52e1e00e 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanBootstrapServer.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanBootstrapServer.java
@@ -21,7 +21,7 @@ import org.eclipse.californium.core.CoapServer;
 import org.eclipse.californium.core.network.CoapEndpoint;
 import org.eclipse.californium.core.network.config.NetworkConfig;
 import org.eclipse.leshan.core.model.LwM2mModel;
-import org.eclipse.leshan.server.bootstrap.BootstrapHandler;
+import org.eclipse.leshan.server.bootstrap.BootstrapHandlerFactory;
 import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager;
 import org.eclipse.leshan.server.bootstrap.BootstrapStore;
 import org.eclipse.leshan.server.bootstrap.LwM2mBootstrapRequestSender;
@@ -46,11 +46,12 @@ public class LeshanBootstrapServer implements LwM2mBootstrapServer {
     private final BootstrapSecurityStore bsSecurityStore;
 
     public LeshanBootstrapServer(CoapEndpoint unsecuredEndpoint, CoapEndpoint securedEndpoint, BootstrapStore bsStore,
-            BootstrapSecurityStore bsSecurityStore, BootstrapSessionManager bsSessionManager, LwM2mModel model,
-            NetworkConfig coapConfig) {
+            BootstrapSecurityStore bsSecurityStore, BootstrapSessionManager bsSessionManager,
+            BootstrapHandlerFactory bsHandlerFactory, LwM2mModel model, NetworkConfig coapConfig) {
 
         Validate.notNull(bsStore, "bootstrap store must not be null");
         Validate.notNull(bsSessionManager, "session manager must not be null");
+        Validate.notNull(bsHandlerFactory, "BootstrapHandler factory must not be null");
         Validate.notNull(model, "model must not be null");
         Validate.notNull(coapConfig, "coapConfig must not be null");
 
@@ -73,7 +74,7 @@ public class LeshanBootstrapServer implements LwM2mBootstrapServer {
                 unsecuredEndpoint, model);
 
         BootstrapResource bsResource = new BootstrapResource(
-                new BootstrapHandler(bsStore, requestSender, bsSessionManager));
+                bsHandlerFactory.create(bsStore, requestSender, bsSessionManager));
         coapServer.add(bsResource);
     }
 
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandler.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandler.java
index 4076a4f0..2c7d065e 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandler.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandler.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2016 Sierra Wireless and others.
+ * Copyright (c) 2019 Sierra Wireless and others.
  * 
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
@@ -15,306 +15,11 @@
  *******************************************************************************/
 package org.eclipse.leshan.server.bootstrap;
 
-import static org.eclipse.leshan.server.bootstrap.BootstrapFailureCause.*;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-import org.eclipse.leshan.core.node.LwM2mMultipleResource;
-import org.eclipse.leshan.core.node.LwM2mNode;
-import org.eclipse.leshan.core.node.LwM2mObjectInstance;
-import org.eclipse.leshan.core.node.LwM2mPath;
-import org.eclipse.leshan.core.node.LwM2mResource;
-import org.eclipse.leshan.core.node.LwM2mSingleResource;
-import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
-import org.eclipse.leshan.core.request.BootstrapFinishRequest;
 import org.eclipse.leshan.core.request.BootstrapRequest;
-import org.eclipse.leshan.core.request.BootstrapWriteRequest;
-import org.eclipse.leshan.core.request.DownlinkRequest;
 import org.eclipse.leshan.core.request.Identity;
-import org.eclipse.leshan.core.response.BootstrapDeleteResponse;
-import org.eclipse.leshan.core.response.BootstrapFinishResponse;
 import org.eclipse.leshan.core.response.BootstrapResponse;
-import org.eclipse.leshan.core.response.BootstrapWriteResponse;
-import org.eclipse.leshan.core.response.ErrorCallback;
-import org.eclipse.leshan.core.response.LwM2mResponse;
-import org.eclipse.leshan.core.response.ResponseCallback;
-import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ACLConfig;
-import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerConfig;
-import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerSecurity;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Handle the bootstrap logic at Server side. Check if the client is allowed to bootstrap, with the wanted security
- * scheme. Then send delete and write request to bootstrap the client, then close the bootstrap session by sending a
- * bootstrap finished request.
- */
-public class BootstrapHandler {
-
-    private static final Logger LOG = LoggerFactory.getLogger(BootstrapHandler.class);
-
-    // We choose a default timeout a bit higher to the MAX_TRANSMIT_WAIT(62-93s) which is the time from starting to
-    // send a Confirmable message to the time when an acknowledgement is no longer expected.
-    private static final long DEFAULT_TIMEOUT = 2 * 60 * 1000l; // 2min in ms
-
-    private final Executor e;
-
-    private final BootstrapStore store;
-    private final LwM2mBootstrapRequestSender sender;
-    private final BootstrapSessionManager sessionManager;
-
-    public BootstrapHandler(BootstrapStore store, LwM2mBootstrapRequestSender sender,
-            BootstrapSessionManager sessionManager) {
-        this.store = store;
-        this.sender = sender;
-        this.sessionManager = sessionManager;
-        this.e = Executors.newFixedThreadPool(5);
-    }
-
-    protected BootstrapHandler(BootstrapStore store, LwM2mBootstrapRequestSender sender,
-            BootstrapSessionManager sessionManager, Executor executor) {
-        this.store = store;
-        this.sender = sender;
-        this.sessionManager = sessionManager;
-        this.e = executor;
-    }
-
-    public BootstrapResponse bootstrap(Identity sender, BootstrapRequest request) {
-        String endpoint = request.getEndpointName();
-
-        // Start session, checking the BS credentials
-        final BootstrapSession session = this.sessionManager.begin(endpoint, sender);
-
-        if (!session.isAuthorized()) {
-            this.sessionManager.failed(session, UNAUTHORIZED, null);
-            return BootstrapResponse.badRequest("Unauthorized");
-        }
-
-        // Get the desired bootstrap config for the endpoint
-        final BootstrapConfig cfg = store.getBootstrap(endpoint, sender);
-        if (cfg == null) {
-            LOG.debug("No bootstrap config for {}", endpoint);
-            this.sessionManager.failed(session, NO_BOOTSTRAP_CONFIG, null);
-            return BootstrapResponse.badRequest("no bootstrap config");
-        }
-
-        // Start the bootstrap session
-        e.execute(new Runnable() {
-            @Override
-            public void run() {
-                sendDelete(session, cfg);
-            }
-        });
-
-        return BootstrapResponse.success();
-    }
-
-    private void sendDelete(final BootstrapSession session, final BootstrapConfig cfg) {
-
-        final BootstrapDeleteRequest deleteRequest = new BootstrapDeleteRequest();
-        send(session, deleteRequest, new ResponseCallback<BootstrapDeleteResponse>() {
-            @Override
-            public void onResponse(BootstrapDeleteResponse response) {
-                LOG.trace("Bootstrap delete {} return code {}", session.getEndpoint(), response.getCode());
-                List<Integer> toSend = new ArrayList<>(cfg.security.keySet());
-                sendBootstrap(session, cfg, toSend);
-            }
-        }, new ErrorCallback() {
-            @Override
-            public void onError(Exception e) {
-                LOG.debug(String.format("Error during bootstrap delete '/' on %s", session.getEndpoint()), e);
-                sessionManager.failed(session, DELETE_FAILED, deleteRequest);
-            }
-        });
-    }
-
-    private void sendBootstrap(final BootstrapSession session, final BootstrapConfig cfg, final List<Integer> toSend) {
-        if (!toSend.isEmpty()) {
-            // 1st encode them into a juicy TLV binary
-            Integer key = toSend.remove(0);
-            ServerSecurity securityConfig = cfg.security.get(key);
-
-            // extract write request parameters
-            LwM2mPath path = new LwM2mPath(0, key);
-            final LwM2mNode securityInstance = convertToSecurityInstance(key, securityConfig);
-
-            final BootstrapWriteRequest writeBootstrapRequest = new BootstrapWriteRequest(path, securityInstance,
-                    session.getContentFormat());
-            send(session, writeBootstrapRequest, new ResponseCallback<BootstrapWriteResponse>() {
-                @Override
-                public void onResponse(BootstrapWriteResponse response) {
-                    LOG.trace("Bootstrap write {} return code {}", session.getEndpoint(), response.getCode());
-                    // recursive call until toSend is empty
-                    sendBootstrap(session, cfg, toSend);
-                }
-            }, new ErrorCallback() {
-                @Override
-                public void onError(Exception e) {
-                    LOG.debug(String.format("Error during bootstrap write of security instance %s on %s",
-                            securityInstance, session.getEndpoint()), e);
-                    sessionManager.failed(session, WRITE_SECURITY_FAILED, writeBootstrapRequest);
-                }
-            });
-        } else {
-            // we are done, send the servers
-            List<Integer> serversToSend = new ArrayList<>(cfg.servers.keySet());
-            sendServers(session, cfg, serversToSend);
-        }
-    }
-
-    private void sendServers(final BootstrapSession session, final BootstrapConfig cfg, final List<Integer> toSend) {
-        if (!toSend.isEmpty()) {
-            // get next config
-            Integer key = toSend.remove(0);
-            ServerConfig serverConfig = cfg.servers.get(key);
-
-            // extract write request parameters
-            LwM2mPath path = new LwM2mPath(1, key);
-            final LwM2mNode serverInstance = convertToServerInstance(key, serverConfig);
-
-            final BootstrapWriteRequest writeServerRequest = new BootstrapWriteRequest(path, serverInstance,
-                    session.getContentFormat());
-            send(session, writeServerRequest, new ResponseCallback<BootstrapWriteResponse>() {
-                @Override
-                public void onResponse(BootstrapWriteResponse response) {
-                    LOG.trace("Bootstrap write {} return code {}", session.getEndpoint(), response.getCode());
-                    // recursive call until toSend is empty
-                    sendServers(session, cfg, toSend);
-                }
-            }, new ErrorCallback() {
-                @Override
-                public void onError(Exception e) {
-                    LOG.warn(String.format("Error during bootstrap write of server instance %s on %s", serverInstance,
-                            session.getEndpoint()), e);
-                    sessionManager.failed(session, WRITE_SERVER_FAILED, writeServerRequest);
-                }
-            });
-        } else {
-            // we are done, send the ACL
-            List<Integer> aclsToSend = new ArrayList<>(cfg.acls.keySet());
-            sendAcls(session, cfg, aclsToSend);
-        }
-    }
-
-    private void sendAcls(final BootstrapSession session, final BootstrapConfig cfg, final List<Integer> toSend) {
-        if (!toSend.isEmpty()) {
-            // get next config
-            Integer key = toSend.remove(0);
-            ACLConfig aclConfig = cfg.acls.get(key);
-
-            // extract write request parameters
-            LwM2mPath path = new LwM2mPath(2, key);
-            final LwM2mNode aclInstance = convertToAclInstance(key, aclConfig);
-
-            final BootstrapWriteRequest writeACLRequest = new BootstrapWriteRequest(path, aclInstance,
-                    session.getContentFormat());
-            send(session, writeACLRequest, new ResponseCallback<BootstrapWriteResponse>() {
-                @Override
-                public void onResponse(BootstrapWriteResponse response) {
-                    LOG.trace("Bootstrap write {} return code {}", session.getEndpoint(), response.getCode());
-                    // recursive call until toSend is empty
-                    sendAcls(session, cfg, toSend);
-                }
-            }, new ErrorCallback() {
-                @Override
-                public void onError(Exception e) {
-                    LOG.warn(String.format("Error during bootstrap write of acl instance %s on %s", aclInstance,
-                            session.getEndpoint()), e);
-                    sessionManager.failed(session, WRITE_ACL_FAILED, writeACLRequest);
-                }
-            });
-        } else {
-            final BootstrapFinishRequest finishBootstrapRequest = new BootstrapFinishRequest();
-            send(session, finishBootstrapRequest, new ResponseCallback<BootstrapFinishResponse>() {
-                @Override
-                public void onResponse(BootstrapFinishResponse response) {
-                    LOG.trace("Bootstrap Finished {} return code {}", session.getEndpoint(), response.getCode());
-                    if (response.isSuccess()) {
-                        sessionManager.end(session);
-                    } else {
-                        sessionManager.failed(session, FINISHED_WITH_ERROR, finishBootstrapRequest);
-                    }
-                }
-            }, new ErrorCallback() {
-                @Override
-                public void onError(Exception e) {
-                    LOG.debug(String.format("Error during bootstrap finished on %s", session.getEndpoint()), e);
-                    sessionManager.failed(session, SEND_FINISH_FAILED, finishBootstrapRequest);
-                }
-            });
-        }
-    }
-
-    private <T extends LwM2mResponse> void send(BootstrapSession session, DownlinkRequest<T> request,
-            ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
-        sender.send(session.getEndpoint(), session.getIdentity(), request, DEFAULT_TIMEOUT, responseCallback,
-                errorCallback);
-    }
-
-    private LwM2mObjectInstance convertToSecurityInstance(int instanceId, ServerSecurity securityConfig) {
-        Collection<LwM2mResource> resources = new ArrayList<>();
-
-        if (securityConfig.uri != null)
-            resources.add(LwM2mSingleResource.newStringResource(0, securityConfig.uri));
-        resources.add(LwM2mSingleResource.newBooleanResource(1, securityConfig.bootstrapServer));
-        if (securityConfig.securityMode != null)
-            resources.add(LwM2mSingleResource.newIntegerResource(2, securityConfig.securityMode.code));
-        if (securityConfig.publicKeyOrId != null)
-            resources.add(LwM2mSingleResource.newBinaryResource(3, securityConfig.publicKeyOrId));
-        if (securityConfig.serverPublicKey != null)
-            resources.add(LwM2mSingleResource.newBinaryResource(4, securityConfig.serverPublicKey));
-        if (securityConfig.secretKey != null)
-            resources.add(LwM2mSingleResource.newBinaryResource(5, securityConfig.secretKey));
-        if (securityConfig.smsSecurityMode != null)
-            resources.add(LwM2mSingleResource.newIntegerResource(6, securityConfig.smsSecurityMode.code));
-        if (securityConfig.smsBindingKeyParam != null)
-            resources.add(LwM2mSingleResource.newBinaryResource(7, securityConfig.smsBindingKeyParam));
-        if (securityConfig.smsBindingKeySecret != null)
-            resources.add(LwM2mSingleResource.newBinaryResource(8, securityConfig.smsBindingKeySecret));
-        if (securityConfig.serverSmsNumber != null)
-            resources.add(LwM2mSingleResource.newStringResource(9, securityConfig.serverSmsNumber));
-        if (securityConfig.serverId != null)
-            resources.add(LwM2mSingleResource.newIntegerResource(10, securityConfig.serverId));
-        if (securityConfig.clientOldOffTime != null)
-            resources.add(LwM2mSingleResource.newIntegerResource(11, securityConfig.clientOldOffTime));
-        if (securityConfig.bootstrapServerAccountTimeout != null)
-            resources.add(LwM2mSingleResource.newIntegerResource(12, securityConfig.bootstrapServerAccountTimeout));
-
-        return new LwM2mObjectInstance(instanceId, resources);
-    }
-
-    private LwM2mObjectInstance convertToServerInstance(int instanceId, ServerConfig serverConfig) {
-        Collection<LwM2mResource> resources = new ArrayList<>();
-
-        resources.add(LwM2mSingleResource.newIntegerResource(0, serverConfig.shortId));
-        resources.add(LwM2mSingleResource.newIntegerResource(1, serverConfig.lifetime));
-        if (serverConfig.defaultMinPeriod != null)
-            resources.add(LwM2mSingleResource.newIntegerResource(2, serverConfig.defaultMinPeriod));
-        if (serverConfig.defaultMaxPeriod != null)
-            resources.add(LwM2mSingleResource.newIntegerResource(3, serverConfig.defaultMaxPeriod));
-        if (serverConfig.disableTimeout != null)
-            resources.add(LwM2mSingleResource.newIntegerResource(5, serverConfig.disableTimeout));
-        resources.add(LwM2mSingleResource.newBooleanResource(6, serverConfig.notifIfDisabled));
-        if (serverConfig.binding != null)
-            resources.add(LwM2mSingleResource.newStringResource(7, serverConfig.binding.name()));
-
-        return new LwM2mObjectInstance(instanceId, resources);
-    }
-
-    private LwM2mObjectInstance convertToAclInstance(int instanceId, ACLConfig aclConfig) {
-        Collection<LwM2mResource> resources = new ArrayList<>();
 
-        resources.add(LwM2mSingleResource.newIntegerResource(0, aclConfig.objectId));
-        resources.add(LwM2mSingleResource.newIntegerResource(1, aclConfig.objectInstanceId));
-        if (aclConfig.acls != null)
-            resources.add(LwM2mMultipleResource.newIntegerResource(2, aclConfig.acls));
-        if (aclConfig.AccessControlOwner != null)
-            resources.add(LwM2mSingleResource.newIntegerResource(3, aclConfig.AccessControlOwner));
+public interface BootstrapHandler {
 
-        return new LwM2mObjectInstance(instanceId, resources);
-    }
+    BootstrapResponse bootstrap(Identity sender, BootstrapRequest request);
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerFactory.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerFactory.java
new file mode 100644
index 00000000..9c480dc8
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerFactory.java
@@ -0,0 +1,21 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.bootstrap;
+
+public interface BootstrapHandlerFactory {
+    BootstrapHandler create(BootstrapStore store, LwM2mBootstrapRequestSender sender,
+            BootstrapSessionManager sessionManager);
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java
new file mode 100644
index 00000000..cafdde1d
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java
@@ -0,0 +1,92 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.bootstrap;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.eclipse.leshan.core.node.LwM2mMultipleResource;
+import org.eclipse.leshan.core.node.LwM2mObjectInstance;
+import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ACLConfig;
+import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerConfig;
+import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerSecurity;
+
+public class BootstrapUtil {
+    public static LwM2mObjectInstance convertToSecurityInstance(int instanceId, ServerSecurity securityConfig) {
+        Collection<LwM2mResource> resources = new ArrayList<>();
+
+        if (securityConfig.uri != null)
+            resources.add(LwM2mSingleResource.newStringResource(0, securityConfig.uri));
+        resources.add(LwM2mSingleResource.newBooleanResource(1, securityConfig.bootstrapServer));
+        if (securityConfig.securityMode != null)
+            resources.add(LwM2mSingleResource.newIntegerResource(2, securityConfig.securityMode.code));
+        if (securityConfig.publicKeyOrId != null)
+            resources.add(LwM2mSingleResource.newBinaryResource(3, securityConfig.publicKeyOrId));
+        if (securityConfig.serverPublicKey != null)
+            resources.add(LwM2mSingleResource.newBinaryResource(4, securityConfig.serverPublicKey));
+        if (securityConfig.secretKey != null)
+            resources.add(LwM2mSingleResource.newBinaryResource(5, securityConfig.secretKey));
+        if (securityConfig.smsSecurityMode != null)
+            resources.add(LwM2mSingleResource.newIntegerResource(6, securityConfig.smsSecurityMode.code));
+        if (securityConfig.smsBindingKeyParam != null)
+            resources.add(LwM2mSingleResource.newBinaryResource(7, securityConfig.smsBindingKeyParam));
+        if (securityConfig.smsBindingKeySecret != null)
+            resources.add(LwM2mSingleResource.newBinaryResource(8, securityConfig.smsBindingKeySecret));
+        if (securityConfig.serverSmsNumber != null)
+            resources.add(LwM2mSingleResource.newStringResource(9, securityConfig.serverSmsNumber));
+        if (securityConfig.serverId != null)
+            resources.add(LwM2mSingleResource.newIntegerResource(10, securityConfig.serverId));
+        if (securityConfig.clientOldOffTime != null)
+            resources.add(LwM2mSingleResource.newIntegerResource(11, securityConfig.clientOldOffTime));
+        if (securityConfig.bootstrapServerAccountTimeout != null)
+            resources.add(LwM2mSingleResource.newIntegerResource(12, securityConfig.bootstrapServerAccountTimeout));
+
+        return new LwM2mObjectInstance(instanceId, resources);
+    }
+
+    public static LwM2mObjectInstance convertToServerInstance(int instanceId, ServerConfig serverConfig) {
+        Collection<LwM2mResource> resources = new ArrayList<>();
+
+        resources.add(LwM2mSingleResource.newIntegerResource(0, serverConfig.shortId));
+        resources.add(LwM2mSingleResource.newIntegerResource(1, serverConfig.lifetime));
+        if (serverConfig.defaultMinPeriod != null)
+            resources.add(LwM2mSingleResource.newIntegerResource(2, serverConfig.defaultMinPeriod));
+        if (serverConfig.defaultMaxPeriod != null)
+            resources.add(LwM2mSingleResource.newIntegerResource(3, serverConfig.defaultMaxPeriod));
+        if (serverConfig.disableTimeout != null)
+            resources.add(LwM2mSingleResource.newIntegerResource(5, serverConfig.disableTimeout));
+        resources.add(LwM2mSingleResource.newBooleanResource(6, serverConfig.notifIfDisabled));
+        if (serverConfig.binding != null)
+            resources.add(LwM2mSingleResource.newStringResource(7, serverConfig.binding.name()));
+
+        return new LwM2mObjectInstance(instanceId, resources);
+    }
+
+    public static LwM2mObjectInstance convertToAclInstance(int instanceId, ACLConfig aclConfig) {
+        Collection<LwM2mResource> resources = new ArrayList<>();
+
+        resources.add(LwM2mSingleResource.newIntegerResource(0, aclConfig.objectId));
+        resources.add(LwM2mSingleResource.newIntegerResource(1, aclConfig.objectInstanceId));
+        if (aclConfig.acls != null)
+            resources.add(LwM2mMultipleResource.newIntegerResource(2, aclConfig.acls));
+        if (aclConfig.AccessControlOwner != null)
+            resources.add(LwM2mSingleResource.newIntegerResource(3, aclConfig.AccessControlOwner));
+
+        return new LwM2mObjectInstance(instanceId, resources);
+    }
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java
new file mode 100644
index 00000000..09850601
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java
@@ -0,0 +1,273 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.bootstrap;
+
+import static org.eclipse.leshan.server.bootstrap.BootstrapFailureCause.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
+import org.eclipse.leshan.core.request.BootstrapFinishRequest;
+import org.eclipse.leshan.core.request.BootstrapRequest;
+import org.eclipse.leshan.core.request.BootstrapWriteRequest;
+import org.eclipse.leshan.core.request.DownlinkRequest;
+import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.core.response.BootstrapDeleteResponse;
+import org.eclipse.leshan.core.response.BootstrapFinishResponse;
+import org.eclipse.leshan.core.response.BootstrapResponse;
+import org.eclipse.leshan.core.response.BootstrapWriteResponse;
+import org.eclipse.leshan.core.response.ErrorCallback;
+import org.eclipse.leshan.core.response.LwM2mResponse;
+import org.eclipse.leshan.core.response.ResponseCallback;
+import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ACLConfig;
+import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerConfig;
+import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerSecurity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handle the bootstrap logic at Server side. Check if the client is allowed to bootstrap, with the wanted security
+ * scheme. Then send delete and write request to bootstrap the client, then close the bootstrap session by sending a
+ * bootstrap finished request.
+ */
+public class DefaultBootstrapHandler implements BootstrapHandler {
+
+    private static final Logger LOG = LoggerFactory.getLogger(DefaultBootstrapHandler.class);
+
+    // We choose a default timeout a bit higher to the MAX_TRANSMIT_WAIT(62-93s) which is the time from starting to
+    // send a Confirmable message to the time when an acknowledgement is no longer expected.
+    protected static final long DEFAULT_TIMEOUT = 2 * 60 * 1000l; // 2min in ms
+
+    protected final Executor e;
+
+    protected final BootstrapStore store;
+    protected final LwM2mBootstrapRequestSender sender;
+    protected final BootstrapSessionManager sessionManager;
+    protected final long requestTimeout;
+
+    public DefaultBootstrapHandler(BootstrapStore store, LwM2mBootstrapRequestSender sender,
+            BootstrapSessionManager sessionManager) {
+        this(store, sender, sessionManager, Executors.newFixedThreadPool(5), DEFAULT_TIMEOUT);
+    }
+
+    public DefaultBootstrapHandler(BootstrapStore store, LwM2mBootstrapRequestSender sender,
+            BootstrapSessionManager sessionManager, Executor executor) {
+        this(store, sender, sessionManager, executor, DEFAULT_TIMEOUT);
+    }
+
+    public DefaultBootstrapHandler(BootstrapStore store, LwM2mBootstrapRequestSender sender,
+            BootstrapSessionManager sessionManager, Executor executor, long requestTimeout) {
+        this.store = store;
+        this.sender = sender;
+        this.sessionManager = sessionManager;
+        this.e = executor;
+        this.requestTimeout = requestTimeout;
+    }
+
+    @Override
+    public BootstrapResponse bootstrap(Identity sender, BootstrapRequest request) {
+        String endpoint = request.getEndpointName();
+
+        // Start session, checking the BS credentials
+        final BootstrapSession session = this.sessionManager.begin(endpoint, sender);
+
+        if (!session.isAuthorized()) {
+            this.sessionManager.failed(session, UNAUTHORIZED, null);
+            return BootstrapResponse.badRequest("Unauthorized");
+        }
+
+        // Get the desired bootstrap config for the endpoint
+        final BootstrapConfig cfg = store.getBootstrap(endpoint, sender);
+        if (cfg == null) {
+            LOG.debug("No bootstrap config for {}", endpoint);
+            this.sessionManager.failed(session, NO_BOOTSTRAP_CONFIG, null);
+            return BootstrapResponse.badRequest("no bootstrap config");
+        }
+
+        // Start the bootstrap session
+        e.execute(new Runnable() {
+            @Override
+            public void run() {
+                startBootstrap(session, cfg);
+            }
+        });
+
+        return BootstrapResponse.success();
+    }
+
+    protected void startBootstrap(BootstrapSession session, BootstrapConfig cfg) {
+        delete(session, cfg);
+    }
+
+    protected void delete(final BootstrapSession session, final BootstrapConfig cfg) {
+
+        final BootstrapDeleteRequest deleteRequest = new BootstrapDeleteRequest();
+        send(session, deleteRequest, new ResponseCallback<BootstrapDeleteResponse>() {
+            @Override
+            public void onResponse(BootstrapDeleteResponse response) {
+                LOG.trace("Bootstrap delete {} return code {}", session.getEndpoint(), response.getCode());
+                List<Integer> instancesToWrite = new ArrayList<>(cfg.security.keySet());
+                writeSecurities(session, cfg, instancesToWrite);
+            }
+        }, new ErrorCallback() {
+            @Override
+            public void onError(Exception e) {
+                LOG.debug(String.format("Error during bootstrap delete '/' on %s", session.getEndpoint()), e);
+                sessionManager.failed(session, DELETE_FAILED, deleteRequest);
+            }
+        });
+    }
+
+    protected void writeSecurities(final BootstrapSession session, final BootstrapConfig cfg,
+            final List<Integer> securityInstancesToWrite) {
+        if (!securityInstancesToWrite.isEmpty()) {
+            // get next Security configuration
+            Integer key = securityInstancesToWrite.remove(0);
+            ServerSecurity securityConfig = cfg.security.get(key);
+
+            // create write request from it
+            LwM2mPath path = new LwM2mPath(0, key);
+            final LwM2mNode securityInstance = BootstrapUtil.convertToSecurityInstance(key, securityConfig);
+            final BootstrapWriteRequest writeBootstrapRequest = new BootstrapWriteRequest(path, securityInstance,
+                    session.getContentFormat());
+
+            // sent it
+            send(session, writeBootstrapRequest, new ResponseCallback<BootstrapWriteResponse>() {
+                @Override
+                public void onResponse(BootstrapWriteResponse response) {
+                    LOG.trace("Bootstrap write {} return code {}", session.getEndpoint(), response.getCode());
+                    // recursive call until securityInstancesToWrite is empty
+                    writeSecurities(session, cfg, securityInstancesToWrite);
+                }
+            }, new ErrorCallback() {
+                @Override
+                public void onError(Exception e) {
+                    LOG.debug(String.format("Error during bootstrap write of security instance %s on %s",
+                            securityInstance, session.getEndpoint()), e);
+                    sessionManager.failed(session, WRITE_SECURITY_FAILED, writeBootstrapRequest);
+                }
+            });
+        } else {
+            // we are done, write the servers now
+            List<Integer> serverInstancesToWrite = new ArrayList<>(cfg.servers.keySet());
+            writeServers(session, cfg, serverInstancesToWrite);
+        }
+    }
+
+    protected void writeServers(final BootstrapSession session, final BootstrapConfig cfg,
+            final List<Integer> serverInstancesToWrite) {
+        if (!serverInstancesToWrite.isEmpty()) {
+            // get next Server configuration
+            Integer key = serverInstancesToWrite.remove(0);
+            ServerConfig serverConfig = cfg.servers.get(key);
+
+            // create write request from it
+            LwM2mPath path = new LwM2mPath(1, key);
+            final LwM2mNode serverInstance = BootstrapUtil.convertToServerInstance(key, serverConfig);
+            final BootstrapWriteRequest writeServerRequest = new BootstrapWriteRequest(path, serverInstance,
+                    session.getContentFormat());
+
+            // sent it
+            send(session, writeServerRequest, new ResponseCallback<BootstrapWriteResponse>() {
+                @Override
+                public void onResponse(BootstrapWriteResponse response) {
+                    LOG.trace("Bootstrap write {} return code {}", session.getEndpoint(), response.getCode());
+                    // recursive call until serverInstancesToWrite is empty
+                    writeServers(session, cfg, serverInstancesToWrite);
+                }
+            }, new ErrorCallback() {
+                @Override
+                public void onError(Exception e) {
+                    LOG.warn(String.format("Error during bootstrap write of server instance %s on %s", serverInstance,
+                            session.getEndpoint()), e);
+                    sessionManager.failed(session, WRITE_SERVER_FAILED, writeServerRequest);
+                }
+            });
+        } else {
+            // we are done, write ACLs now
+            List<Integer> aclInstancesToWrite = new ArrayList<>(cfg.acls.keySet());
+            writedAcls(session, cfg, aclInstancesToWrite);
+        }
+    }
+
+    protected void writedAcls(final BootstrapSession session, final BootstrapConfig cfg,
+            final List<Integer> aclInstancesToWrite) {
+        if (!aclInstancesToWrite.isEmpty()) {
+            // get next ACL configuration
+            Integer key = aclInstancesToWrite.remove(0);
+            ACLConfig aclConfig = cfg.acls.get(key);
+
+            // create write request from it
+            LwM2mPath path = new LwM2mPath(2, key);
+            final LwM2mNode aclInstance = BootstrapUtil.convertToAclInstance(key, aclConfig);
+            final BootstrapWriteRequest writeACLRequest = new BootstrapWriteRequest(path, aclInstance,
+                    session.getContentFormat());
+
+            // sent it
+            send(session, writeACLRequest, new ResponseCallback<BootstrapWriteResponse>() {
+                @Override
+                public void onResponse(BootstrapWriteResponse response) {
+                    LOG.trace("Bootstrap write {} return code {}", session.getEndpoint(), response.getCode());
+                    // recursive call until aclInstancesToWrite is empty
+                    writedAcls(session, cfg, aclInstancesToWrite);
+                }
+            }, new ErrorCallback() {
+                @Override
+                public void onError(Exception e) {
+                    LOG.warn(String.format("Error during bootstrap write of acl instance %s on %s", aclInstance,
+                            session.getEndpoint()), e);
+                    sessionManager.failed(session, WRITE_ACL_FAILED, writeACLRequest);
+                }
+            });
+        } else {
+            // we are done, send bootstrap finished.
+            bootstrapFinished(session, cfg);
+        }
+    }
+
+    protected void bootstrapFinished(final BootstrapSession session, final BootstrapConfig cfg) {
+
+        final BootstrapFinishRequest finishBootstrapRequest = new BootstrapFinishRequest();
+        send(session, finishBootstrapRequest, new ResponseCallback<BootstrapFinishResponse>() {
+            @Override
+            public void onResponse(BootstrapFinishResponse response) {
+                LOG.trace("Bootstrap Finished {} return code {}", session.getEndpoint(), response.getCode());
+                if (response.isSuccess()) {
+                    sessionManager.end(session);
+                } else {
+                    sessionManager.failed(session, FINISHED_WITH_ERROR, finishBootstrapRequest);
+                }
+            }
+        }, new ErrorCallback() {
+            @Override
+            public void onError(Exception e) {
+                LOG.debug(String.format("Error during bootstrap finished on %s", session.getEndpoint()), e);
+                sessionManager.failed(session, SEND_FINISH_FAILED, finishBootstrapRequest);
+            }
+        });
+    }
+
+    protected <T extends LwM2mResponse> void send(BootstrapSession session, DownlinkRequest<T> request,
+            ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
+        sender.send(session.getEndpoint(), session.getIdentity(), request, requestTimeout, responseCallback,
+                errorCallback);
+    }
+}


Test patch:
diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java
index e9446abc..965220d9 100644
--- a/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java
+++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java
@@ -43,7 +43,7 @@ public class BootstrapHandlerTest {
     public void error_if_not_authorized() {
         final BootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(false);
 
-        BootstrapHandler bsHandler = new BootstrapHandler(null, null, bsSessionManager);
+        BootstrapHandler bsHandler = new DefaultBootstrapHandler(null, null, bsSessionManager);
         BootstrapResponse bootstrapResponse = bsHandler
                 .bootstrap(Identity.psk(new InetSocketAddress(4242), "pskdentity"), new BootstrapRequest("enpoint"));
         assertEquals(ResponseCode.BAD_REQUEST, bootstrapResponse.getCode());
@@ -61,7 +61,7 @@ public class BootstrapHandlerTest {
             }
         };
 
-        BootstrapHandler bsHandler = new BootstrapHandler(bsStore, requestSender, bsSessionManager, new Executor() {
+        BootstrapHandler bsHandler = new DefaultBootstrapHandler(bsStore, requestSender, bsSessionManager, new Executor() {
 
             @Override
             public void execute(Runnable command) {
@@ -86,7 +86,7 @@ public class BootstrapHandlerTest {
             }
         };
 
-        BootstrapHandler bsHandler = new BootstrapHandler(bsStore, requestSender, bsSessionManager, new Executor() {
+        BootstrapHandler bsHandler = new DefaultBootstrapHandler(bsStore, requestSender, bsSessionManager, new Executor() {
 
             @Override
             public void execute(Runnable command) {