diff --git a/demo-apps/cf-cloud-demo-server/README.md b/demo-apps/cf-cloud-demo-server/README.md new file mode 100644 index 0000000000..ea6eae61d2 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/README.md @@ -0,0 +1,115 @@ +![Californium logo](../../cf_64.png) + +# Californium (Cf) - Cloud Demo Server + +!!! Under construction !!! + +Simple cloud demo server, sets up only coaps/DTLS endpoints. + +Supports an optional, very simple HTTPS server to read the last CoAP POSTs to resource "devices" or resource "echo" with query-parameter "keep". + +## General Usage + +Start the cf-cloud-demo-server-3.8.0.jar with: + +```sh +java -jar cf-cloud-demo-server-3.8.0.jar -h + +Usage: CloudDemoServer [-h] [--trust-all] [--client-auth=] + [--https-credentials=] + [--https-port=] [--wildcard-interface | [[-- + [no-]loopback] [--[no-]external] [--[no-]ipv4] [--[no-] + ipv6] [--interfaces-pattern=[, + ...]]...]] [--psk-file= + [--psk-file-password64=] + [--psk-file-export=] + [--psk-file-export-password64=]] + [--store-file= --store-max-age= + [--store-password64=]] + --client-auth= + client authentication. Values NONE, WANTED, NEEDED. + -h, --help display a help message + --https-credentials= + Folder to https credentials. + --https-port= + Port of https service. + --interfaces-pattern=[,...] + interface regex patterns for endpoints. + --[no-]external enable endpoints on external network. + --[no-]ipv4 enable endpoints for ipv4. + --[no-]ipv6 enable endpoints for ipv6. + --[no-]loopback enable endpoints on loopback network. + --psk-file= File name of PSK store. + --psk-file-export= + File name for export PSK store. Defaults to + --psk-file, if --psk-file-export-password64 is + provided. + --psk-file-export-password64= + Password for export PSK store. Base 64 encoded. + --psk-file-password64= + Password for PSK store. Base 64 encoded. + --store-file= file store dtls state. + --store-max-age= + maximum age of connections in hours. + --store-password64= + password to store dtls state. Base 64 encoded. + --trust-all trust all valid certificates. + --wildcard-interface Use wildcard-address for local network interfaces. + +Examples: + DemoServer --no-loopback + (DemoServer listening only on external network interfaces.) + + DemoServer --psk-file device.psk --store-file dtls.bin --store-max-age 168 \ + --store-password64 ZVhiRW5pdkx1RUs2dmVoZg== + (DemoServer with PSK credentials from file and dtls-graceful restart. + Devices/sessions with no exchange for more then a week (168 hours) + are skipped when saving.) + + DemoServer --psk-file device.psk \ + --psk-file-export-password64 V3plQUdkTnFLQjRnZWtSeg== + (DemoServer encrypts plain PSK credentials file (in place). + Exits afterwards.) +``` + +To see the set of options and arguments. + +## DTLS Graceful Restart + +The cloud demo server supports to save the DTLS connection state and load it again. With this feature, it's possible to restart the server without losing the DTLS connection state. Provide the arguments `--store-file` (filename to save and load the DTLS connection state), `--store-password64` (base64 encoded password to save and load the DTLS connection state), and `--store-max-age` (maximum age of connections to be stored. Value in hours) are provided. + +Stop the server and start it again using the same `--store-file` and `--store-password64` as before and also provide the `--store-max-age`. + +Note: if it takes too long between stopping and restart, the clients will detect a timeout and trigger new handshakes. So just pause a small couple of seconds! + +Note: only the DTLS state is persisted. To use this feature, the client is intended to use mainly CON request and the server the use piggybacked responses. Neither DTLS handshakes, separate responses, observe/notifies, nor blockwise transfers are supported. + +## HTTPS x509 certificate + +One x509 provider is [letsencrypt.org](https://letsencrypt.org/). Install `certbot` and request a x509 http server certificate. + +``` +sudo certbot certonly --standalone --key-type ecdsa --elliptic-curve secp256r1 -d +``` + +Usually the received credentials are stored in `/etc/letsencrypt/live/` and you must add a group as reader for the private key (see instructions of `certbot`). + +## Systemd service + +The server runs as [systemd service](service/cali.service). Please adapt the DOMAIN. +Copy the edited `cali.service` into `/etc/systemd/system` + +## HTTPS forwarding + +The server runs as user and therefore required to forward TCP:443 to a user service port (8080). Copy [iptables service](service/iptables.service) into `/etc/systemd/system` and [iptables-firewall.sh](service/iptables-firewall.sh) into `sbin` and make that file executable. + + +## Missing + +- cloudinit setup +- user/group setup +- fail2ban setup + +See for now [cf-unix-setup](https://github.com/eclipse/californium/tree/main/demo-apps/cf-unix-setup). + +!!! Under construction !!! diff --git a/demo-apps/cf-cloud-demo-server/demo.psk b/demo-apps/cf-cloud-demo-server/demo.psk new file mode 100644 index 0000000000..78aeaab5b0 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/demo.psk @@ -0,0 +1,3 @@ +# PSK store for Cloud Demo +Client_identity=c2VjcmV0UFNL + diff --git a/demo-apps/cf-cloud-demo-server/pom.xml b/demo-apps/cf-cloud-demo-server/pom.xml new file mode 100755 index 0000000000..044e2bcee0 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/pom.xml @@ -0,0 +1,68 @@ + + + + 4.0.0 + + + org.eclipse.californium + demo-apps + 3.8.0-SNAPSHOT + + cf-cloud-demo-server + jar + + Cf-CloudDemoServer + Californium (Cf) Cloud Demo server + + + org.eclipse.californium.cloud.DemoServer + false + false + false + + + + + ${project.groupId} + californium-core + + + ${project.groupId} + scandium + + + ${project.groupId} + cf-unix-health + ${project.version} + + + info.picocli + picocli + + + com.upokecenter + cbor + + + com.google.code.gson + gson + + + + + ${project.groupId} + demo-certs + runtime + + + + + + + maven-assembly-plugin + + + + + + \ No newline at end of file diff --git a/demo-apps/cf-cloud-demo-server/service/cali.service b/demo-apps/cf-cloud-demo-server/service/cali.service new file mode 100644 index 0000000000..a4d85d12df --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/cali.service @@ -0,0 +1,59 @@ +#/******************************************************************************* +# * Copyright (c) 2022 Contributors to the Eclipse Foundation. +# * +# * 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. +# * +# ******************************************************************************/ +# +# To install, cp to /etc/systemd/system +# +# The value of "TasksMax" is increasing with the numbers of connectors +# according the used networkconfig. +# +# Use +# top -H +# +# to see the number of threads +# +# In order to update the service, cp the new .jar to +# /home/cali/cf-cloud-demo-server-update.jar +# +# on +# systemctl restart cali +# +# that file is copied to cf-cloud-demo-server.jar and executed. +# If cf-cloud-demo-server.jar is updated inplace when running, +# that my cause unintended exceptions, which prevents Californium +# from successfully gracefull-restart of the dtls state. +# + +[Unit] +Description=Californium Cloud Demo Server +BindsTo=network-online.target +After=network-online.target +RequiresMountsFor=/home + +[Service] +Type=simple +TasksMax=256 +User=cali +WorkingDirectory=/home/cali +Environment="JAR=cf-cloud-demo-server.jar" +Environment="ARGS=--no-loopback --store-file=connections.bin --store-max-age=72 --store-password64=TDNLOmJTWi13JUs/YGdvNA== --psk-file demo.psk --https-credentials /etc/letsencrypt/live/cloudcoap.io --https-port 8080" +Environment="S3_ARGS=--s3-endpoint https://??? --s3-access-key ??? --s3-secret ??? --s3-bucket devices --s3-acl public-read --s3-device-list list --s3-concurrency 400" +Environment="OPTS=-XX:MaxRAMPercentage=75 -Dlogback.configurationFile=./logback.xml" +ExecStartPre=/bin/cp -u cf-cloud-demo-server-update.jar cf-cloud-demo-server.jar +ExecStart=/usr/bin/java $OPTS -jar ${JAR} $ARGS $S3_ARGS +RestartSec=10 +Restart=always +OOMPolicy=stop + +[Install] +WantedBy=multi-user.target diff --git a/demo-apps/cf-cloud-demo-server/service/iptables-firewall.sh b/demo-apps/cf-cloud-demo-server/service/iptables-firewall.sh new file mode 100755 index 0000000000..c473add3ce --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/iptables-firewall.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +#/******************************************************************************* +# * Copyright (c) 2022 Contributors to the Eclipse Foundation. +# * +# * 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. +# * +# ******************************************************************************/ +# +# To install, cp to /sbin/iptables-firewall.sh + + +# Limit PATH +PATH="/sbin:/usr/sbin:/bin:/usr/bin" + +# iptables configuration +firewall_start() { + # Define https forward + iptables -t nat -I PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8080 + ip6tables -t nat -I PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8080 +} + +# clear iptables configuration +firewall_stop() { + # Define https forward + iptables -t nat -D PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8080 + ip6tables -t nat -D PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8080 +} + +# execute action +case "$1" in + start|restart) + echo "Starting firewall" + firewall_stop + firewall_start + ;; + stop) + echo "Stopping firewall" + firewall_stop + ;; +esac diff --git a/demo-apps/cf-cloud-demo-server/service/iptables.service b/demo-apps/cf-cloud-demo-server/service/iptables.service new file mode 100644 index 0000000000..bfd437d514 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/iptables.service @@ -0,0 +1,32 @@ +#/******************************************************************************* +# * Copyright (c) 2022 Contributors to the Eclipse Foundation. +# * +# * 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. +# * +# ******************************************************************************/ +# +# To install, cp to /etc/systemd/system +# +# Requires /sbin/iptables-firewall.sh + +[Unit] +Description=iptables firewall service +After=network.target + +[Service] +Type=oneshot +ExecStart=/sbin/iptables-firewall.sh start +RemainAfterExit=true +ExecStop=/sbin/iptables-firewall.sh stop +StandardOutput=journal + +[Install] +WantedBy=multi-user.target + diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/BaseServer.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/BaseServer.java new file mode 100644 index 0000000000..7a967028de --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/BaseServer.java @@ -0,0 +1,677 @@ +/******************************************************************************* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * 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. + * + ******************************************************************************/ +package org.eclipse.californium.cloud; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.security.GeneralSecurityException; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.crypto.SecretKey; +import javax.net.ssl.KeyManager; +import javax.net.ssl.X509KeyManager; + +import org.eclipse.californium.cloud.resources.Devices; +import org.eclipse.californium.cloud.resources.Diagnose; +import org.eclipse.californium.cloud.resources.MyContext; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.config.CoapConfig; +import org.eclipse.californium.core.config.CoapConfig.MatcherMode; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.core.network.Endpoint; +import org.eclipse.californium.core.network.interceptors.HealthStatisticLogger; +import org.eclipse.californium.core.observe.ObserveStatisticLogger; +import org.eclipse.californium.core.server.resources.Resource; +import org.eclipse.californium.elements.EndpointContextMatcher; +import org.eclipse.californium.elements.PrincipalEndpointContextMatcher; +import org.eclipse.californium.elements.config.CertificateAuthenticationMode; +import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.elements.config.Configuration.DefinitionsProvider; +import org.eclipse.californium.elements.config.SystemConfig; +import org.eclipse.californium.elements.config.TimeDefinition; +import org.eclipse.californium.elements.util.Bytes; +import org.eclipse.californium.elements.util.ClockUtil; +import org.eclipse.californium.elements.util.CounterStatisticManager; +import org.eclipse.californium.elements.util.DatagramWriter; +import org.eclipse.californium.elements.util.EncryptedPersistentComponentUtil; +import org.eclipse.californium.elements.util.ExecutorsUtil; +import org.eclipse.californium.elements.util.NamedThreadFactory; +import org.eclipse.californium.elements.util.NetworkInterfacesUtil; +import org.eclipse.californium.elements.util.NetworkInterfacesUtil.InetAddressFilter; +import org.eclipse.californium.elements.util.NetworkInterfacesUtil.SimpleInetAddressFilter; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.elements.util.StringUtil; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.DtlsHealthLogger; +import org.eclipse.californium.scandium.MdcConnectionListener; +import org.eclipse.californium.scandium.config.DtlsConfig; +import org.eclipse.californium.scandium.config.DtlsConfig.DtlsRole; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.cipher.CipherSuite; +import org.eclipse.californium.scandium.dtls.pskstore.AdvancedPskStore; +import org.eclipse.californium.scandium.dtls.pskstore.MultiPskFileStore; +import org.eclipse.californium.scandium.dtls.x509.AsyncKeyManagerCertificateProvider; +import org.eclipse.californium.scandium.dtls.x509.AsyncNewAdvancedCertificateVerifier; +import org.eclipse.californium.scandium.util.SecretUtil; +import org.eclipse.californium.unixhealth.NetSocketHealthLogger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import picocli.CommandLine; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.ParseResult; + +/** + * The basic cloud server. + * + * Creates {@link Endpoint}s using DTLS. Adds resources {@link Diagnose} and + * {@link MyContext}. + * + * @since 3.8 + */ +public class BaseServer extends CoapServer { + + static { + // only coap + dtls + CoapConfig.register(); + DtlsConfig.register(); + } + + private static final Logger STATISTIC_LOGGER = LoggerFactory.getLogger("org.eclipse.californium.statistics"); + + private static final int DEFAULT_MAX_CONNECTIONS = 200000; + private static final int DEFAULT_MAX_RESOURCE_SIZE = 8192; + private static final int DEFAULT_MAX_MESSAGE_SIZE = 1280; + private static final int DEFAULT_BLOCK_SIZE = 1024; + + private static final char[] KEY_STORE_PASSWORD = "endPass".toCharArray(); + private static final String KEY_STORE_LOCATION = "certs/keyStore.jks"; + private static final char[] TRUST_STORE_PASSWORD = "rootPass".toCharArray(); + private static final String TRUST_STORE_LOCATION = "certs/trustStore.jks"; + + // exit codes for runtime errors + public static final int ERR_INIT_FAILED = 1; + + public static final List PRESELECTED_CIPHER_SUITES = Arrays.asList( + CipherSuite.TLS_PSK_WITH_AES_128_CCM_8, CipherSuite.TLS_ECDHE_PSK_WITH_AES_128_CCM_8_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8, CipherSuite.TLS_PSK_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_PSK_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256); + + public enum InterfaceType { + LOCAL, EXTERNAL, IPV4, IPV6, + } + + /** + * Interval to read number of dropped udp messages. + */ + public static final TimeDefinition UDP_DROPS_READ_INTERVAL = new TimeDefinition("UDP_DROPS_READ_INTERVAL", + "Interval to read upd drops from OS (currently only Linux).", 2000, TimeUnit.MILLISECONDS); + + public static DefinitionsProvider DEFAULTS = new DefinitionsProvider() { + + @Override + public void applyDefinitions(Configuration config) { + int processors = Runtime.getRuntime().availableProcessors(); + config.set(SystemConfig.HEALTH_STATUS_INTERVAL, 300, TimeUnit.SECONDS); + config.set(CoapConfig.MAX_RESOURCE_BODY_SIZE, DEFAULT_MAX_RESOURCE_SIZE); + config.set(CoapConfig.MAX_MESSAGE_SIZE, DEFAULT_MAX_MESSAGE_SIZE); + config.set(CoapConfig.PREFERRED_BLOCK_SIZE, DEFAULT_BLOCK_SIZE); + config.set(CoapConfig.NOTIFICATION_CHECK_INTERVAL_COUNT, 4); + config.set(CoapConfig.NOTIFICATION_CHECK_INTERVAL_TIME, 30, TimeUnit.SECONDS); + config.set(CoapConfig.MAX_ACTIVE_PEERS, DEFAULT_MAX_CONNECTIONS); + config.set(CoapConfig.PEERS_MARK_AND_SWEEP_MESSAGES, 16); + config.set(CoapConfig.DEDUPLICATOR, CoapConfig.DEDUPLICATOR_PEERS_MARK_AND_SWEEP); + config.set(CoapConfig.RESPONSE_MATCHING, MatcherMode.PRINCIPAL_IDENTITY); + config.set(DtlsConfig.DTLS_ROLE, DtlsRole.SERVER_ONLY); + config.set(DtlsConfig.DTLS_AUTO_HANDSHAKE_TIMEOUT, null, TimeUnit.SECONDS); + config.set(DtlsConfig.DTLS_CONNECTION_ID_LENGTH, 6); + config.set(DtlsConfig.DTLS_PRESELECTED_CIPHER_SUITES, PRESELECTED_CIPHER_SUITES); + config.set(DtlsConfig.DTLS_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS); + config.set(DtlsConfig.DTLS_READ_WRITE_LOCK_CONNECTION_STORE, true); + config.set(DtlsConfig.DTLS_REMOVE_STALE_DOUBLE_PRINCIPALS, true); + config.set(DtlsConfig.DTLS_SERVER_USE_SESSION_ID, false); + config.set(DtlsConfig.DTLS_RECEIVE_BUFFER_SIZE, 1000000); + config.set(DtlsConfig.DTLS_RECEIVER_THREAD_COUNT, processors > 3 ? 2 : 1); + config.set(DtlsConfig.DTLS_MAC_ERROR_FILTER_QUIET_TIME, 4, TimeUnit.SECONDS); + config.set(DtlsConfig.DTLS_MAC_ERROR_FILTER_THRESHOLD, 8); + config.set(UDP_DROPS_READ_INTERVAL, 2000, TimeUnit.MILLISECONDS); + } + }; + + public static class ServerConfig { + + @Option(names = { "-h", "--help" }, usageHelp = true, description = "display a help message") + public boolean helpRequested; + + @ArgGroup(exclusive = true) + public NetworkConfig network; + + public static class NetworkConfig { + + @Option(names = "--wildcard-interface", description = "Use wildcard-address for local network interfaces.") + public boolean wildcard; + + @ArgGroup(exclusive = false) + public NetworkSelectConfig selectInterfaces; + } + + public static class NetworkSelectConfig { + + @Option(names = "--no-loopback", negatable = true, description = "enable endpoints on loopback network.") + public boolean loopback = true; + + @Option(names = "--no-external", negatable = true, description = "enable endpoints on external network.") + public boolean external = true; + + @Option(names = "--no-ipv4", negatable = true, description = "enable endpoints for ipv4.") + public boolean ipv4 = true; + + @Option(names = "--no-ipv6", negatable = true, description = "enable endpoints for ipv6.") + public boolean ipv6 = true; + + @Option(names = "--interfaces-pattern", split = ",", description = "interface regex patterns for endpoints.") + public List interfacePatterns; + + public InetAddressFilter getFilter(String tag) { + if (interfacePatterns == null || interfacePatterns.isEmpty()) { + return new SimpleInetAddressFilter(tag, external, loopback, ipv4, ipv6); + } else { + String[] patterns = new String[interfacePatterns.size()]; + patterns = interfacePatterns.toArray(patterns); + return new SimpleInetAddressFilter(tag, external, loopback, ipv4, ipv6, patterns); + } + } + } + + @Option(names = "--https-credentials", description = "Folder to https credentials.") + public String httpsCredentials; + + @Option(names = "--https-port", description = "Port of https service.") + public int httpsPort; + + @Option(names = "--trust-all", description = "trust all valid certificates.") + public boolean trustall; + + @Option(names = "--client-auth", description = "client authentication. Values ${COMPLETION-CANDIDATES}.") + public CertificateAuthenticationMode clientAuth; + + @ArgGroup(exclusive = false) + public PskStore pskStore; + + public static class PskStore { + + @Option(names = "--psk-file", required = true, description = "File name of PSK store.") + public String file; + + @Option(names = "--psk-file-password64", required = false, description = "Password for PSK store. Base 64 encoded.") + public String password64; + + @Option(names = "--psk-file-export", required = false, description = "File name for export PSK store. Defaults to --psk-file, if --psk-file-export-password64 is provided.") + public String exportFile; + + @Option(names = "--psk-file-export-password64", required = false, description = "Password for export PSK store. Base 64 encoded.") + public String exportPassword64; + } + + @ArgGroup(exclusive = false) + public Store store; + + public static class Store { + + @Option(names = "--store-file", required = true, description = "file store dtls state.") + public String file; + + @Option(names = "--store-max-age", required = true, description = "maximum age of connections in hours.") + public Integer maxAge; + + @Option(names = "--store-password64", required = false, description = "password to store dtls state. Base 64 encoded.") + public String password64; + + } + + /** + * Setup dependent defaults. + */ + public void defaults() { + if (network == null) { + network = new NetworkConfig(); + network.wildcard = true; + } + } + } + + public static final String CALIFORNIUM_BUILD_VERSION; + + static { + String version = StringUtil.CALIFORNIUM_VERSION; + if (version != null) { + String build = StringUtil.readFile(new File("build"), null); + if (build != null && !build.isEmpty()) { + version = version + "_" + build; + } + } else { + version = ""; + } + CALIFORNIUM_BUILD_VERSION = version; + } + + public static void start(String[] args, String name, ServerConfig config, BaseServer server) { + + CommandLine cmd = new CommandLine(config); + try { + ParseResult result = cmd.parseArgs(args); + if (result.isVersionHelpRequested()) { + System.out.println("\nCalifornium (Cf) " + cmd.getCommandName() + " " + CALIFORNIUM_BUILD_VERSION); + cmd.printVersionHelp(System.out); + System.out.println(); + } + if (result.isUsageHelpRequested()) { + cmd.usage(System.out); + return; + } + } catch (ParameterException ex) { + System.err.println(ex.getMessage()); + System.err.println(); + cmd.usage(System.err); + System.exit(-1); + } + + config.defaults(); + // forward cli parameter + if (config.clientAuth != null) { + server.getConfig().set(DtlsConfig.DTLS_CLIENT_AUTHENTICATION_MODE, config.clientAuth); + } + + if (config.pskStore != null) { + if (config.pskStore.exportFile != null || config.pskStore.exportPassword64 != null) { + // export PSK only + exportPsk(config.pskStore); + return; + } + } + + // print startup message + long max = Runtime.getRuntime().maxMemory(); + StringBuilder builder = new StringBuilder(name); + if (!CALIFORNIUM_BUILD_VERSION.isEmpty()) { + builder.append(", version ").append(CALIFORNIUM_BUILD_VERSION); + } + builder.append(", ").append(max / (1024 * 1024)).append("MB heap, started ..."); + LOGGER.info("{}", builder); + + // management statistic + STATISTIC_LOGGER.error("start!"); + ManagementStatistic management = new ManagementStatistic(STATISTIC_LOGGER); + + if (config.httpsCredentials != null) { + LOGGER.info("HTTPS service at port {}", config.httpsPort); + HttpService.createHttpService(config.httpsPort, config.httpsCredentials); + } + + // create server + try { + server.initialize(config); + if (server.getEndpoints().isEmpty()) { + System.err.println("no endpoint available!"); + System.exit(ERR_INIT_FAILED); + } + } catch (Exception e) { + System.err.printf("Failed to create " + BaseServer.class.getSimpleName() + ": %s\n", e.getMessage()); + e.printStackTrace(System.err); + System.err.println("Exiting"); + System.exit(ERR_INIT_FAILED); + } + + if (config.store != null) { + server.setupPersistence(config.store); + } + + server.start(); + + LOGGER.info("{} started ...", name); + + if (config.httpsCredentials != null) { + HttpService.startHttpService(); + LOGGER.info("HTTPS service at port {} started", config.httpsPort); + } + long interval = server.getConfig().get(SystemConfig.HEALTH_STATUS_INTERVAL, TimeUnit.MILLISECONDS); + long inputTimeout = interval < 15000 ? interval : 15000; + long lastGcCount = 0; + long lastDumpNanos = ClockUtil.nanoRealtime(); + for (;;) { + try { + Thread.sleep(inputTimeout); + } catch (InterruptedException e) { + break; + } + long gcCount = management.getCollectionCount(); + if (lastGcCount < gcCount) { + management.printManagementStatistic(); + lastGcCount = gcCount; + long clones = DatagramWriter.COPIES.get(); + long takes = DatagramWriter.TAKES.get(); + if (clones + takes > 0) { + STATISTIC_LOGGER.info("DatagramWriter {} clones, {} takes, {}%", clones, takes, + (takes * 100L) / (takes + clones)); + } + } + long now = ClockUtil.nanoRealtime(); + if ((now - lastDumpNanos - TimeUnit.MILLISECONDS.toNanos(interval)) > 0) { + lastDumpNanos = now; + server.dump(); + } + } + LOGGER.info("Executor shutdown ..."); + if (config.httpsCredentials != null) { + HttpService.stopHttpService(); + LOGGER.info("HTTPS service at port {} stopped", config.httpsPort); + } + server.stop(); + server.destroy(); + exit(); + LOGGER.info("Exit ..."); + } + + public static void exit() { + int count = Thread.activeCount(); + while (count > 0) { + int size = Thread.activeCount(); + Thread[] all = new Thread[size]; + int available = Thread.enumerate(all); + if (available < size) { + size = available; + } + count = 0; + for (int index = 0; index < size; ++index) { + Thread thread = all[index]; + if (!thread.isDaemon() && thread.isAlive()) { + ++count; + LOGGER.info("Thread [{}] {}", thread.getId(), thread.getName()); + } + } + if (count == 1) { + break; + } + try { + Thread.sleep(500); + } catch (InterruptedException e) { + break; + } + } + } + + public static void exportPsk(ServerConfig.PskStore config) { + if (config.exportFile != null || config.exportPassword64 != null) { + // load psk store + MultiPskFileStore filePskStore; + if (config.password64 != null) { + byte[] secret = StringUtil.base64ToByteArray(config.password64); + SecretKey key = SecretUtil.create(secret, "PW"); + Bytes.clear(secret); + filePskStore = new MultiPskFileStore().loadPskCredentials(config.file, key); + SecretUtil.destroy(key); + } else { + filePskStore = new MultiPskFileStore().loadPskCredentials(config.file); + } + if (config.exportFile == null) { + config.exportFile = config.file; + } + boolean replace = config.exportFile.equals(config.file); + if (config.exportPassword64 != null) { + byte[] secret = StringUtil.base64ToByteArray(config.exportPassword64); + SecretKey key = SecretUtil.create(secret, "PW"); + Bytes.clear(secret); + filePskStore.savePskCredentials(config.exportFile, key); + SecretUtil.destroy(key); + if (replace) { + LOGGER.info("PSK file {} is replaced encrypted, exit ...", config.file); + } else { + LOGGER.info("PSK file {} is exported encrypted to {}, exit ...", config.file, config.exportFile); + } + } else { + filePskStore.savePskCredentials(config.exportFile); + if (replace) { + LOGGER.info("PSK file {} is replaced unencrypted, exit ...", config.file); + } else { + LOGGER.info("PSK file {} is exported unencrypted to {}, exit ...", config.file, config.exportFile); + } + } + SecretUtil.destroy(filePskStore); + } + } + + public BaseServer(Configuration config) { + super(config); + setVersion(CALIFORNIUM_BUILD_VERSION); + setTag("CLOUD-DEMO"); + } + + public void initialize(ServerConfig cliConfig) throws SocketException { + Configuration config = getConfig(); + // executors + ScheduledExecutorService executor = ExecutorsUtil.newScheduledThreadPool(// + config.get(CoapConfig.PROTOCOL_STAGE_THREAD_COUNT), // + new NamedThreadFactory("CoapServer(main)#")); //$NON-NLS-1$ + ScheduledExecutorService secondaryExecutor = ExecutorsUtil + .newDefaultSecondaryScheduler("CoapServer(secondary)#"); + + addEndpoints(cliConfig); + + addResource(cliConfig, executor); + + setExecutors(executor, secondaryExecutor, false); + + // additional health loggers + setupUdpHealthLogger(secondaryExecutor); + setupObserveHealthLogger(); + + setupHttpService(cliConfig); + + LOGGER.info("{} initialized.", getTag()); + } + + public void addResource(ServerConfig cliConfig, ScheduledExecutorService executor) { + // add resources to the server + Devices devices = new Devices(); + add(new Diagnose(this)); + add(devices); + add(new MyContext(MyContext.RESOURCE_NAME, CALIFORNIUM_BUILD_VERSION, false)); + } + + /** + * Add endpoints. + * + * @param cliConfig client cli-config. Used e.g. for password store. + */ + public void addEndpoints(ServerConfig cliConfig) { + Configuration config = getConfig(); + int coapsPort = config.get(CoapConfig.COAP_SECURE_PORT); + boolean healthLogger = config.get(SystemConfig.HEALTH_STATUS_INTERVAL, TimeUnit.MILLISECONDS) > 0; + KeyManager[] serverCredentials = null; + Certificate[] trustedCertificates = null; + AdvancedPskStore pskStore = null; + + // load certificate credentials + try { + serverCredentials = SslContextUtil.loadKeyManager(SslContextUtil.CLASSPATH_SCHEME + KEY_STORE_LOCATION, + "server.*", KEY_STORE_PASSWORD, KEY_STORE_PASSWORD); + trustedCertificates = SslContextUtil.loadTrustedCertificates( + SslContextUtil.CLASSPATH_SCHEME + TRUST_STORE_LOCATION, null, TRUST_STORE_PASSWORD); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + // load PSK credentials + if (cliConfig.pskStore != null) { + if (cliConfig.pskStore.password64 != null) { + byte[] secret = StringUtil.base64ToByteArray(cliConfig.pskStore.password64); + SecretKey key = SecretUtil.create(secret, "PW"); + Bytes.clear(secret); + pskStore = new MultiPskFileStore().loadPskCredentials(cliConfig.pskStore.file, key); + SecretUtil.destroy(key); + } else { + pskStore = new MultiPskFileStore().loadPskCredentials(cliConfig.pskStore.file); + } + } + + // Context matcher load PSK credentials + EndpointContextMatcher customContextMatcher = null; + if (MatcherMode.PRINCIPAL == config.get(CoapConfig.RESPONSE_MATCHING)) { + customContextMatcher = new PrincipalEndpointContextMatcher(true); + } + + // + Collection localAddresses; + if (cliConfig.network.wildcard) { + localAddresses = Collections.singleton(new InetSocketAddress(0).getAddress()); + } else { + localAddresses = NetworkInterfacesUtil + .getNetworkInterfaces(cliConfig.network.selectInterfaces.getFilter(getTag())); + } + for (InetAddress addr : localAddresses) { + InetSocketAddress bindToAddress = new InetSocketAddress(addr, coapsPort); + + DtlsConnectorConfig.Builder dtlsConfigBuilder = DtlsConnectorConfig.builder(config); + dtlsConfigBuilder.setAddress(bindToAddress); + String tag = "dtls:" + StringUtil.toString(bindToAddress); + dtlsConfigBuilder.setLoggingTag(tag); + if (pskStore != null) { + dtlsConfigBuilder.setAdvancedPskStore(pskStore); + } + if (serverCredentials != null) { + X509KeyManager keyManager = SslContextUtil.getX509KeyManager(serverCredentials); + AsyncKeyManagerCertificateProvider certificateProvider = new AsyncKeyManagerCertificateProvider( + keyManager, CertificateType.RAW_PUBLIC_KEY, CertificateType.X_509); + dtlsConfigBuilder.setCertificateIdentityProvider(certificateProvider); + } + if (cliConfig.trustall || trustedCertificates != null) { + AsyncNewAdvancedCertificateVerifier.Builder verifierBuilder = AsyncNewAdvancedCertificateVerifier + .builder(); + if (cliConfig.trustall) { + verifierBuilder.setTrustAllCertificates(); + } else { + verifierBuilder.setTrustedCertificates(trustedCertificates); + } + verifierBuilder.setTrustAllRPKs(); + AsyncNewAdvancedCertificateVerifier verifier = verifierBuilder.build(); + dtlsConfigBuilder.setAdvancedCertificateVerifier(verifier); + } + dtlsConfigBuilder.setConnectionListener(new MdcConnectionListener()); + + // setup health logger + if (healthLogger) { + DtlsHealthLogger health = new DtlsHealthLogger(tag); + dtlsConfigBuilder.setHealthHandler(health); + add(health); + } + + DTLSConnector connector = new DTLSConnector(dtlsConfigBuilder.build()); + + tag = "coaps:" + StringUtil.toString(bindToAddress); + + CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); + builder.setLoggingTag(tag); + builder.setConnector(connector); + builder.setConfiguration(config); + if (customContextMatcher != null) { + builder.setEndpointContextMatcher(customContextMatcher); + } + + CoapEndpoint endpoint = builder.build(); + if (healthLogger) { + HealthStatisticLogger health = new HealthStatisticLogger(tag, true); + endpoint.addPostProcessInterceptor(health); + add(health); + } + addEndpoint(endpoint); + LOGGER.info("{}listen on {} ({})", getTag(), endpoint.getUri(), + addr.isLoopbackAddress() ? "LOCAL" : "EXTERNAL"); + } + } + + public void setupHttpService(ServerConfig config) { + if (config.httpsCredentials != null) { + HttpService httpService = HttpService.getHttpService(); + if (httpService != null) { + httpService.setExternalMeta(HtmlGenerator.createForwardingMetaElement("devices")); + httpService.setExternalSection(HtmlGenerator.createForwardSection("devices")); + httpService.setServer(this); + } + } + } + + public void setupUdpHealthLogger(ScheduledExecutorService secondaryExecutor) { + Configuration config = getConfig(); + final NetSocketHealthLogger socketLogger = new NetSocketHealthLogger("udp"); + long interval = config.get(SystemConfig.HEALTH_STATUS_INTERVAL, TimeUnit.MILLISECONDS); + if (interval > 0 && socketLogger.isEnabled()) { + long readInterval = config.get(UDP_DROPS_READ_INTERVAL, TimeUnit.MILLISECONDS); + if (interval > readInterval) { + secondaryExecutor.scheduleAtFixedRate(new Runnable() { + + @Override + public void run() { + socketLogger.read(); + } + }, readInterval, readInterval, TimeUnit.MILLISECONDS); + } + addDefaultEndpointObserver(new EndpointNetSocketObserver(socketLogger)); + } + } + + public void setupObserveHealthLogger() { + ObserveStatisticLogger obsStatLogger = new ObserveStatisticLogger(getTag()); + if (obsStatLogger.isEnabled()) { + add(obsStatLogger); + setObserveHealth(obsStatLogger); + List statistics = new ArrayList<>(); + statistics.add(obsStatLogger); + Resource child = getRoot().getChild(Diagnose.RESOURCE_NAME); + if (child instanceof Diagnose) { + ((Diagnose) child).update(statistics); + } + } + } + + public void setupPersistence(ServerConfig.Store store) { + Runnable hook = new Runnable() { + + @Override + public void run() { + stop(); + } + }; + char[] password64 = store.password64 == null ? null : store.password64.toCharArray(); + EncryptedPersistentComponentUtil serialization = new EncryptedPersistentComponentUtil(); + serialization.addProvider(this); + serialization.loadAndRegisterShutdown(store.file, password64, TimeUnit.HOURS.toSeconds(store.maxAge), hook); + } +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/DemoServer.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/DemoServer.java new file mode 100755 index 0000000000..e64f413246 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/DemoServer.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * 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. + * + ******************************************************************************/ +package org.eclipse.californium.cloud; + +import java.io.File; + +import org.eclipse.californium.cloud.BaseServer.ServerConfig; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.elements.config.Configuration; + +import picocli.CommandLine.Command; + +/** + * The cloud demo server. + * + * Read {@link Configuration} and start the {@link BaseServer}. + * + * @since 3.8 + */ +public class DemoServer extends CoapServer { + + private static final File CONFIG_FILE = new File("CaliforniumCloudDemo3.properties"); + private static final String CONFIG_HEADER = "Californium CoAP Properties file for Cloud-Demo Server"; + + @Command(name = "CloudDemoServer", version = "(c) 2022, Contributors to the Eclipse Foundation.", footer = { + "", + "Examples:", " DemoServer --no-loopback", + " (DemoServer listening only on external network interfaces.)", + "", + " DemoServer --psk-file device.psk --store-file dtls.bin --store-max-age 168 \\", + " --store-password64 ZVhiRW5pdkx1RUs2dmVoZg==", + " (DemoServer with PSK credentials from file and dtls-graceful restart.", + " Devices/sessions with no exchange for more then a week (168 hours)", + " are skipped when saving.)", + "", " DemoServer --psk-file device.psk \\", + " --psk-file-export-password64 V3plQUdkTnFLQjRnZWtSeg==", + " (DemoServer encrypts plain PSK credentials file (in place).", + " Exits afterwards.)", + "", }) + public static class Config extends ServerConfig { + + } + + public static void main(String[] args) { + Configuration configuration = Configuration.createWithFile(CONFIG_FILE, CONFIG_HEADER, BaseServer.DEFAULTS); + BaseServer.start(args, DemoServer.class.getSimpleName(), new Config(), + new BaseServer(configuration)); + } +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/EndpointNetSocketObserver.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/EndpointNetSocketObserver.java new file mode 100644 index 0000000000..67953665cf --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/EndpointNetSocketObserver.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * 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. + * + ******************************************************************************/ +package org.eclipse.californium.cloud; + +import java.net.InetSocketAddress; + +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.core.network.Endpoint; +import org.eclipse.californium.core.network.EndpointObserver; +import org.eclipse.californium.elements.Connector; +import org.eclipse.californium.elements.util.CounterStatisticManager; +import org.eclipse.californium.elements.util.SimpleCounterStatistic; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.DtlsHealth; +import org.eclipse.californium.scandium.DtlsHealthLogger; +import org.eclipse.californium.unixhealth.NetSocketHealthLogger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Register {@link Endpoint}s at a {@link NetSocketHealthLogger}. + * + * Registers the local addresses of {@link Endpoint}s at the + * {@link NetSocketHealthLogger} to read the related udp message drops. + * Registers also external {@link SimpleCounterStatistic}, if available, to + * forward the read number of dropped messages. That enables currently the + * {@link DtlsHealthLogger} to display the dropped udp messages as well. + * + * @since 3.8 + */ +public class EndpointNetSocketObserver implements EndpointObserver { + + private static final Logger LOGGER = LoggerFactory.getLogger(EndpointNetSocketObserver.class); + + /** + * Net socket statistic to register endpoints. + */ + private final NetSocketHealthLogger netSocketStatistic; + + /** + * Create net socket endpoint observer. + * + * @param netSocketStatistic net-socket statistic to register endpoints + */ + public EndpointNetSocketObserver(NetSocketHealthLogger netSocketStatistic) { + this.netSocketStatistic = netSocketStatistic; + } + + public void add(Endpoint endpoint) { + InetSocketAddress address = endpoint.getAddress(); + if (netSocketStatistic.add(address, getExternalStatistic(endpoint))) { + LOGGER.debug("added {}", address); + } else { + LOGGER.debug("enabled {}", address); + } + } + + public void remove(Endpoint endpoint) { + InetSocketAddress address = endpoint.getAddress(); + netSocketStatistic.remove(address); + LOGGER.debug("removed {}", address); + } + + @Override + public void stopped(Endpoint endpoint) { + remove(endpoint); + } + + @Override + public void started(Endpoint endpoint) { + add(endpoint); + } + + @Override + public void destroyed(Endpoint endpoint) { + remove(endpoint); + } + + /** + * Get net socket health statistic. + * + * @return net socket health statistic + */ + public NetSocketHealthLogger getNetSocketHealth() { + return netSocketStatistic; + } + + /** + * Get external statistic to register at {@link NetSocketHealthLogger} in + * order to forward the number of dropped udp messages.. + * + * @param endpoint endpoint + * @return external statistic to register + */ + protected SimpleCounterStatistic getExternalStatistic(Endpoint endpoint) { + CounterStatisticManager dtlsStatisticManager = getDtlsStatisticManager(endpoint); + return dtlsStatisticManager != null ? dtlsStatisticManager.getByKey(DtlsHealthLogger.DROPPED_UDP_MESSAGES) + : null; + } + + public static CounterStatisticManager getDtlsStatisticManager(Endpoint endpoint) { + if (endpoint instanceof CoapEndpoint) { + Connector connector = ((CoapEndpoint) endpoint).getConnector(); + if (connector instanceof DTLSConnector) { + DtlsHealth healthHandler = ((DTLSConnector) connector).getHealthHandler(); + if (healthHandler instanceof CounterStatisticManager) { + return (CounterStatisticManager) healthHandler; + } + } + } + return null; + } + +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/HtmlGenerator.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/HtmlGenerator.java new file mode 100644 index 0000000000..11717223ea --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/HtmlGenerator.java @@ -0,0 +1,206 @@ +/******************************************************************************* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * 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. + * + ******************************************************************************/ +package org.eclipse.californium.cloud; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.List; +import java.util.Set; + +import org.eclipse.californium.core.WebLink; +import org.eclipse.californium.core.coap.CoAP; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Generator for Html pages. + * + * Create forward and list pages. + * + * @since 3.8 + */ +public class HtmlGenerator { + + /** + * Logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(HtmlGenerator.class); + + /** + * Create a forward meta-element. + * + * Forwards immediately to the provided link. + * + * @param link link to forward request + * @return meta element. + */ + public static String createForwardingMetaElement(String link) { + return ""; + } + + /** + * Create section with link. + * + * @param link link to forward + * @return section with link + */ + public static String createForwardSection(String link) { + StringBuilder external = new StringBuilder(); + external.append("

External S3 endpoint:

\n"); + external.append(""); + external.append(link); + external.append(":"); + return external.toString(); + } + + /** + * Create forwarding page. + * + * @param externalMeta meta-element with forward link for automatic + * forwarding + * @param externalSection section with link for manual forwarding. + * @return forwarding page. + * @see #createForwardingMetaElement(String) + * @see #createForwardSection(String) + */ + public static String createForwardPage(String externalMeta, String externalSection) { + StringBuilder page = new StringBuilder(); + page.append("\n"); + page.append("\n"); + page.append("\n"); + page.append("\n"); + if (externalMeta != null) { + page.append(externalMeta); + } + page.append("Cloudcoap To S3 proxy\n"); + page.append("\n"); + page.append("\n"); + if (externalSection != null) { + page.append(externalSection); + } + page.append("\n"); + page.append("\n"); + return page.toString(); + } + + /** + * Create page from {@link WebLink}s. + * + * The links are prepared as relative links, if possible. + * + * @param pagePath path to this page. Required to create relative links. + * @param base base for all created links + * @param title title of page and list + * @param links set of links + * @param subLinksAttribute attribute for sub-links. If {@code null}, + * sub-links are not included. + * @param attributes attributes to include in the overview. + * @return create page with list of links. + */ + public static String createListPage(String pagePath, String base, String title, Set links, + String subLinksAttribute, String... attributes) { + if (title.isEmpty()) { + title = "List"; + } else if (Character.isLowerCase(title.charAt(0))) { + title = Character.toUpperCase(title.charAt(0)) + title.substring(1).toLowerCase(); + } + StringBuilder page = new StringBuilder(); + page.append("\n"); + page.append("\n"); + page.append("\n"); + page.append("\n"); + page.append(""); + page.append(title); + page.append("\n"); + page.append("\n"); + page.append("\n"); + page.append("

"); + page.append(title + ":"); + page.append("

\n"); + for (WebLink link : links) { + String uri = link(pagePath, link.getURI()); + String name = uri; + int index = name.lastIndexOf('/'); + if (index >= 0 && index < name.length()) { + name = name.substring(index + 1); + } + name = decode(name); + LOGGER.debug("add '{}'#'{}': '{}'", base, uri, name); + page.append(""); + page.append(name); + page.append(""); + if (subLinksAttribute != null) { + String subLink = link.getAttributes().getFirstAttributeValue(subLinksAttribute); + if (subLink != null && !subLink.isEmpty()) { + page.append(": "); + if (subLink.startsWith("/")) { + // absolute link + subLink = link(pagePath, subLink); + page.append(""); + } else { + // relative link to device resource + page.append(""); + } + subLink = decode(subLink); + page.append(subLink); + page.append(""); + } + } + for (String attribute : attributes) { + List values = link.getAttributes().getAttributeValues(attribute); + if (!values.isEmpty()) { + page.append(": ").append(values.get(0)); + } + } + page.append("
\n"); + } + page.append("\n"); + page.append("\n"); + return page.toString(); + } + + /** + * Create link. + * + * If provided link starts with pagePath, reduce the link to a relative + * link. + * + * @param pagePath path to page. + * @param link link to be included in page. + * @return resulting link, relative, if possible. + */ + public static String link(String pagePath, String link) { + if (link.startsWith(pagePath)) { + return link.substring(pagePath.length()); + } else { + return link; + } + } + + /** + * URL decode the link. + * + * @param link link to decode + * @return decoded link + */ + public static String decode(String link) { + try { + return URLDecoder.decode(link, CoAP.UTF8_CHARSET.name()); + } catch (UnsupportedEncodingException e) { + // UTF-8 must be supported, + // otherwise many functions will fail + return link; + } + } +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/HttpService.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/HttpService.java new file mode 100644 index 0000000000..9b83c61c4f --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/HttpService.java @@ -0,0 +1,457 @@ +/******************************************************************************* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * 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. + * + ******************************************************************************/ +package org.eclipse.californium.cloud; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +import org.eclipse.californium.cloud.resources.Devices; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.WebLink; +import org.eclipse.californium.core.coap.CoAP.ResponseCode; +import org.eclipse.californium.core.coap.LinkFormat; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.network.Exchange; +import org.eclipse.californium.core.network.Exchange.Origin; +import org.eclipse.californium.elements.util.Bytes; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.elements.util.StandardCharsets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsParameters; +import com.sun.net.httpserver.HttpsServer; + +/** + * Https service to access data sent via coap. + * + * Simplified http2coap proxy. Supports only very limited conversion from http + * to coap. Allows only GET access to resource "diagnose" and "devices" with + * sub-resources. + * + * No authentication supported! + * + * @since 3.8 + */ +@SuppressWarnings("restriction") +public class HttpService { + + /** + * Logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(HttpService.class); + /** + * Service instance. + */ + private static HttpService httpService; + /** + * Local secure address. + */ + private final InetSocketAddress localSecureAddress; + /** + * Ssl context for https. + */ + private final SSLContext context; + /** + * Executor for http server. + */ + private ExecutorService executor; + /** + * Https server. + */ + private HttpsServer secureServer; + /** + * Related CoAP server. + */ + private volatile CoapServer coapServer; + /** + * External meta-element, intended for automatic forwarding. + */ + private volatile String externalMeta; + /** + * External section, intended for manual forwarding. + */ + private volatile String externalSection; + + /** + * Create service. + * + * @param localSecureAddress local address for secure endpoint (https). + * @param sslContext ssl context + */ + public HttpService(InetSocketAddress localSecureAddress, SSLContext sslContext) { + this.localSecureAddress = localSecureAddress; + this.context = sslContext; + } + + /** + * Set the related CoAP server. + * + * @param coapServer CoAP server + */ + public void setServer(CoapServer coapServer) { + this.coapServer = coapServer; + } + + /** + * Set the external section. + * + * If provided, included at the top of the list page. Intended to be used + * for manual forwarding to the S3 endpoint. + * + * @param externalSection external section + */ + public void setExternalSection(String externalSection) { + this.externalSection = externalSection; + } + + /** + * Set the external meta. + * + * If provided, included in the header. Intended to be used for automatic + * forwarding to the S3 endpoint. + * + * @param externalMeta external meta + */ + public void setExternalMeta(String externalMeta) { + this.externalMeta = externalMeta; + } + + /** + * Start https service. + */ + public void start() { + executor = Executors.newCachedThreadPool(); + if (localSecureAddress != null && context != null) { + try { + secureServer = HttpsServer.create(localSecureAddress, 10); + secureServer.setHttpsConfigurator(new HttpsConfigurator(context) { + + @Override + public void configure(HttpsParameters parameters) { + parameters.setWantClientAuth(true); + } + + }); + if (coapServer != null) { + HttpHandler handler = new CoapForwardHandler(); + secureServer.createContext("/devices", handler); + secureServer.createContext("/diagnose", handler); + } + HttpHandler handler = new FavoriteIconHandler(null); + secureServer.createContext("/favicon.ico", handler); + handler = new S3ForwardHandler(); + secureServer.createContext("/", handler); + + // Thread control is given to executor service. + secureServer.setExecutor(executor); + secureServer.start(); + } catch (IOException ex) { + LOGGER.warn("starting {} failed!", localSecureAddress, ex); + } + } + } + + /** + * Stop https service. + */ + public void stop() { + if (secureServer != null) { + // stop with 2s delay + secureServer.stop(2); + } + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + } + if (executor != null) { + executor.shutdown(); + } + } + + /** + * HTTP handler to forward to S3. + */ + private class S3ForwardHandler implements HttpHandler { + + public void handle(final HttpExchange httpExchange) throws IOException { + final URI uri = httpExchange.getRequestURI(); + LOGGER.info("request: {} {}", httpExchange.getRequestMethod(), uri); + String method = httpExchange.getRequestMethod(); + byte[] payload; + int httpCode; + if (method.equals("GET")) { + String page = HtmlGenerator.createForwardPage(externalMeta, externalSection); + httpCode = 200; + payload = page.toString().getBytes(StandardCharsets.UTF_8); + } else { + httpCode = 405; + payload = "

405 - Method not allowed

".getBytes(StandardCharsets.UTF_8); + } + try { + httpExchange.getResponseHeaders().add("Content-Type", "text/html; charset=utf-8"); + httpExchange.sendResponseHeaders(httpCode, payload.length); + try (OutputStream out = httpExchange.getResponseBody()) { + out.write(payload); + } + } catch (IOException e) { + LOGGER.warn("write response to {} failed!", httpExchange.getRemoteAddress(), e); + } + } + } + + /** + * HTTP handler for favicon.ico. + */ + private class FavoriteIconHandler implements HttpHandler { + + private final byte[] data; + + public FavoriteIconHandler(String icon) { + byte[] data = Bytes.EMPTY; + if (icon != null && !icon.isEmpty()) { + InputStream inStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(icon); + if (inStream != null) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(8192); + byte[] temp = new byte[4096]; + int length; + while ((length = inStream.read(temp)) > 0) { + out.write(temp, 0, length); + } + data = out.toByteArray(); + } catch (IOException ex) { + LOGGER.info("Failure loading icon {}", icon, ex); + } finally { + try { + inStream.close(); + } catch (IOException e) { + } + } + } + } + this.data = data; + LOGGER.info("FavIcon {} bytes", data.length); + } + + public void handle(final HttpExchange httpExchange) throws IOException { + final URI uri = httpExchange.getRequestURI(); + LOGGER.info("request: {} {}", httpExchange.getRequestMethod(), uri); + String method = httpExchange.getRequestMethod(); + byte[] payload; + int httpCode; + if (method.equals("GET")) { + httpCode = 200; + payload = data; + httpExchange.getResponseHeaders().add("Content-Type", "image/x-icon"); + } else { + httpCode = 405; + payload = "

405 - Method not allowed

".getBytes(StandardCharsets.UTF_8); + httpExchange.getResponseHeaders().add("Content-Type", "text/html; charset=utf-8"); + } + try { + httpExchange.sendResponseHeaders(httpCode, payload.length); + try (OutputStream out = httpExchange.getResponseBody()) { + LOGGER.info("icon {} bytes.", payload.length); + out.write(payload); + LOGGER.debug("HTTP returned {}", httpCode); + } + } catch (IOException e) { + LOGGER.warn("write icon to {} failed!", httpExchange.getRemoteAddress(), e); + } + } + } + + /** + * HTTP handler for coap-resource. + */ + private class CoapForwardHandler implements HttpHandler { + + /** + * Simply transformation of http-get-request into coap-get-request. + */ + @Override + public void handle(final HttpExchange httpExchange) throws IOException { + final URI uri = httpExchange.getRequestURI(); + LOGGER.info("request: {} {}", httpExchange.getRequestMethod(), uri); + Request request = null; + String method = httpExchange.getRequestMethod(); + if (method.equals("GET")) { + request = Request.newGet(); + } else { + // use ping to fail ... + request = Request.newPing(); + } + String[] path = uri.getPath().split("/"); + for (String element : path) { + if (!element.isEmpty()) { + request.getOptions().addUriPath(element); + } + } + Exchange coapExchange = new Exchange(request, httpExchange.getRemoteAddress(), Origin.REMOTE, executor) { + + @Override + public void sendAccept() { + // has no meaning for HTTP: do nothing + } + + @Override + public void sendReject() { + Response response = Response.createResponse(getRequest(), ResponseCode.INTERNAL_SERVER_ERROR); + sendResponse(response); + } + + @Override + public void sendResponse(Response response) { + Request request = getRequest(); + request.setResponse(response); + byte[] payload = response.getPayload(); + int httpCode = 200; + if (response.isSuccess()) { + httpExchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8"); + } else { + switch (response.getCode()) { + case NOT_FOUND: + httpCode = 404; + payload = "

404 - Not found

".getBytes(StandardCharsets.UTF_8); + break; + case METHOD_NOT_ALLOWED: + httpCode = 405; + payload = "

405 - Method not allowed

".getBytes(StandardCharsets.UTF_8); + break; + default: + httpCode = 500; + payload = "

500 - Internal server error

".getBytes(StandardCharsets.UTF_8); + break; + } + httpExchange.getResponseHeaders().add("Content-Type", "text/html; charset=utf-8"); + } + if (response.getOptions().getContentFormat() == MediaTypeRegistry.APPLICATION_LINK_FORMAT) { + Set links = LinkFormat.parse(response.getPayloadString()); + String path = uri.getPath(); + String title = path; + int index = path.lastIndexOf('/'); + if (index >= 0) { + title = path.substring(index + 1); + path = path.substring(0, index + 1); + } + String page = HtmlGenerator.createListPage(path, "", title, links, null, Devices.ATTRIBUTE_TIME); + payload = page.getBytes(StandardCharsets.UTF_8); + httpExchange.getResponseHeaders().add("Content-Type", "text/html; charset=utf-8"); + } + try { + httpExchange.sendResponseHeaders(httpCode, payload.length); + try (OutputStream out = httpExchange.getResponseBody()) { + LOGGER.info("request"); + out.write(payload); + LOGGER.debug("HTTP returned {}", response); + } + } catch (IOException e) { + LOGGER.warn("write response to {} failed!", getPeersIdentity(), e); + } + } + }; + coapServer.getMessageDeliverer().deliverRequest(coapExchange); + } + } + + /** + * Name of private key file. + */ + private static final String HTTPS_PRIVATE_KEY = "privkey.pem"; + /** + * Name of full chain file. + */ + private static final String HTTPS_FULL_CHAIN = "fullchain.pem"; + + /** + * Create SSLContext from "lets encrypt path". + * + * Appends {@link #HTTPS_PRIVATE_KEY} and {@link #HTTPS_FULL_CHAIN} to the + * provided path to load credentials. + * + * @param credentialsPath path to lets encrypt credentials + * @return created SSLContext + * @throws IOException if an i/o error occurs + * @throws GeneralSecurityException if an encryption error occurs. + * @see #createSslContext(String, String) + */ + public static SSLContext createSslContext(String credentialsPath) throws IOException, GeneralSecurityException { + return createSslContext(credentialsPath + "/" + HTTPS_PRIVATE_KEY, credentialsPath + "/" + HTTPS_FULL_CHAIN); + } + + /** + * Create SSLContext. + * + * @param privateKeyUrl file with private key + * @param fullChainUrl file with full-chain + * @return created SSLContext + * @throws IOException if an i/o error occurs + * @throws GeneralSecurityException if an encryption error occurs. + * @see #createSslContext(String) + */ + public static SSLContext createSslContext(String privateKeyUrl, String fullChainUrl) + throws IOException, GeneralSecurityException { + PrivateKey privateKey = SslContextUtil.loadPrivateKey(privateKeyUrl, null, null, null); + X509Certificate[] certificateChain = SslContextUtil.loadCertificateChain(fullChainUrl, null, null); + KeyManager[] keyManager = SslContextUtil.createKeyManager("server", privateKey, certificateChain); + TrustManager[] trustManager = SslContextUtil.createTrustAllManager(); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManager, trustManager, null); + return sslContext; + } + + public static HttpService getHttpService() { + return httpService; + } + + public static void createHttpService(int httpsPort, String credentialsPath) { + try { + SSLContext context = HttpService.createSslContext(credentialsPath); + HttpService service = new HttpService(new InetSocketAddress(httpsPort), context); + httpService = service; + } catch (IOException e) { + e.printStackTrace(); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } + } + + public static void startHttpService() { + httpService.start(); + } + + public static void stopHttpService() { + httpService.stop(); + } +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/ManagementStatistic.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/ManagementStatistic.java new file mode 100755 index 0000000000..5eb386f24a --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/ManagementStatistic.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * 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. + * + ******************************************************************************/ +package org.eclipse.californium.cloud; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; +import java.lang.management.OperatingSystemMXBean; +import java.lang.management.ThreadMXBean; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; + +/** + * Several statistics based on {@link ManagementFactory}. + * + * @since 3.8 + */ +public class ManagementStatistic { + + /** + * Number representing {@code 1024*1024}. + */ + private static final long MEGA = 1024 * 1024L; + + /** + * LOgger to be used for logging. + */ + private final Logger logger; + + /** + * Indicates, that a GC is used, where a warning running out of heap + * indicates a shortage of memory. e.g. for ZGC that doesn't work so better + * don't print a warning for that. + */ + private final boolean warnMemoryUsage; + + /** + * Create a instance. + * + * @param logger logger to be used for the statistic + */ + public ManagementStatistic(Logger logger) { + this.logger = logger; + ThreadMXBean mxBean = ManagementFactory.getThreadMXBean(); + if (mxBean.isThreadCpuTimeSupported() && !mxBean.isThreadCpuTimeEnabled()) { + mxBean.setThreadCpuTimeEnabled(true); + } + Boolean zgc = null; + List gcNames = new ArrayList<>(); + for (GarbageCollectorMXBean gcMxBean : ManagementFactory.getGarbageCollectorMXBeans()) { + String name = gcMxBean.getName(); + if (!gcNames.contains(name)) { + gcNames.add(name); + if (zgc == null || zgc) { + zgc = name.startsWith("ZGC"); + } + } + } + // ZGC will trigger warnings, so disable warnings + warnMemoryUsage = zgc == null || !zgc; + logger.info("GC: {}", gcNames); + } + + /** + * Check, if a warning for memory usage should be used. + * + * @return {@code true}, use memory warnings, {@code false}, if not. + */ + public boolean useWarningMemoryUsage() { + return warnMemoryUsage; + } + + /** + * Get accumulated GC collection counts. + * + * @return accumulated GC collection counts + */ + public long getCollectionCount() { + long gcCount = 0; + for (GarbageCollectorMXBean gcMxBean : ManagementFactory.getGarbageCollectorMXBeans()) { + long count = gcMxBean.getCollectionCount(); + if (0 < count) { + gcCount += count; + } + logger.debug("{}: {} calls.", gcMxBean.getName(), count); + } + logger.debug("Overall {} calls.", gcCount); + return gcCount; + } + + /** + * Log management statistic. + */ + public void printManagementStatistic() { + OperatingSystemMXBean osMxBean = ManagementFactory.getOperatingSystemMXBean(); + int processors = osMxBean.getAvailableProcessors(); + logger.info("{} processors", processors); + ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean(); + if (threadMxBean.isThreadCpuTimeSupported() && threadMxBean.isThreadCpuTimeEnabled()) { + long alltime = 0; + long[] ids = threadMxBean.getAllThreadIds(); + for (long id : ids) { + long time = threadMxBean.getThreadCpuTime(id); + if (0 < time) { + alltime += time; + } + } + long pTime = alltime / processors; + logger.info("cpu-time: {} ms (per-processor: {} ms)", TimeUnit.NANOSECONDS.toMillis(alltime), + TimeUnit.NANOSECONDS.toMillis(pTime)); + } + long gcCount = 0; + long gcTime = 0; + for (GarbageCollectorMXBean gcMxBean : ManagementFactory.getGarbageCollectorMXBeans()) { + long count = gcMxBean.getCollectionCount(); + if (0 < count) { + gcCount += count; + } + long time = gcMxBean.getCollectionTime(); + if (0 < time) { + gcTime += time; + } + logger.info("{}: {} ms, {} calls.", gcMxBean.getName(), time, count); + } + logger.info("gc: {} ms, {} calls.", gcTime, gcCount); + MemoryMXBean memoryMxBean = ManagementFactory.getMemoryMXBean(); + printMemoryUsage(logger, "heap", memoryMxBean.getHeapMemoryUsage()); + printMemoryUsage(logger, "non-heap", memoryMxBean.getNonHeapMemoryUsage()); + double loadAverage = osMxBean.getSystemLoadAverage(); + if (!(loadAverage < 0.0d)) { + logger.info("average load: {}", String.format("%.2f", loadAverage)); + } + } + + /** + * Log memory usage. + * + * @param logger logger to write usage + * @param title title to be used for usage + * @param memoryUsage memory usage + */ + public static void printMemoryUsage(Logger logger, String title, MemoryUsage memoryUsage) { + long max = memoryUsage.getMax(); + if (max > 0) { + if (max > MEGA) { + logger.info("{}: {} m-bytes used of {}/{}.", title, memoryUsage.getUsed() / MEGA, + memoryUsage.getCommitted() / MEGA, max / MEGA); + } else { + logger.info("{}: {} bytes used of {}/{}.", title, memoryUsage.getUsed(), memoryUsage.getCommitted(), + max); + } + return; + } + max = memoryUsage.getCommitted(); + if (max > MEGA) { + logger.info("{}: {} m-bytes used of {}.", title, memoryUsage.getUsed() / MEGA, max / MEGA); + } else { + logger.info("{}: {} bytes used of {}.", title, memoryUsage.getUsed(), max); + } + } +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/Devices.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/Devices.java new file mode 100644 index 0000000000..d2ff5b15aa --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/Devices.java @@ -0,0 +1,253 @@ +/******************************************************************************* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * 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. + * + ******************************************************************************/ +package org.eclipse.californium.cloud.resources; + +import static org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED; +import static org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT; +import static org.eclipse.californium.core.coap.CoAP.ResponseCode.NOT_ACCEPTABLE; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_CBOR; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_JAVASCRIPT; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_JSON; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_LINK_FORMAT; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_OCTET_STREAM; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_XML; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.TEXT_PLAIN; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.UNDEFINED; + +import java.security.Principal; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.eclipse.californium.core.CoapResource; +import org.eclipse.californium.core.WebLink; +import org.eclipse.californium.core.coap.CoAP.ResponseCode; +import org.eclipse.californium.core.coap.LinkFormat; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.californium.core.server.resources.Resource; +import org.eclipse.californium.core.server.resources.ResourceAttributes; +import org.eclipse.californium.elements.util.LeastRecentlyUpdatedCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Devices resource. + * + * Keeps the content of POST request as sub-resource using the principal's name + * as name of the sub-resource. e.g.: + * + * {@code coap://host/devices POST "Hi!"} by principal "Client_idenity" results + * in a resource: + * + * "/devices/Client_idenity" with content "Hi!". + * + * Supported content types: + * + *
    + *
  • {@link MediaTypeRegistry#TEXT_PLAIN}
  • + *
  • {@link MediaTypeRegistry#APPLICATION_OCTET_STREAM}
  • + *
  • {@link MediaTypeRegistry#APPLICATION_CBOR}
  • + *
  • {@link MediaTypeRegistry#APPLICATION_JSON}
  • + *
  • {@link MediaTypeRegistry#APPLICATION_XML}
  • + *
  • {@link MediaTypeRegistry#APPLICATION_JAVASCRIPT}
  • + *
+ * + * For GET {@link MediaTypeRegistry#APPLICATION_LINK_FORMAT} is also supported. + * + * @since 3.8 + */ +public class Devices extends CoapResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(Devices.class); + + private static final SimpleDateFormat ISO_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + public static final String RESOURCE_NAME = "devices"; + public static final String SUB_RESOURCE_NAME = "series"; + + public static final String ATTRIBUTE_TIME = "time"; + + private final LeastRecentlyUpdatedCache keptPosts = new LeastRecentlyUpdatedCache<>(100, 500, 6, + TimeUnit.HOURS); + + private final int[] CONTENT_TYPES = { TEXT_PLAIN, APPLICATION_OCTET_STREAM, APPLICATION_JSON, APPLICATION_CBOR, + APPLICATION_XML, APPLICATION_JAVASCRIPT, APPLICATION_LINK_FORMAT }; + + /** + * Create devices resource. + */ + public Devices() { + super(RESOURCE_NAME); + Arrays.sort(CONTENT_TYPES); + getAttributes().setTitle("Resource, which keeps track of POSTing devices."); + getAttributes().addContentTypes(CONTENT_TYPES); + } + + @Override + public void add(Resource child) { + throw new UnsupportedOperationException("Not supported!"); + } + + @Override + public boolean delete(Resource child) { + throw new UnsupportedOperationException("Not supported!"); + } + + @Override + public Resource getChild(String name) { + return keptPosts.get(name); + } + + @Override // should be used for read-only + public Collection getChildren() { + return keptPosts.values(); + } + + @Override + public void handleGET(final CoapExchange exchange) { + Request request = exchange.advanced().getRequest(); + int accept = request.getOptions().getAccept(); + if (accept != UNDEFINED && accept != APPLICATION_LINK_FORMAT) { + exchange.respond(NOT_ACCEPTABLE); + } else { + List query = exchange.getRequestOptions().getUriQuery(); + if (query.size() > 1) { + exchange.respond(ResponseCode.BAD_OPTION, "only one search query is supported!", + MediaTypeRegistry.TEXT_PLAIN); + return; + } + Set subTree = LinkFormat.getSubTree(this, query); + Response response = new Response(CONTENT); + response.setPayload(LinkFormat.serialize(subTree)); + response.getOptions().setContentFormat(APPLICATION_LINK_FORMAT); + exchange.respond(response); + } + } + + @Override + public void handlePOST(final CoapExchange exchange) { + Request request = exchange.advanced().getRequest(); + final String principal = getPrincipalName(request); + + LOGGER.info("POST from {}", principal); + + int format = request.getOptions().getContentFormat(); + if (format != UNDEFINED && Arrays.binarySearch(CONTENT_TYPES, format) < 0) { + exchange.respond(NOT_ACCEPTABLE); + return; + } + + Response response = new Response(CHANGED); + if (principal != null) { + request.setProtectFromOffload(); + final String timestamp = ISO_DATE_FORMAT.format(new Date()); + Device device; + synchronized (keptPosts) { + Resource child = keptPosts.update(principal); + if (child instanceof Device) { + device = (Device) child; + } else { + device = new Device(principal); + } + device.setPost(request, timestamp); + if (device.getParent() == null) { + device.setParent(this); + keptPosts.put(principal, device); + } + } + } + exchange.respond(response); + } + + public static String getPrincipalName(Request request) { + Principal principal = request.getSourceContext().getPeerIdentity(); + if (principal != null) { + return principal.getName(); + } + return null; + } + + /** + * Resource representing devices + */ + public static class Device extends CoapResource { + + private volatile Request post; + + private Device(String principal) { + super(principal); + setObservable(true); + } + + private void setPost(Request post, String timestamp) { + long previousTime = 0; + synchronized (this) { + if (this.post != null) { + previousTime = this.post.getNanoTimestamp(); + } + this.post = post; + } + ResourceAttributes attributes = new ResourceAttributes(getAttributes()); + attributes.clearContentType(); + if (post.getOptions().hasContentFormat()) { + attributes.addContentType(post.getOptions().getContentFormat()); + } + if (previousTime > 0 && post.getNanoTimestamp() > 0) { + long time = TimeUnit.NANOSECONDS.toSeconds(post.getNanoTimestamp() - previousTime); + if (time > 0) { + long minutes = TimeUnit.SECONDS.toMinutes(time + 10); + if (minutes > 0) { + timestamp += " (" + minutes + "min.)"; + } else { + timestamp += " (" + time + "sec.)"; + } + } + } + attributes.setAttribute(ATTRIBUTE_TIME, timestamp); + setAttributes(attributes); + changed(); + } + + @Override + public void handleGET(CoapExchange exchange) { + Request devicePost = post; + // get request to read out details + Request request = exchange.advanced().getRequest(); + int format = devicePost.getOptions().getContentFormat(); + int accept = request.getOptions().getAccept(); + if (accept == UNDEFINED) { + accept = format == UNDEFINED ? APPLICATION_OCTET_STREAM : format; + } else if (format == UNDEFINED) { + if (accept != TEXT_PLAIN && accept != APPLICATION_OCTET_STREAM) { + exchange.respond(NOT_ACCEPTABLE); + return; + } + } else if (accept != format) { + exchange.respond(NOT_ACCEPTABLE); + return; + } + Response response = new Response(CONTENT); + response.setPayload(devicePost.getPayload()); + response.getOptions().setContentFormat(accept); + exchange.respond(response); + } + } +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/Diagnose.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/Diagnose.java new file mode 100644 index 0000000000..286f77731d --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/Diagnose.java @@ -0,0 +1,320 @@ +/******************************************************************************* + * Copyright (c) 2021 Bosch IO GmbH 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: + * Bosch IO GmbH - initial creation + ******************************************************************************/ +package org.eclipse.californium.cloud.resources; + +import static org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT; +import static org.eclipse.californium.core.coap.CoAP.ResponseCode.NOT_ACCEPTABLE; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_CBOR; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_JSON; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_LINK_FORMAT; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.TEXT_PLAIN; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.UNDEFINED; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +import org.eclipse.californium.core.CoapResource; +import org.eclipse.californium.core.WebLink; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.LinkFormat; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.coap.CoAP.ResponseCode; +import org.eclipse.californium.core.network.Endpoint; +import org.eclipse.californium.core.network.interceptors.MessageInterceptor; +import org.eclipse.californium.core.server.ServerInterface; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.californium.core.server.resources.Resource; +import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.elements.util.ClockUtil; +import org.eclipse.californium.elements.util.CounterStatisticManager; +import org.eclipse.californium.elements.util.SimpleCounterStatistic; +import org.eclipse.californium.elements.util.StringUtil; +import org.eclipse.californium.cloud.EndpointNetSocketObserver; +import org.eclipse.californium.scandium.config.DtlsConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.upokecenter.cbor.CBORObject; + +/** + * CoAP resource for request statistic. + * + * @since 3.8 + */ +public class Diagnose extends CoapResource { + + public static final String RESOURCE_NAME = "diagnose"; + + private static final Logger LOGGER = LoggerFactory.getLogger(Diagnose.class); + + private static final long START_TIME = System.currentTimeMillis(); + + private final List serverList = new ArrayList<>(); + private final ConcurrentMap> endpointsHealth; + private final List endpointHealth; + + public Diagnose(ServerInterface server) { + this(RESOURCE_NAME, "Resource for diagnose statistics", server); + } + + public Diagnose(String name, String title, ServerInterface server) { + super(name); + init(title); + if (server != null) { + this.serverList.add(server); + } + this.endpointsHealth = new ConcurrentHashMap<>(); + this.endpointHealth = null; + } + + public Diagnose(String name, String title, List endpointHealth) { + super(name); + init(title); + this.endpointsHealth = null; + this.endpointHealth = endpointHealth; + } + + private void init(String title) { + getAttributes().setTitle(title); + getAttributes().addContentType(TEXT_PLAIN); + getAttributes().addContentType(APPLICATION_JSON); + getAttributes().addContentType(APPLICATION_CBOR); + } + + public void add(ServerInterface server) { + if (server != null) { + serverList.add(server); + } + } + + public void update(List serverHealth) { + endpointsHealth.clear(); + for (Resource child : getChildren()) { + delete(child); + } + for (ServerInterface server : serverList) { + for (Endpoint ep : server.getEndpoints()) { + String scheme = ep.getUri().getScheme(); + if (CoAP.isUdpScheme(scheme)) { + addHealth(ep, serverHealth); + } + } + } + } + + public void addHealth(Endpoint endpoint, List serverHealth) { + List health = new ArrayList<>(serverHealth); + String protocol = CoAP.getProtocolForScheme(endpoint.getUri().getScheme()); + InetSocketAddress local = endpoint.getAddress(); + String key = protocol + ":" + StringUtil.toString(local); + CounterStatisticManager statistic = EndpointNetSocketObserver.getDtlsStatisticManager(endpoint); + if (statistic != null) { + health.add((CounterStatisticManager) statistic); + } + for (MessageInterceptor interceptor : endpoint.getInterceptors()) { + if (interceptor instanceof CounterStatisticManager) { + health.add((CounterStatisticManager) interceptor); + } + } + for (MessageInterceptor interceptor : endpoint.getPostProcessInterceptors()) { + if (interceptor instanceof CounterStatisticManager) { + health.add((CounterStatisticManager) interceptor); + } + } + if (!health.isEmpty()) { + this.endpointsHealth.put(local, health); + Diagnose child = new Diagnose(key, "Resource for diagnose statistic of " + key, health); + add(child); + LOGGER.debug("added {} diagnose for {}", health.size(), key); + } + } + + @Override + public void handleGET(CoapExchange exchange) { + Response response = new Response(CONTENT); + Integer maxConnections = null; + Integer nodeId = null; + List healths = endpointHealth; + Endpoint endpoint = exchange.advanced().getEndpoint(); + if (endpoint != null) { + if (CoAP.COAP_SECURE_URI_SCHEME.equalsIgnoreCase(endpoint.getUri().getScheme())) { + Configuration config = endpoint.getConfig(); + maxConnections = config.get(DtlsConfig.DTLS_MAX_CONNECTIONS); + nodeId = config.get(DtlsConfig.DTLS_CONNECTION_ID_NODE_ID); + } + if (endpointsHealth != null) { + healths = endpointsHealth.get(endpoint.getAddress()); + } + } else if (healths == null) { + List query = exchange.getRequestOptions().getUriQuery(); + if (query.size() > 1) { + exchange.respond(ResponseCode.BAD_OPTION, "only one search query is supported!", + MediaTypeRegistry.TEXT_PLAIN); + return; + } + Set subTree = LinkFormat.getSubTree(this, query); + response.setPayload(LinkFormat.serialize(subTree)); + response.getOptions().setContentFormat(APPLICATION_LINK_FORMAT); + exchange.respond(response); + return; + } + + switch (exchange.getRequestOptions().getAccept()) { + case UNDEFINED: + case TEXT_PLAIN: + response.getOptions().setContentFormat(TEXT_PLAIN); + response.setPayload(toText(maxConnections, nodeId, healths)); + break; + + case APPLICATION_JSON: + response.getOptions().setContentFormat(APPLICATION_JSON); + response.setPayload(toJson(maxConnections, nodeId, healths)); + break; + + case APPLICATION_CBOR: + response.getOptions().setContentFormat(APPLICATION_CBOR); + response.setPayload(toCbor(maxConnections, nodeId, healths)); + break; + + default: + response = new Response(NOT_ACCEPTABLE); + break; + } + + exchange.respond(response); + } + + public String toText(Integer maxConnections, Integer nodeId, List healths) { + String eol = System.lineSeparator(); + StringBuilder builder = new StringBuilder(); + builder.append("systemstart:").append(START_TIME).append(eol); + if (nodeId != null) { + builder.append("node-id:").append(nodeId).append(eol); + } + if (maxConnections != null) { + builder.append("max. connnections:").append(maxConnections).append(eol); + } + if (healths != null && !healths.isEmpty()) { + CounterStatisticManager first = healths.get(0); + long lastTransfer = ClockUtil.nanoRealtime() - first.getLastTransferTime(); + builder.append("since: ").append(TimeUnit.NANOSECONDS.toSeconds(lastTransfer)).append("s").append(eol); + int counter = 0; + for (CounterStatisticManager manager : healths) { + String tag = manager.getTag(); + if (tag != null && !tag.isEmpty()) { + builder.append(tag).append(eol); + } else { + builder.append(++counter).append(eol); + } + String head = " "; + for (String key : manager.getKeys()) { + SimpleCounterStatistic statistic = manager.getByKey(key); + if (statistic != null) { + long[] pair = statistic.getCountersPair(); + builder.append(head).append(key).append(",").append(pair[0]).append(",").append(pair[1]) + .append(eol); + } + } + } + } + return builder.toString(); + } + + public String toJson(Integer maxConnections, Integer nodeId, List healths) { + JsonObject element = new JsonObject(); + element.addProperty("systemstart", START_TIME); + if (nodeId != null) { + element.addProperty("node-id", nodeId); + } + if (maxConnections != null) { + element.addProperty("max-connections", maxConnections); + } + if (healths != null && !healths.isEmpty()) { + CounterStatisticManager first = healths.get(0); + long lastTransfer = ClockUtil.nanoRealtime() - first.getLastTransferTime(); + element.addProperty("since", TimeUnit.NANOSECONDS.toSeconds(lastTransfer) + "s"); + int counter = 0; + for (CounterStatisticManager manager : healths) { + JsonObject group = new JsonObject(); + for (String key : manager.getKeys()) { + SimpleCounterStatistic statistic = manager.getByKey(key); + if (statistic != null) { + long[] pair = statistic.getCountersPair(); + JsonObject info = new JsonObject(); + info.addProperty("cur", pair[0]); + info.addProperty("all", pair[1]); + group.add(key, info); + } + } + String tag = manager.getTag(); + if (tag != null && !tag.isEmpty()) { + element.add(tag, group); + } else { + element.add(Integer.toString(++counter), group); + } + } + } + GsonBuilder builder = new GsonBuilder(); + Gson gson = builder.create(); + return gson.toJson(element); + } + + public byte[] toCbor(Integer maxConnections, Integer nodeId, List healths) { + CBORObject map = CBORObject.NewOrderedMap(); + map.set("systemstart", CBORObject.FromObject(START_TIME)); + if (nodeId != null) { + map.set("node-id", CBORObject.FromObject(nodeId)); + } + if (maxConnections != null) { + map.set("max-connections", CBORObject.FromObject(maxConnections)); + } + if (healths != null && !healths.isEmpty()) { + CounterStatisticManager first = healths.get(0); + long lastTransfer = ClockUtil.nanoRealtime() - first.getLastTransferTime(); + map.set("since", CBORObject.FromObject(TimeUnit.NANOSECONDS.toSeconds(lastTransfer) + "s")); + int counter = 0; + for (CounterStatisticManager manager : healths) { + CBORObject group = CBORObject.NewOrderedMap(); + for (String key : manager.getKeys()) { + SimpleCounterStatistic statistic = manager.getByKey(key); + if (statistic != null) { + long[] pair = statistic.getCountersPair(); + CBORObject info = CBORObject.NewOrderedMap(); + info.set("cur", CBORObject.FromObject(pair[0])); + info.set("all", CBORObject.FromObject(pair[1])); + group.set(key, info); + } + } + String tag = manager.getTag(); + if (tag != null && !tag.isEmpty()) { + map.set(tag, group); + } else { + map.set(Integer.toString(++counter), group); + } + } + } + return map.EncodeToBytes(); + } +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/MyContext.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/MyContext.java new file mode 100644 index 0000000000..b460ef6dd0 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/MyContext.java @@ -0,0 +1,293 @@ +/******************************************************************************* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * 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. + * + ******************************************************************************/ +package org.eclipse.californium.cloud.resources; + +import static org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT; +import static org.eclipse.californium.core.coap.CoAP.ResponseCode.NOT_ACCEPTABLE; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_CBOR; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_JSON; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_XML; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.TEXT_PLAIN; +import static org.eclipse.californium.core.coap.MediaTypeRegistry.UNDEFINED; + +import java.net.InetSocketAddress; +import java.security.Principal; + +import org.eclipse.californium.core.CoapResource; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.network.Endpoint; +import org.eclipse.californium.core.network.Exchange; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.californium.elements.DtlsEndpointContext; +import org.eclipse.californium.elements.EndpointContext; +import org.eclipse.californium.elements.TlsEndpointContext; +import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.elements.util.StandardCharsets; +import org.eclipse.californium.elements.util.StringUtil; +import org.eclipse.californium.scandium.config.DtlsConfig; + +import com.upokecenter.cbor.CBORObject; + +/** + * Context resource. + * + * @since 3.0 (renamed, was Context) + */ +public class MyContext extends CoapResource { + + public static final String RESOURCE_NAME = "mycontext"; + + private final String version; + + public MyContext(String name, String version, boolean visible) { + super(name, visible); + getAttributes().setTitle("Communication Context"); + getAttributes().addContentType(TEXT_PLAIN); + getAttributes().addContentType(APPLICATION_CBOR); + getAttributes().addContentType(APPLICATION_JSON); + getAttributes().addContentType(APPLICATION_XML); + this.version = version; + } + + @Override + public void handleGET(CoapExchange exchange) { + + // get request to read out details + Request request = exchange.advanced().getRequest(); + + int accept = request.getOptions().getAccept(); + if (accept == UNDEFINED) { + accept = TEXT_PLAIN; + } + + Response response = new Response(CONTENT); + response.getOptions().setContentFormat(accept); + + byte[] payload = null; + switch (accept) { + case TEXT_PLAIN: + payload = handleGet(exchange, new Text()); + break; + case APPLICATION_CBOR: + payload = handleGet(exchange, new Cbor()); + break; + case APPLICATION_JSON: + payload = handleGet(exchange, new Json()); + break; + case APPLICATION_XML: + payload = handleGet(exchange, new Xml("context")); + break; + default: + String ct = MediaTypeRegistry.toString(accept); + exchange.respond(NOT_ACCEPTABLE, "Type \"" + ct + "\" is not supported for this resource!", TEXT_PLAIN); + return; + } + response.setPayload(payload); + exchange.respond(response); + } + + private byte[] handleGet(CoapExchange coapExchange, Formatter formatter) { + Exchange exchange = coapExchange.advanced(); + EndpointContext context = exchange.getRequest().getSourceContext(); + InetSocketAddress source = context.getPeerAddress(); + + formatter.add("ip", StringUtil.toString(source.getAddress())); + formatter.add("port", new Long(source.getPort())); + Endpoint endpoint = exchange.getEndpoint(); + if (endpoint != null) { + Configuration config = endpoint.getConfig(); + Integer nodeId = config.get(DtlsConfig.DTLS_CONNECTION_ID_NODE_ID); + if (nodeId != null) { + formatter.add("node-id", nodeId.toString()); + } + } + Principal peerIdentity = context.getPeerIdentity(); + if (peerIdentity != null) { + formatter.add("peer", peerIdentity.getName()); + } + String cipherSuite = context.getString(DtlsEndpointContext.KEY_CIPHER); + if (cipherSuite == null) { + cipherSuite = context.getString(TlsEndpointContext.KEY_CIPHER); + } + if (cipherSuite != null) { + formatter.add("cipher-suite", cipherSuite); + } + String sessionId = context.getString(DtlsEndpointContext.KEY_SESSION_ID); + if (sessionId == null) { + sessionId = context.getString(TlsEndpointContext.KEY_SESSION_ID); + } + if (sessionId != null) { + formatter.add("session-id", sessionId); + } + String cid = context.getString(DtlsEndpointContext.KEY_READ_CONNECTION_ID); + if (cid != null) { + formatter.add("read-cid", cid); + } + cid = context.getString(DtlsEndpointContext.KEY_WRITE_CONNECTION_ID); + if (cid != null) { + formatter.add("write-cid", cid); + } + String via = context.getString(DtlsEndpointContext.KEY_VIA_ROUTER); + if (via != null) { + formatter.add("via", via); + } + Boolean extendedMasterSecret = context.get(DtlsEndpointContext.KEY_EXTENDED_MASTER_SECRET); + if (extendedMasterSecret != null) { + formatter.add("ext-master-secret", extendedMasterSecret); + } + Boolean newest = context.get(DtlsEndpointContext.KEY_NEWEST_RECORD); + if (newest != null) { + formatter.add("newest-record", newest); + } + Integer limit = context.get(DtlsEndpointContext.KEY_MESSAGE_SIZE_LIMIT); + if (limit != null) { + formatter.add("message-size-limit", new Long(limit)); + } + InetSocketAddress previous = context.get(DtlsEndpointContext.KEY_PREVIOUS_ADDRESS); + if (previous != null) { + formatter.add("prev-ip", StringUtil.toString(previous.getAddress())); + formatter.add("prev-port", new Long(previous.getPort())); + } + if (version != null) { + formatter.add("server", "Cf " + version); + } + return formatter.getPayload(); + } + + private static interface Formatter { + void add(String name, String value); + + void add(String name, Long value); + + void add(String name, Boolean value); + + byte[] getPayload(); + } + + private static class Text implements Formatter { + private StringBuilder payload = new StringBuilder(); + + @Override + public void add(String name, String value) { + payload.append(name).append(": ").append(value).append("\n"); + } + + @Override + public void add(String name, Long value) { + payload.append(name).append(": ").append(value).append("\n"); + } + + @Override + public void add(String name, Boolean value) { + payload.append(name).append(": ").append(value).append("\n"); + } + + @Override + public byte[] getPayload() { + if (payload.length() > 0) { + payload.setLength(payload.length() - 1); + } + return payload.toString().getBytes(StandardCharsets.UTF_8); + } + + } + + private static class Cbor implements Formatter { + CBORObject map = CBORObject.NewMap(); + + @Override + public void add(String name, String value) { + map.set(name, CBORObject.FromObject(value)); + } + + @Override + public void add(String name, Long value) { + map.set(name, CBORObject.FromObject(value)); + } + + @Override + public void add(String name, Boolean value) { + map.set(name, CBORObject.FromObject(value)); + } + + @Override + public byte[] getPayload() { + return map.EncodeToBytes(); + } + + } + + private static class Json implements Formatter { + private StringBuilder payload = new StringBuilder("{\n"); + + @Override + public void add(String name, String value) { + payload.append(" \"").append(name).append("\": \"").append(value).append("\",\n"); + } + + @Override + public void add(String name, Long value) { + payload.append(" \"").append(name).append("\": ").append(value).append(",\n"); + } + + @Override + public void add(String name, Boolean value) { + payload.append(" \"").append(name).append("\": ").append(value).append(",\n"); + } + + @Override + public byte[] getPayload() { + if (payload.length() > 0) { + // remove last ",\n" + payload.setLength(payload.length() - 2); + } + payload.append("\n}"); + return payload.toString().getBytes(StandardCharsets.UTF_8); + } + + } + + private static class Xml implements Formatter { + private StringBuilder payload = new StringBuilder(); + + Xml(String element) { + payload.append("<").append(element).append(" "); + } + + @Override + public void add(String name, String value) { + payload.append(name).append("=\"").append(value).append("\" "); + } + + @Override + public void add(String name, Long value) { + payload.append(name).append("=\"").append(value).append("\" "); + } + + @Override + public void add(String name, Boolean value) { + payload.append(name).append("=\"").append(value).append("\" "); + } + + @Override + public byte[] getPayload() { + payload.append("/>"); + return payload.toString().getBytes(StandardCharsets.UTF_8); + } + + } + +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/resources/logback.xml b/demo-apps/cf-cloud-demo-server/src/main/resources/logback.xml new file mode 100644 index 0000000000..0e4b0ce46f --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/resources/logback.xml @@ -0,0 +1,114 @@ + + + + + + %d{HH:mm:ss.SSS} %level [%logger{0}]: %msg%n + + + + + logs/anonymorigin.log + + + logs/anonymorigin-%d{yyyy-MM}.%i.log + + 5MB + 200 + 500MB + + + + + [%date{yyyy-MM-dd HH:mm:ss}]\t%msg%n + + + + + logs/error.log + + WARN + + + + logs/error-%d{yyyy-MM}.%i.log + + 5MB + 200 + 500MB + + + + + [%date{yyyy-MM-dd HH:mm:ss}]\t%msg%n + + + + + logs/ban.log + true + + + logs/ban-%d{yyyy-MM}.%i.log + + 1MB + 10 + 10MB + + + + + [%date{yyyy-MM-dd HH:mm:ss}]\t%msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo-apps/pom.xml b/demo-apps/pom.xml index 726dbddcce..97a5768c33 100644 --- a/demo-apps/pom.xml +++ b/demo-apps/pom.xml @@ -28,6 +28,7 @@ cf-plugtest-checker cf-plugtest-client cf-plugtest-server + cf-cloud-demo-server cf-proxy2 cf-secure cf-unix-setup