
org.eclipse.leshan.client.demo.cli.LeshanClientDemoCLI Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of leshan-client-demo Show documentation
Show all versions of leshan-client-demo Show documentation
A demonstration client built upon the Leshan client.
The newest version!
/*******************************************************************************
* Copyright (c) 2021 Sierra Wireless and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v20.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.client.demo.cli;
import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.scandium.dtls.cipher.CipherSuite;
import org.eclipse.leshan.core.CertificateUsage;
import org.eclipse.leshan.core.demo.cli.MultiParameterException;
import org.eclipse.leshan.core.demo.cli.StandardHelpOptions;
import org.eclipse.leshan.core.demo.cli.VersionProvider;
import org.eclipse.leshan.core.demo.cli.converters.CIDConverter;
import org.eclipse.leshan.core.demo.cli.converters.InetAddressConverter;
import org.eclipse.leshan.core.demo.cli.converters.ResourcePathConverter;
import org.eclipse.leshan.core.demo.cli.converters.StrictlyPositiveIntegerConverter;
import org.eclipse.leshan.core.endpoint.Protocol;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.util.StringUtils;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.ITypeConverter;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Spec;
/**
* This is the class defining the Command Line Interface of Leshan Client Demo.
*/
@Command(name = "leshan-client-demo",
sortOptions = false,
description = "%n"//
+ "@|italic " //
+ "This is a LWM2M client demo implemented with Leshan library.%n" //
+ "You can launch it without any option and it will try to register to a LWM2M server at " + "coap://"
+ LeshanClientDemoCLI.DEFAULT_COAP_URL + ".%n" //
+ "%n" //
+ "Californium is used as CoAP library and some CoAP parameters can be tweaked in 'Californium.properties' file." //
+ "|@%n%n",
versionProvider = VersionProvider.class)
public class LeshanClientDemoCLI implements Runnable {
public static final String DEFAULT_COAP_URL = "localhost:" + CoAP.DEFAULT_COAP_PORT;
public static final String DEFAULT_COAPS_URL = "localhost:" + CoAP.DEFAULT_COAP_SECURE_PORT;
private static String defaultEndpoint() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
return "LeshanClientDemo";
}
}
@Mixin
public StandardHelpOptions helpsOptions;
/* ********************************** General Section ******************************** */
@ArgGroup(validate = false, heading = "%n")
public GeneralSection main = new GeneralSection();
public static class GeneralSection {
@Option(names = { "-u", "--server-url" },
description = { //
"Set the server URL. If port is missing it will be added automatically with default value.", //
"Default: ", //
" - " + DEFAULT_COAP_URL + " for coap", //
" - " + DEFAULT_COAPS_URL + " for coaps" })
public String url;
@Option(names = { "-b", "--bootstrap" },
description = { "Do bootstrap instead of registration.",
"In this case your server-url should target a LWM2M bootstrap server instead of a LWM2M server." })
public boolean bootstrap;
@Option(names = { "-n", "--endpoint-name" },
description = { //
"Set the endpoint name of the Client.", //
"Default the hostname or 'LeshanClientDemo' if no hostname." })
public String endpoint = LeshanClientDemoCLI.defaultEndpoint();
@Option(names = { "-l", "--lifetime" },
defaultValue = "300" /* 5 minutes */,
description = { //
"The registration lifetime in seconds.", //
"Ignored if -b is used.", //
"Default : ${DEFAULT-VALUE}s." },
converter = StrictlyPositiveIntegerConverter.class)
public Integer lifetimeInSec;
@Option(names = { "-cp", "--communication-period" },
description = { //
"The communication period in seconds", //
"It should be smaller than the lifetime.", //
"It will be used even if -b is used." },
converter = StrictlyPositiveIntegerConverter.class)
public Integer comPeriodInSec;
@Option(names = { "-q", "--queue-mode" }, description = { "Client use queue mode (not fully implemented)." })
public boolean queueMode;
@Option(names = { "-lh", "--local-address" },
description = { //
"Set the local CoAP address of the Client.", //
"Default: any local address." },
converter = InetAddressConverter.class)
public InetAddress localAddress;
@Option(names = { "-m", "--models-folder" },
description = { //
"A folder which contains object models in OMA DDF(xml)format." })
public File modelsFolder;
@Option(names = { "-aa", "--additional-attributes" },
description = { //
"Use additional attributes at registration time.", //
"syntax is :", //
"-aa attrName1=attrValue1,attrName2=\\\"attrValue2\\\"" },
split = ",")
public Map additionalAttributes;
@Option(names = { "-bsaa", "--bootstrap-additional-attributes" },
description = { //
"Use additional attributes at bootstrap time.", //
"syntax is :", //
" -bsaa attrName1=attrValue1,attrName2=\\\"attrValue2\\\"" },
split = ",")
public Map bsAdditionalAttributes;
@Option(names = { "-ocf", "--support-old-format" },
description = { //
"Activate support of old/unofficial content format.", //
"See https://github.com/eclipse/leshan/pull/720" })
public boolean supportOldFormat;
@Option(names = { "-jc", "--use-java-coap" },
description = { //
"Use java-coap for CoAP protocol instead of Californium." })
public boolean useJavaCoap;
@Option(names = { "-fb", "--factory-bootstrap" },
description = { //
"Can be used to initialize existing resource with custom value.", //
"LWM2M Text Content format encoding should be used for resource value", //
"E.g. to change Short Server ID :", //
" -fb /0/0/10=1234,/1/0/0=1234", //
},
split = ",",
converter = ResourcePathConverter.class)
public Map factoryBootstrap;
}
/* ********************************** Location Section ******************************** */
@ArgGroup(validate = false,
heading = "%n@|bold,underline Object Location Options|@ %n%n"//
+ "@|italic " //
+ "A very Simple implementation of Object (6) location with simulated values is provided. Those options aim to set this object." //
+ "|@%n%n")
public LocationSection location = new LocationSection();
public static class LocationSection {
@Option(names = { "-pos", "--initial-position" },
defaultValue = "random",
description = { //
"Set the initial location (latitude, longitude) of the device to be reported by the Location object.", //
"Format: lat_float:long_float" },
converter = PositionConverter.class)
public Position position;
public static class Position {
public Float latitude;
public Float longitude;
};
private static class PositionConverter implements ITypeConverter {
@Override
public Position convert(String pos) {
Position position = new Position();
if (pos.equals("random"))
return position;
int colon = pos.indexOf(':');
if (colon == -1 || colon == 0 || colon == pos.length() - 1)
throw new IllegalArgumentException(
"Position must be a set of two floats separated by a colon, e.g. 48.131:11.459");
position.latitude = Float.valueOf(pos.substring(0, colon));
position.longitude = Float.valueOf(pos.substring(colon + 1));
return position;
}
};
@Option(names = { "-sf", "--scale-factor" },
defaultValue = "1.0",
description = { //
"Scale factor to apply when shifting position.", //
"Default is ${DEFAULT-VALUE}." })
public Float scaleFactor;
}
/* ********************************** DTLS Section ******************************** */
@ArgGroup(validate = false,
heading = "%n@|bold,underline DTLS Options|@ %n%n"//
+ "@|italic " //
+ "Here some options aiming to configure the client behavior when it uses CoAP over DTLS." //
+ "%n" //
+ "Scandium is used as DTLS library and some DTLS parameters can be tweaked in 'Californium.properties' file." //
+ "|@%n%n")
public DTLSSection dtls = new DTLSSection();
public static class DTLSSection {
@Option(names = { "-r", "--rehanshake-on-update" },
description = { //
"Force reconnection/rehandshake on registration update." })
public boolean reconnectOnUpdate;
@Option(names = { "-f", "--force-full-handshake" },
description = { //
"By default client will try to resume DTLS session by using abbreviated Handshake. This option force to always do a full handshake." })
public boolean forceFullhandshake;
@Option(names = { "-cid", "--connection-id" },
defaultValue = "off",
description = { //
"Control usage of DTLS connection ID.", //
"- 'on' to activate Connection ID support (same as -cid 0)", //
"- 'off' to deactivate it", //
"- Positive value define the size in byte of CID generated.", //
"- 0 value means we accept to use CID but will not generated one for foreign peer.", //
"Default: off" },
converter = ClientCIDConverter.class)
public Integer cid;
private static class ClientCIDConverter extends CIDConverter {
public ClientCIDConverter() {
super(0);
}
};
@Option(names = { "-c", "--cipher-suites" }, //
description = { //
"Define cipher suites to use.", //
"CipherCuite enum value separated by ',' without spaces.", //
"E.g: TLS_PSK_WITH_AES_128_CCM_8,TLS_PSK_WITH_AES_128_CCM " },
split = ",")
public List ciphers;
@Option(names = { "-oc", "--support-deprecated-ciphers" },
description = { //
"Activate support of old/deprecated cipher suites." })
public boolean supportDeprecatedCiphers;
}
/* ********************************** Identity Section ******************************** */
@ArgGroup(exclusive = true)
public IdentitySection identity = new IdentitySection();
/* ********************************** OSCORE Section ******************************** */
@ArgGroup(exclusive = false,
heading = "%n@|bold,underline OSCORE Options|@ %n%n"//
+ "@|italic " //
+ "By default Leshan demo does not use OSCORE.%n"//
+ "|@" + "@|red, OSCORE implementation in Leshan is in an experimental state.|@%n" //
+ "%n")
public OscoreSection oscore;
@Spec
CommandSpec spec;
@Override
public void run() {
// Some post-validation which imply several options.
// For validation about only one option, just use ITypeConverter instead
// check certificate usage
if (identity.isx509()) {
if (identity.getX509().certUsage == CertificateUsage.SERVICE_CERTIFICATE_CONSTRAINT
&& identity.getX509().trustStore.isEmpty()) {
throw new MultiParameterException(spec.commandLine(),
"You need to set a truststore when you are using \"service certificate constraint\" usage",
"-cu", "-ts");
}
}
// check OSCORE
if (oscore != null) {
oscore.validateOscoreSetting(spec.commandLine());
}
normalizedServerUrl();
// validate url.
// extract scheme
int indexOf = main.url.indexOf("://");
String scheme = main.url.substring(0, indexOf);
// check URI scheme is supported
List supportedUnsecuredProtocol = Arrays.asList(Protocol.COAP, Protocol.COAP_TCP) //
.stream().map(Protocol::getUriScheme).collect(Collectors.toList());
List supportedTlsBasedProtocol = Arrays.asList(Protocol.COAPS, Protocol.COAPS_TCP) //
.stream().map(Protocol::getUriScheme).collect(Collectors.toList());
List allSupportedProtocol = Stream
.concat(supportedUnsecuredProtocol.stream(), supportedTlsBasedProtocol.stream())
.collect(Collectors.toList());
if (!allSupportedProtocol.contains(scheme)) {
throw new MultiParameterException(spec.commandLine(),
String.format("Invalid URL %s : unknown scheme '%s', we support only %s for now", main.url, scheme,
String.join(" or ", allSupportedProtocol)),
"-u");
}
// check scheme matches configuration
if (identity.hasIdentity()) {
if (!supportedTlsBasedProtocol.contains(scheme)) {
throw new MultiParameterException(spec.commandLine(), String.format(
"Invalid URL %s : '%s' scheme must be used without PSK, RPK or x509 option. Do you mean %s ? ",
main.url, scheme, String.join(" or ", supportedTlsBasedProtocol)), "-u");
}
} else {
if (!supportedUnsecuredProtocol.contains(scheme)) {
throw new MultiParameterException(spec.commandLine(), String.format(
"Invalid URL %s : '%s' scheme must be used with PSK, RPK or x509 option. Do you mean %s ? ",
main.url, scheme, String.join(" or ", supportedUnsecuredProtocol)), "-u");
}
}
}
protected void normalizedServerUrl() {
String url = main.url;
if (url == null)
url = "localhost";
// try to guess if port is present.
String[] splittedUrl = url.split(":");
String port = splittedUrl[splittedUrl.length - 1];
if (!StringUtils.isNumeric(port)) {
// it seems port is not present, so we try to add it
if (identity.hasIdentity()) {
main.url = url + ":" + CoAP.DEFAULT_COAP_SECURE_PORT;
} else {
main.url = url + ":" + CoAP.DEFAULT_COAP_PORT;
}
}
// try to guess if scheme is present :
if (!main.url.contains("://")) {
// it seems scheme is not present try to add it
if (identity.hasIdentity()) {
main.url = "coaps://" + main.url;
} else {
main.url = "coap://" + main.url;
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy