From 8c4cde053ba96af43d70514094b8d6f06b6a3cf7 Mon Sep 17 00:00:00 2001 From: Achim Kraus Date: Thu, 4 Aug 2022 12:07:08 +0200 Subject: [PATCH] Add cloud demo server. Signed-off-by: Achim Kraus --- demo-apps/cf-cloud-demo-server/README.md | 408 +++++++++ .../docs/browser-device.png | Bin 0 -> 18791 bytes .../docs/browser-diagnose-list.png | Bin 0 -> 39496 bytes .../docs/browser-diagnose-page.png | Bin 0 -> 87686 bytes .../docs/browser-list.png | Bin 0 -> 26813 bytes .../cf-cloud-demo-server/docs/cloudcoap.svg | 47 + demo-apps/cf-cloud-demo-server/pom.xml | 92 ++ .../cf-cloud-demo-server/service/cali.service | 66 ++ .../cloud-installs/cloud-config-dev.yaml | 47 + .../service/cloud-installs/deploy-dev.sh | 326 +++++++ .../service/cloud-installs/provider-aws.sh | 124 +++ .../service/cloud-installs/provider-do.sh | 118 +++ .../service/cloud-installs/provider-exo.sh | 141 +++ .../service/demo-devices.txt | 38 + .../service/fail2ban/cali2fail.conf | 50 ++ .../service/fail2ban/calidtls.conf | 30 + .../service/fail2ban/calihttps.conf | 30 + .../service/fail2ban/calilogin.conf | 30 + .../service/iptables-firewall.sh | 59 ++ .../service/iptables.service | 37 + .../service/letsencrypt.sh | 76 ++ .../service/permissions.sh | 31 + .../eclipse/californium/cloud/BaseServer.java | 807 +++++++++++++++++ .../eclipse/californium/cloud/DemoServer.java | 64 ++ .../cloud/EndpointNetSocketObserver.java | 148 ++++ .../cloud/ManagementStatistic.java | 177 ++++ .../californium/cloud/http/HtmlGenerator.java | 251 ++++++ .../californium/cloud/http/HttpService.java | 832 ++++++++++++++++++ .../cloud/option/ReadEtagOption.java | 50 ++ .../cloud/option/ReadResponseOption.java | 76 ++ .../californium/cloud/option/TimeOption.java | 179 ++++ .../californium/cloud/resources/Devices.java | 525 +++++++++++ .../californium/cloud/resources/Diagnose.java | 332 +++++++ .../cloud/resources/MyContext.java | 173 ++++ .../cloud/util/CredentialsStore.java | 561 ++++++++++++ .../cloud/util/DeviceGredentialsProvider.java | 68 ++ .../californium/cloud/util/DeviceManager.java | 383 ++++++++ .../californium/cloud/util/DeviceParser.java | 801 +++++++++++++++++ .../californium/cloud/util/Formatter.java | 292 ++++++ .../cloud/util/LinuxConfigParser.java | 500 +++++++++++ .../cloud/util/ResourceParser.java | 52 ++ .../californium/cloud/util/ResourceStore.java | 500 +++++++++++ .../src/main/resources/logback.xml | 120 +++ demo-apps/pom.xml | 1 + 44 files changed, 8642 insertions(+) create mode 100644 demo-apps/cf-cloud-demo-server/README.md create mode 100644 demo-apps/cf-cloud-demo-server/docs/browser-device.png create mode 100644 demo-apps/cf-cloud-demo-server/docs/browser-diagnose-list.png create mode 100644 demo-apps/cf-cloud-demo-server/docs/browser-diagnose-page.png create mode 100644 demo-apps/cf-cloud-demo-server/docs/browser-list.png create mode 100644 demo-apps/cf-cloud-demo-server/docs/cloudcoap.svg create mode 100755 demo-apps/cf-cloud-demo-server/pom.xml create mode 100644 demo-apps/cf-cloud-demo-server/service/cali.service create mode 100644 demo-apps/cf-cloud-demo-server/service/cloud-installs/cloud-config-dev.yaml create mode 100755 demo-apps/cf-cloud-demo-server/service/cloud-installs/deploy-dev.sh create mode 100755 demo-apps/cf-cloud-demo-server/service/cloud-installs/provider-aws.sh create mode 100755 demo-apps/cf-cloud-demo-server/service/cloud-installs/provider-do.sh create mode 100755 demo-apps/cf-cloud-demo-server/service/cloud-installs/provider-exo.sh create mode 100644 demo-apps/cf-cloud-demo-server/service/demo-devices.txt create mode 100644 demo-apps/cf-cloud-demo-server/service/fail2ban/cali2fail.conf create mode 100644 demo-apps/cf-cloud-demo-server/service/fail2ban/calidtls.conf create mode 100644 demo-apps/cf-cloud-demo-server/service/fail2ban/calihttps.conf create mode 100644 demo-apps/cf-cloud-demo-server/service/fail2ban/calilogin.conf create mode 100755 demo-apps/cf-cloud-demo-server/service/iptables-firewall.sh create mode 100644 demo-apps/cf-cloud-demo-server/service/iptables.service create mode 100755 demo-apps/cf-cloud-demo-server/service/letsencrypt.sh create mode 100755 demo-apps/cf-cloud-demo-server/service/permissions.sh create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/BaseServer.java create mode 100755 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/DemoServer.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/EndpointNetSocketObserver.java create mode 100755 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/ManagementStatistic.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/http/HtmlGenerator.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/http/HttpService.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/ReadEtagOption.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/ReadResponseOption.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/TimeOption.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/Devices.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/Diagnose.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/MyContext.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/util/CredentialsStore.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/util/DeviceGredentialsProvider.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/util/DeviceManager.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/util/DeviceParser.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/util/Formatter.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/util/LinuxConfigParser.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/util/ResourceParser.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/util/ResourceStore.java create mode 100644 demo-apps/cf-cloud-demo-server/src/main/resources/logback.xml 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..56ac5a8267 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/README.md @@ -0,0 +1,408 @@ +![Californium logo](../../cf_64.png) + +# Californium (Cf) - Cloud Demo Server + +## - Reliable - Efficient - Encrypted - + +[](./docs/cloudcoap.svg) + +Simple cloud demo server for device communication with CoAP/DTLS 1.2 CID . Based on the [Eclipse/Californium CoAP/DTLS 1.2 CID java library](https://github.com/eclipse-californium/californium). + + Kickstart your UDP experience + +The server supports DTLS 1.2 CID with **P**re**S**hared**K**ey (similar to username/password) and **R**aw**P**ublic**Key** (a public key without additional information like subject or validity as certificate) authentication for device communication. X509 certificate based device authentication may be added to the cloud deom server in the future. Californium (Cf) itself does already support x509. + +Supports resource "devices": + +```sh +coap-client -v 6 -m POST -e "test 1234" -u Client_identity -k secretPSK coaps://:5684/devices +v:1 t:CON c:POST i:307a {01} [ Uri-Path:devices ] :: 'test 1234' +INFO Identity Hint '' provided +v:1 t:ACK c:2.04 i:307a {01} [ ] +INFO * :33681 <-> :5684 DTLS: SSL3 alert write:warning:close notify +``` + +```sh +coap-client -v 6 -m GET -u Client_identity -k secretPSK coaps://:5684/devices/Client_identity +v:1 t:CON c:GET i:7fcf {01} [ Uri-Path:devices, Uri-Path:Client_identity ] +INFO Identity Hint '' provided +v:1 t:ACK c:2.05 i:7fcf {01} [ Content-Format:application/octet-stream ] :: binary data length 9 +<<746573742031323334>> +<> +INFO * :60667 <-> :5684 DTLS: SSL3 alert write:warning:close notify +``` + +A device may also read the data of other devices. + +The server comes with an optional very simple HTTPS server to GET the last CoAP POSTs to resource "devices". + +![browser-list](./docs/browser-list.png) + +![browser-device](./docs/browser-device.png) + +It includes also an optional `diagnose` resource. That records the number of messages and failures. + +![browser-diagnose-list](./docs/browser-diagnose-list.png) + +![browser-diagnose-page](./docs/browser-diagnose-page.png) + +## General Usage + +**Note:** the Cloud Demo Server is not released! It requires a [Build using Maven](../../README.md#build-using-maven) before usage. + +Start the cf-cloud-demo-server-3.12.0.jar with: + +```sh +Usage: CloudDemoServer [-h] [--diagnose] [--wildcard-interface | [[--[no-] + loopback] [--[no-]external] [--[no-]ipv4] [--[no-]ipv6] + [--interfaces-pattern=[, + ...]]...]] [--https-port= + --https-credentials= + [--https-password64=]] + [--coaps-credentials= + [--coaps-password64=]] [--device-file= + [--device-file-password64=]] + [--store-file= --store-max-age= + [--store-password64=]] + --coaps-credentials= + Folder containing coaps credentials in 'privkey. + pem' and 'pubkey.pem' + --coaps-password64= + Password for device store. Base 64 encoded. + --device-file= Filename of device store for coap. + --device-file-password64= + Password for device store. Base 64 encoded. + --diagnose enable 'diagnose'-resource. + -h, --help display a help message + --https-credentials= + Folder containing https credentials in 'privkey. + pem' and 'fullchain.pem'. + --https-password64= + Folder containing https credentials in 'privkey. + pem' and 'fullchain.pem'. + --https-port= Port of https service. + --interfaces-pattern=[,...] + interface regex patterns for coap endpoints. + --[no-]external enable coap endpoints on external network. + --[no-]ipv4 enable coap endpoints for ipv4. + --[no-]ipv6 enable coap endpoints for ipv6. + --[no-]loopback enable coap endpoints on loopback network. + --store-file= file-store for dtls state. + --store-max-age= + maximum age of connections in hours to store dtls + state. + --store-password64= + password to store dtls state. Base 64 encoded. + --wildcard-interface Use local wildcard-address for coap endpoints. + +Examples: + DemoServer --no-loopback + (DemoServer listening only on external network interfaces.) + + DemoServer --store-file dtls.bin --store-max-age 168 \ + --store-password64 ZVhiRW5pdkx1RUs2dmVoZg== \ + --device-file devices.txt + + (DemoServer with device credentials from file and dtls-graceful restart. + Devices/sessions with no exchange for more then a week (168 hours) + are skipped when saving.) +``` + +to see the set of options and arguments. + +When the server is started the first time, it creates the "CaliforniumCloudDemo3.properties" file. this contains the settings, which may be adjusted by editing this file. + +``` +# Californium CoAP Properties file for S3 Proxy Server +# Fri Apr 05 18:16:24 CEST 2024 +# +# Cache maximum devices. +# Default: 5000 +CACHE_MAX_DEVICES=5000 +# Threshold for stale devices. Devices will only get removed for new +# ones, if at least for that threshold no messages are exchanged with +# that device. +# Default: 1[d] +CACHE_STALE_DEVICE_THRESHOLD=1[d] +# Reload device credentials interval. 0 to load credentials only on +# startup. +# Default: 1[min] +DEVICE_CREDENTIALS_RELOAD_INTERVAL=30[s] +# Reload HTTPS credentials interval. 0 to load credentials only on startup. +# Default: 30[min] +HTTPS_CREDENTIALS_RELOAD_INTERVAL=30[min] +... +``` + +**Note:** to use encrypted device files, please use the [cf-encrypt](../../cf-utils/cf-encrypt/README.md) utility. Same applies for the https and coaps credentials, encrypted PKCS #8 is not supported, you must use PKCS #8 without encryption and apply that `cf-encrypt` utility. + +## Device Credentials + +In order to authenticate the devices, PSK (Pre-Shared-Key, [RFC4279](https://www.rfc-editor.org/rfc/rfc4279)), or RPK (Raw-Public-Key, [RFC7250](https://www.rfc-editor.org/rfc/rfc7250)) are supported. The device credentials are stored in a [text-file, e.g. demo-devices.txt](./service/demo-devices.txt) in the local file system of the cloud VM. + +``` +# Device store for Cloud Demo + +Demo1=Thing +# default openssl PSK credentials +.psk='Client_identity',c2VjcmV0UFNL +# Californium demo-client RPK certificate +.rpk=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQxYO5/M5ie6+3QPOaAy5MD6CkFILZwIb2rOBCX/EWPaocX1H+eynUnaEEbmqxeN6rnI/pH19j4PtsegfHLrzzQ== + +Demo2=Thing +.psk='cali.0012345','secretPSK' + +``` + +The format starts with a device definition, using the device `name`, followed by a '=' and the `group` the device belongs to. Each device belongs to one group and you may use a couple of groups to partition your devices. Currently the groups are only used by the [Californium (Cf) - Cloud CoAP-S3-Proxy Server](../cf-s3-proxy-server) to select the devices shown initial after login. + +PSK credentials are provided with `[].psk=,`. If the `name` is skipped, the name of the last device definition is used. If the `name` is provided, then it must match the name of the last device definition. The `psk-identity` is provided in UTF-8 and the `psk-secret` in base64. The `psk-identity` must be unique for each device of the system. If hexadecimal should be used, then provide it with preceding ":0x". + +``` +# default openssl PSK credentials +.psk='Client_identity',:0x73656372657450534B +``` +RPK credentials are provided similar with `[].rpk=`. The `public-key` is in base64 or with ":0x" in hexadecimal. That `public-key` must also be unique for each device, which will be natural, if a fresh key-pair is generated for each device. RPK requires the server also to load it's private and public key with `--coaps-credentials `. The directory must contain a `privkey.pem`, and, if that doesn't contain the public key as well, a `publickey.pem`. + +You may either provide that server key pair on your own or create one with: + +```sh +openssl ecparam -genkey -name prime256v1 -noout -out privkey.pem +``` + +(The PEM created by that command contains both, the private and the public key. Therefore you need only this one. Other formats or tools may have other results and you may need then two files, one `privkey.pem` and one `publickey.pem`.) + +**Note:** the device credentials file is read using UTF-8 encoding, '=' are not supported for device names. Lines starting with '#' are skipped as comment, therefore a device name must not start with a '#'. Empty lines are also skipped. + +The cloud demo server checks frequently, if the file has changed and automatically reloads the device credentials. + +Devices already connected with removed credentials are kept connected but on the next handshake these device will fail to communicate with the cloud demo server. + +## 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 then uses piggybacked responses. Neither DTLS handshakes, separate responses, observe/notifies, nor blockwise transfers are supported to span a restart. + +## HTTPS forwarding + +The server runs as user "cali" and therefore requires 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. + +## HTTPS x509 certificate + +The https server requires x509 certificate credentials. You may setup a own CA, use a public one, or the CA provided by your cloud-provider. + +Regardless of a private, public or cloud provider's CA, the result should be a private key and a related and signed x509 certificate chain. Save both in a directory using the names `privkey.pem` and `fullchain.pem` and pass that directory using `--https-credentials `. Ensure, the user "cali" or group "cali" has read access to it. The server first reads the `fullchain.pem`. If that also contains the private key, `privkey.pem` is not used. + +Please consult the CA's documentation how to create these credentials. + +### HTTPS x509 certificate - Let's encrypt + +One public CA is [Let's Encrypt](https://letsencrypt.org/). + +If you want to use it, install [certbot](https://certbot.eff.org/instructions?ws=other&os=ubuntufocal) or a similar tool and request a x509 http server certificate for your DNS-domain. Raw ip-addresses are not supported by let's encrypt. Prior to requesting a certificate, you need to ensure, that you possess/control the DNS-domain and that this DNS-domain points to the right ip-address of the cloud-vm. Consult the documentation of your domain provider how to achieve that. + +```sh +certbot certonly --standalone --key-type ecdsa --elliptic-curve secp256r1 -d +``` + +You will be asked about an e-mail address and to accept the [Let's Encrypt Service Agreement](https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf). + +The received credentials are stored in `/etc/letsencrypt/archive//` and stable links to read them are available in `/etc/letsencrypt/live/`. Pass that `--https-credentials=/etc/letsencrypt/live/` to the cloud demo server. + +Let's encrypt assumes by default the root user reads the credentials. The cloud demo server uses the limited user "cali". In order to read the credentials by that limited user "cali", the access rights on the file-system must be adjusted. + +Adjust permission to grant others read-access to the directories "live" and "archive": + +```sh +chmod go+rx /etc/letsencrypt/live +chmod go+rx /etc/letsencrypt/archive +``` + +And in order to read the private key, you need to add adjust the permission of the file as well. Therefore set group to "cali" and adjust permission to grant the group read-access: + +```sh +chown root:cali /etc/letsencrypt/archive//privkey1.pem +chmod g+r /etc/letsencrypt/archive//privkey1.pem +``` + +(**Note:** in "archive" the files are numbered, in "live" are links to the most recent file are without numbers. See [letsencrypt.sh](./service/letsencrypt.sh).) + +These permissions are kept, if the credentials are renewed. `Certbot` does this renewal automatically. The https server also checks for new certificates and restarts automatically, when new certificates are detected. + +For more details about `certbot, see instructions at [certbot](https://eff-certbot.readthedocs.io/en/stable/using.html#where-are-my-certificates). + +## fail2ban + +To ban some host from continue sending malicious messages, Californium support to write the source ip-addresses of malicious messages into a special log file. +The cloud demo server uses logback for logging, and configures therefore a file-appender and configures the logger "org.eclipse.californium.ban" to use that. + +``` + + logs/ban.log + + + logs/ban-%d{yyyy-MM}.%i.log + + 1MB + 10 + 10MB + + + + + [%date{yyyy-MM-dd HH:mm:ss}]\t%msg%n + + + + + + + +``` + +The content of that file is: + +``` +[2023-05-12 07:10:11] coaps Option Content-Format value of 3 bytes must be in range of [0-2] bytes. 5203ABDA15E9B763 DTLS Ban:??.??.??.?? +[2023-05-12 07:57:50] https: GET /actuator/gateway/routes HTTP/1.1 HTTPS Ban: ??.??.??.?? +[2023-05-12 08:26:38] https: GET /robots.txt HTTP/1.1 HTTPS Ban: ??.??.??.?? +``` + +### fail2ban - installation + +``` +sudo apt install fail2ban +``` + +To configure fail2ban, define filters, e.g.: + +``` +[INCLUDES] + +before = common.conf + +[Definition] + +failregex = DTLS\s+Ban:\s+$ +``` + +and copy [calidtls.conf](./service/fail2ban/calidtls.conf) into folder "/etc/fail2ban/filter.d". That selects the `` after the tags "DTLS" and "Ban:". + +Additional [calihttps.conf](./service/fail2ban/calihttps.conf) filters for "HTTPS" and "Ban:", and [calilogin.conf](./service/fail2ban/calilogin.conf) filters for "LOGIN" and "Ban:". These filter must additionally be copied into folder "/etc/fail2ban/filter.d". + +The different filters enables to define different jail rules for violations, e.g.: + +``` +[DEFAULT] +bantime = 1800 +findtime = 300 + +[cali-dtls] +enabled = true +port = 5684 +protocol = udp +filter = calidtls +logpath = /home/cali/logs/ban.log + +# https: use nat destination port 8080! + +[cali-https] +enabled = true +port = 8080 +protocol = tcp +filter = calihttps +logpath = /home/cali/logs/ban.log + +[cali-login] +enabled = true +port = 8080 +protocol = tcp +filter = calilogin +logpath = /home/cali/logs/ban.log +bantime = 300 +findtime = 150 +``` + +and copy [cali2fail.conf](./service/fail2ban/cali2fail.conf) into folder "/etc/fail2ban/jail.d". That applies the before defined filter to the "ban.log" and "jails" ``. DTLS and HTTPS violations will be banned for 30 minutes, if 3 violations are detected within 5 minutes. LOGIN violations will be banned for 5 minutes, if 3 violations are detected within 2 minutes and 30 seconds. + +To check the jail, use + +``` +fail2ban-client status cali-https +Status for the jail: cali-https +|- Filter +| |- Currently failed: 2 +| |- Total failed: 1000 +| `- File list: /home/cali/logs/ban.log +`- Actions + |- Currently banned: 0 + |- Total banned: 23 + `- Banned IP list: +``` + +## Systemd service + +**Note:** The installation contains "secrets", e.g. to store the DTLS state or to read the device credentials. Therefore a dedicated cloud-VM must be used and the access to that cloud-VM must be protected! This basic/simple setup also uses the "root" user. Please replace/add a different user according your security policy. + +**Note:** the Cloud Demo Server is not released! It requires a [Build using Maven](../../README.md#build-using-maven) before usage. + +The server runs as [systemd service](./service/cali.service). It may be installed either manually or using the [installation script](./service/cloud-installs/deploy-dev.sh). + +Manual installation follows the [cf-unix-setup](../cf-unix-setup). It requires also to add the [HTTPS forwarding](#https-forwarding) and to create the https x509 credentials as [described above](#https-x509-certificate). That approach doesn't require specific scripts and should fit for any cloud, which supports "compute instance" with UDP support. + +The alternative is to use the [installation script](./service/cloud-installs/deploy-dev.sh), which supports the [ExoScale](https://www.exoscale.com/), [DigitalOcean](https://cloud.digitalocean.com), and [AWS](https://aws.amazon.com). It requires an account at that provider, to download and install the provider's CLI tools and it comes with costs! See [ExoScale Script](./service/cloud-installs/provider-exo.sh), [DigitalOcean Script](./service/cloud-installs/provider-do.sh) and [AWS Script](./service/cloud-installs/provider-aws.sh) for more details and requirements. + +The script provides jobs to "create" (create cloud VM/EC2 instance), "install" (install cloud demo server service from local sources and builds, along with fail2ban, https-forwarding and preparation for `certbot`), "update" (Update cloud demo server service from local sources and builds), "login" (ssh login into cloud-VM), and "delete" (delete cloud-VM). It is currently configured to use a [Ubuntu 22.04 LTS Server](https://ubuntu.com/download/server) image, but you may adapt that in the scripts. + +Usage: + +```sh +./deploy-dev.sh exo create install +``` + +Creates a ExoScale cloud-VM and installs the cloud-demo-server including generating a key-pair for RPK. On finish, the file [permissions](./service/permissions.sh) are adjusted and the installation is completed by a reboot. + +Initially the default openssl PSK device credentials are provided (identity: Client_identity, shared key: secretPSK). That is intended to be replaced by you soon by editing the "/home/cali/demo-devices.txt" file. + +To enable the simple web read access, a x509 certificate is required. As mentioned [above](#https-x509-certificate---lets-encrypt), this requires to redirect the intended DNS-domain to the ip-address of the create cloud VM. The ip-address is shown at the end of the installation: + +```sh +... +Reboot cloud VM. +use: ssh root@?1.?2.??7.8? to login! +``` + +Please consult your DNS provider how to redirect your DNS-domain to that ip-address. + +After redirecting the DNS-domain, login to the cloud VM. + +```sh +ssh root@?1.?2.??7.8? +``` + +and request a certificate with the script provided during the installation. + +```sh +./letsencrypt.sh +``` + +The script adjusts the required permission on the file-system and also adapts the [/etc/systemd/system/cali.service](./service/cali.service) file on the cloud-VM and adding `--https-credentials` + +``` +Environment="HTTPS_CERT_ARGS=--https-credentials=/etc/letsencrypt/live/" +``` + +Finally it restarts the service + +That's it for the web application. After a couple of seconds try to test the coaps-server using: + +```sh +coap-client -v 6 -m POST -e "test 1234" -u Client_identity -k secretPSK coaps://:5684/devices +``` + +and the https-server using a web-browser and `https://`. If both test are succeed, then your cloud demo server is installed well. diff --git a/demo-apps/cf-cloud-demo-server/docs/browser-device.png b/demo-apps/cf-cloud-demo-server/docs/browser-device.png new file mode 100644 index 0000000000000000000000000000000000000000..7208f5674254b24ef1341c9f7989e2b300ab763a GIT binary patch literal 18791 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

Yy83H9umj8KCGfAv@llo8Ky-n} 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 TVeNcmW6I0J=jc;6%y0hzsGI7E literal 0 HcmV?d00001 diff --git a/demo-apps/cf-cloud-demo-server/docs/browser-diagnose-page.png b/demo-apps/cf-cloud-demo-server/docs/browser-diagnose-page.png new file mode 100644 index 0000000000000000000000000000000000000000..193c31bf3323323483952913079fb1ccdd16668b GIT binary patch literal 87686 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 Vwli;{tr7AY>{}(pBU;Y39 literal 0 HcmV?d00001 diff --git a/demo-apps/cf-cloud-demo-server/docs/cloudcoap.svg b/demo-apps/cf-cloud-demo-server/docs/cloudcoap.svg new file mode 100644 index 0000000000..18d30379dd --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/docs/cloudcoap.svg @@ -0,0 +1,47 @@ + + + + + + + +coap + + + + + + + + + + + + + + + + + + + + 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..263641c629 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/pom.xml @@ -0,0 +1,92 @@ + + + + 4.0.0 + + + org.eclipse.californium + demo-apps + 3.12.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 + + + + maven-dependency-plugin + + + copy-installed + + install + + copy + + + + + ${project.groupId} + cf-encrypt + ${project.version} + ${project.packaging} + + + target + + + + + + + + \ 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..08100a42f4 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/cali.service @@ -0,0 +1,66 @@ +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# 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="OPTS=-XX:MaxRAMPercentage=75 -Dlogback.configurationFile=./logback.xml" +Environment="ARGS1=--no-loopback --store-file=connections.bin --store-max-age=72 --store-password64=TDNLOmJTWi13JUs/YGdvNA==" +Environment="ARGS2=--device-file=demo-devices.txt --coaps-credentials ." +Environment="HTTPS_ARGS=--https-port=8080" +Environment="HTTPS_CERT_ARGS=--https-credentials=/etc/letsencrypt/live/" +ExecStartPre=/bin/cp -u cf-cloud-demo-server-update.jar ${JAR} +ExecStart=/usr/bin/java $OPTS -jar ${JAR} $ARGS1 $ARGS2 $HTTPS_ARGS $HTTPS_CERT_ARGS +RestartSec=10 +Restart=always +OOMPolicy=stop + +[Install] +WantedBy=multi-user.target diff --git a/demo-apps/cf-cloud-demo-server/service/cloud-installs/cloud-config-dev.yaml b/demo-apps/cf-cloud-demo-server/service/cloud-installs/cloud-config-dev.yaml new file mode 100644 index 0000000000..8c445db688 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/cloud-installs/cloud-config-dev.yaml @@ -0,0 +1,47 @@ +#cloud-config + +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# cloud-init configuration to deploy local artefacts with deploy-dev.sh + +package_upgrade: true + +packages: +# java - runtime for java application + - openjdk-17-jre-headless +# fail2ban - network protection + - fail2ban + +snap: + commands: + - snap refresh +# public x509 certificate / letsencrypt + - snap install --classic certbot + +disable_root: false + +users: + - name: cali + gecos: (Cf) Californium Demo Server + lock_passwd: true + +# the java application, ip-firewall and fail2ban configuration +# are applied from local storage with scp/ssh + diff --git a/demo-apps/cf-cloud-demo-server/service/cloud-installs/deploy-dev.sh b/demo-apps/cf-cloud-demo-server/service/cloud-installs/deploy-dev.sh new file mode 100755 index 0000000000..1976c5eb7a --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/cloud-installs/deploy-dev.sh @@ -0,0 +1,326 @@ +#!/bin/sh + +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# requirements: (see "provider_???.sh" for details) +# - create cloud-provider account +# - download/install cloud-provider's CLI tools +# - upload ssh keys +# +# This script deploys the local artefacts into the cloud. +# A script to deploy a release will come. +# +# Each provider script must implement: +# - get_ip : returns the ip-address of the cloud-vm ${name} in ${ip} +# - provider_create_cloud_vm : creates a cloud-vm using ${name}, ${ssh_key_id}, and +# ${cloud_config} +# - provider_delete_cloud_vm : deletes cloud-vm ${name} +# +# required sh commands: +# - readlink +# - sed +# - head +# - base64 +# +# if readlink is not available, you may export following paths to prevent errors: +# +# export INCPATH=/path/to/service/cloud-installs +# export SETUPATH=/path/to/service // with firewall and letsencrypt.sh +# export FAIL2BANPATH=/path/to/service/fail2ban +# export SERVICEPATH=/path/to/service // with cali.service and demo-devices.txt +# +# if head or base64 are not available, you may export SECRET to prevent errors. +# +# The script uses "cali-demo" as service name, and the "cali" as ssh-key-id. +# To change that export "name" and/or "ssh_key_id": +# +# export name=coaps-s3 +# +# Required sh commands for do (Digital Ocean) only: +# - grep +# - cut +# + +# Name of cloud VM +if [ -z "$name" ] ; then + export name=cali-demo +fi + +# Ensure, your ssh keys are already uploaded to your provider with name "cali"! +# See "provider_???.sh" for some instructions. +if [ -z "$ssh_key_id" ] ; then + export ssh_key_id="cali" +fi + +# setup firewall and letsencrypt +if [ -z "${SETUPPATH}" ] ; then + SETUPFULLPATH=$(readlink -f $0) + SETUPPATH=${SETUPFULLPATH%/*/*} +fi + +# import "provider_???.sh" +if [ -z "${INCPATH}" ] ; then + INCPATH=${SETUPPATH}/cloud-installs +fi + +# setup fail2ban +if [ -z "${FAIL2BANPATH}" ] ; then + FAIL2BANPATH=${SETUPPATH}/fail2ban +fi + +# setup service +if [ -z "${SERVICEPATH}" ] ; then + SERVICEPATH=${SETUPPATH} +fi + +if [ -z "$cloud_config" ] ; then + export cloud_config=${INCPATH}/cloud-config-dev.yaml +fi + +# Version to deploy +: "${CALI_VERSION=3.12.0-SNAPSHOT}" + +# ssh login user +: "${user=root}" + +# run jobs per default +: "${run_jobs=1}" + +wait_cloud_init_ready () { + status=$(ssh -o "StrictHostKeyChecking=accept-new" ${user}@${ip} "cloud-init status") + + while [ "${status}" != "status: done" ] ; do + echo "cloud-init: ${status}, waiting for done" + sleep 10 + status=$(ssh -o "StrictHostKeyChecking=accept-new" ${user}@${ip} "cloud-init status") + done + echo "cloud-init: ${status}" +} + +install_cloud_vm_base () { + + get_ip + + ssh -o "StrictHostKeyChecking=accept-new" ${user}@${ip} "exit" + + # firewall & forward + scp ${SETUPPATH}/iptables.service ${user}@${ip}:/etc/systemd/system + scp ${SETUPPATH}/iptables-firewall.sh ${user}@${ip}:/sbin + ssh ${user}@${ip} "chmod u+x /sbin/iptables-firewall.sh; systemctl enable iptables" + + # let's encrypt + scp ${SETUPPATH}/letsencrypt.sh ${user}@${ip}:. + + # fail2ban + scp ${FAIL2BANPATH}/cali2fail.conf ${user}@${ip}:/etc/fail2ban/jail.d + scp ${FAIL2BANPATH}/calidtls.conf ${user}@${ip}:/etc/fail2ban/filter.d + scp ${FAIL2BANPATH}/calihttps.conf ${user}@${ip}:/etc/fail2ban/filter.d + scp ${FAIL2BANPATH}/calilogin.conf ${user}@${ip}:/etc/fail2ban/filter.d + + ssh ${user}@${ip} "su cali -c 'mkdir /home/cali/logs; touch /home/cali/logs/ban.log'" + + ssh ${user}@${ip} "systemctl enable fail2ban" + + # random secret for dtls graceful restart + if [ -z "${SECRET}" ] ; then + SECRET=$(cat /dev/urandom | head -c32 | base64) + echo "${SECRET}" > ${SERVICEPATH}/store-password64 + fi +} + +install_cloud_vm () { + echo "install ${provider} server ${name}" + + install_cloud_vm_base + + # replace dtls graceful restart password + sed "s!--store-password64=[^\"\t ]*!--store-password64=${SECRET}!" ${SERVICEPATH}/cali.service >${SERVICEPATH}/cali.service.e + + # service (includes credentials!) + scp ${SERVICEPATH}/cali.service.e ${user}@${ip}:/etc/systemd/system/cali.service + scp ${SERVICEPATH}/../target/cf-cloud-demo-server-${CALI_VERSION}.jar ${user}@${ip}:/home/cali/cf-cloud-demo-server-update.jar + scp ${SERVICEPATH}/../src/main/resources/logback.xml ${user}@${ip}:/home/cali + + scp ${SERVICEPATH}/demo-devices.txt ${user}@${ip}:/home/cali + scp ${SERVICEPATH}/permissions.sh ${user}@${ip}:/home/cali + + # create coap ec-key-pair and apply permissions + ssh ${user}@${ip} "openssl ecparam -genkey -name prime256v1 -noout -out /home/cali/privkey.pem; sh /home/cali/permissions.sh" + + ssh ${user}@${ip} "systemctl enable cali" + + ssh ${user}@${ip} "systemctl reboot" + + echo "Reboot cloud VM." + + echo "use: ssh ${user}@${ip} to login!" +} + +update_cloud_vm () { + echo "update ${provider} server ${name}" + + get_ip + + if [ -z "${SECRET}" ] ; then + SECRET=$(cat ${SERVICEPATH}/store-password64) + fi + + # replace dtls graceful restart password + sed "s!--store-password64=[^\"\t ]*!--store-password64=${SECRET}!" ${SERVICEPATH}/cali.service >${SERVICEPATH}/cali.service.e + + # update service (includes credentials!) + scp ${SERVICEPATH}/cali.service.e ${user}@${ip}:/etc/systemd/system/cali.service + scp ${SERVICEPATH}/../target/cf-cloud-demo-server-${CALI_VERSION}.jar ${user}@${ip}:/home/cali/cf-cloud-demo-server-update.jar + scp ${SERVICEPATH}/../src/main/resources/logback.xml ${user}@${ip}:/home/cali + + ssh ${user}@${ip} "sh /home/cali/permissions.sh; systemctl daemon-reload; systemctl restart cali;" + + echo "use: ssh ${user}@${ip} to login!" +} + +update_app () { + echo "update app ${provider} server ${name}" + + get_ip + + scp ${SERVICEPATH}/../target/cf-cloud-demo-server-${CALI_VERSION}.jar ${user}@${ip}:/home/cali/cf-cloud-demo-server-update.jar + + ssh ${user}@${ip} "systemctl restart cali" + + echo "use: ssh ${user}@${ip} to login!" +} + +login_cloud_vm () { + echo "login ${provider} server ${name}" + + get_ip + + if [ "${ip}" ] ; then + wait_cloud_init_ready + + echo "use: ssh ${user}@${ip} to login!" + else + echo "${name} not available at ${provider}!" + exit 1 + fi +} + +create_cloud_vm () { + get_ip + + if [ "${ip}" ] ; then + echo "${name} already exists!" + exit 1 + fi + + provider_create_cloud_vm +} + +delete_cloud_vm () { + get_ip + + provider_delete_cloud_vm + + echo "Please verify the successful deletion via the Web UI to prevent unexpected costs!" + + if [ -n "${ip}" ] ; then + echo "Remove the ssh trust for ${ip}" + ssh-keygen -f ~/.ssh/known_hosts -R "${ip}" + else + echo "No IP address found for ${name}." + fi +} + +provider () { + provider_id=$1 + case $1 in + "exo") + provider="ExoScale" + . $INCPATH/provider-exo.sh + ;; + "aws") + provider="AWS" + . $INCPATH/provider-aws.sh + ;; + "do") + provider="DigitalOcean" + . $INCPATH/provider-do.sh + ;; + *) + echo "Provider \"$1\" unknown! Use: exo|aws|do." + exit 1 + ;; + esac +} + +jobs () { + echo "${provider} $1" + case $1 in + "create") + create_cloud_vm + ;; + "delete") + delete_cloud_vm + ;; + "install") + install_cloud_vm + ;; + "login") + login_cloud_vm + ;; + "update") + update_cloud_vm + ;; + "update-app") + update_cloud_vm + ;; + *) + echo "Job \"$1\" unknown! Use: (create|delete|install|login|update)+" + exit 1 + ;; + esac +} + +all_jobs () { + for JOB in ${JOBS}; do + jobs ${JOB} + done +} + + +if [ -z "$1" ] ; then + echo "Missing cloud provider. Use: exo|aws|do" + exit +else + provider $1 + shift +fi + +if [ -z "$1" ] ; then + echo "Missing job. Use: (create|delete|install|login|update)+" + exit +else + JOBS=$@ +fi + +if [ ${run_jobs} -eq 1 ] ; then +# skipped, if included + all_jobs +fi diff --git a/demo-apps/cf-cloud-demo-server/service/cloud-installs/provider-aws.sh b/demo-apps/cf-cloud-demo-server/service/cloud-installs/provider-aws.sh new file mode 100755 index 0000000000..ff7d74052a --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/cloud-installs/provider-aws.sh @@ -0,0 +1,124 @@ +#!/bin/sh + +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# requirements: +# +# - activate account at https://portal.aws.amazon.com/billing/signup (please obey the resulting costs!) +# - follow https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html +# - Create an IAM user account https://docs.aws.amazon.com/cli/latest/userguide/getting-started-prereqs.html#getting-started-prereqs-iam +# - Create an access key ID and secret access key https://docs.aws.amazon.com/cli/latest/userguide/getting-started-prereqs.html#getting-started-prereqs-keys +# - install awscli2 https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html +# and configure it https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html +# - upload your ssh-key (either rsa or ed25519) using the ec2 console using the name "cali" or +# copy a different used name to "ssh_key_id" below. +# +# Adapt the the "vmsize" according your requirements and wanted price. +# See https://aws.amazon.com/ec2/pricing/ +# + +vmsize="t2.micro" +vmimage="ubuntu/images/hvm-ssd/*22.04*amd64*server*" + +get_ami() { + echo "AMI: " $(aws ec2 describe-images --filters "Name=name,Values=${vmimage}" --owner amazon --query "sort_by(Images, &CreationDate)[-1].Name") + ami_id=$(aws ec2 describe-images --filters "Name=name,Values=${vmimage}" --owner amazon --query "sort_by(Images, &CreationDate)[-1].ImageId" --output text) + echo "ami-id: ${ami_id}" +} + +get_vm_id() { + vm_id=$(aws ec2 describe-instances --filter Name=instance.group-name,Values=${name}-sg --query='Reservations[*].Instances[*].InstanceId' --output text) + echo "vm-id: ${vm_id}" +} + +get_ip() { + ip=$(aws ec2 describe-instances --filter Name=instance.group-name,Values=${name}-sg --query='Reservations[*].Instances[*].PublicIpAddress' --output text) + echo "vm-ip : ${ip}" + ipv6=$(aws ec2 describe-instances --filter Name=instance.group-name,Values=${name}-sg --query='Reservations[*].Instances[*].Ipv6Address' --output text) + echo "vm-ipv6: ${ipv6}" +} + +wait_vm_ready() { + status=$(aws ec2 describe-instances --filter Name=instance.group-name,Values=${name}-sg --query='Reservations[*].Instances[*].State.Name' --output text) + while [ "${status}" != "running" ] ; do + echo "vm: ${status}, waiting for running" + sleep 10 + status=$(aws ec2 describe-instances --filter Name=instance.group-name,Values=${name}-sg --query='Reservations[*].Instances[*].State.Name' --output text) + done + echo "vm: ${status}" +} + +wait_vm_terminated() { + status=$(aws ec2 describe-instances --filter Name=instance.group-name,Values=${name}-sg --query='Reservations[*].Instances[*].State.Name' --output text) + while [ "${status}" = "running" ] || [ "${status}" = "shutting-down" ] ; do + echo "vm: ${status}, waiting" + sleep 10 + status=$(aws ec2 describe-instances --filter Name=instance.group-name,Values=${name}-sg --query='Reservations[*].Instances[*].State.Name' --output text) + done + echo "vm: ${status}" +} + +provider_create_cloud_vm() { + echo "create aws server ${name}" + + get_ami + + aws ec2 create-security-group --group-name ${name}-sg --description "${name} security group" --output text + + aws ec2 authorize-security-group-ingress --group-name ${name}-sg --ip-permissions \ + IpProtocol=tcp,FromPort=22,ToPort=22,IpRanges='[{CidrIp=0.0.0.0/0}]' \ + IpProtocol=tcp,FromPort=22,ToPort=22,Ipv6Ranges='[{CidrIpv6=::/0}]' \ + IpProtocol=tcp,FromPort=80,ToPort=80,IpRanges='[{CidrIp=0.0.0.0/0}]' \ + IpProtocol=tcp,FromPort=80,ToPort=80,Ipv6Ranges='[{CidrIpv6=::/0}]' \ + IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges='[{CidrIp=0.0.0.0/0}]' \ + IpProtocol=tcp,FromPort=443,ToPort=443,Ipv6Ranges='[{CidrIpv6=::/0}]' \ + IpProtocol=udp,FromPort=5684,ToPort=5684,IpRanges='[{CidrIp=0.0.0.0/0}]' \ + IpProtocol=udp,FromPort=5684,ToPort=5684,Ipv6Ranges='[{CidrIpv6=::/0}]' \ + --no-cli-pager + + aws ec2 run-instances --image-id ${ami_id} --count 1 --instance-type ${vmsize} \ + --key-name ${ssh_key_id} --security-groups ${name}-sg --no-cli-pager \ + --user-data file://${cloud_config} + + echo "wait to give vm time to finish the installation!" + + wait_vm_ready + + get_ip + + wait_cloud_init_ready + + echo "use: ssh root@${ip} to login!" +} + +provider_delete_cloud_vm() { + echo "delete aws server ${name}" + + get_vm_id + + if [ -n "${vm_id}" ] ; then + aws ec2 terminate-instances --instance-ids ${vm_id} + wait_vm_terminated + sleep 5 + fi + + aws ec2 delete-security-group --group-name ${name}-sg + +} diff --git a/demo-apps/cf-cloud-demo-server/service/cloud-installs/provider-do.sh b/demo-apps/cf-cloud-demo-server/service/cloud-installs/provider-do.sh new file mode 100755 index 0000000000..4fcab1b5fd --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/cloud-installs/provider-do.sh @@ -0,0 +1,118 @@ +#!/bin/sh + +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# requirements: +# +# - activate account at https://www.digitalocean.com/ (please obey the resulting costs!) +# - install https://docs.digitalocean.com/reference/doctl/how-to/install/ . That requires +# to create an API access token. +# - upload your ssh-key to https://cloud.digitalocean.com/account/security and copy +# the fingerprint of the ssh-key to "ssh_key_id" below. +# +# Adapt the the "vmsize" according your requirements and wanted price. +# See https://www.digitalocean.com/pricing/ and +# run `doctl compute size list` to see the options. +# +# Available regions: +# run `doctl compute region list` +# +# Available images: +# run `doctl compute image list-distribution` +# +# required sh commands: +# - grep +# - cut + +vmsize="s-2vcpu-2gb" +vmimage="ubuntu-22-04-x64" + +get_ip() { + ip=$(doctl compute droplet get ${name} --template {{.PublicIPv4}}) + echo "vm-ip : ${ip}" + ipv6=$(doctl compute droplet get ${name} --template {{.PublicIPv6}}) + echo "vm-ipv6: ${ipv6}" +} + +wait_vm_ready() { + status=$(doctl compute droplet get ${name} --template {{.Status}}) + while [ "${status}" != "active" ] ; do + echo "vm: ${status}, waiting for active" + sleep 10 + status=$(doctl compute droplet get ${name} --template {{.Status}}) + done + echo "vm: ${status}" +} + +provider_create_cloud_vm() { + echo "create digitalocean firewall ${name}" + + doctl compute tag create ${name} + + doctl compute firewall create \ + --name ${name} \ + --tag-names ${name} \ + --inbound-rules="protocol:tcp,ports:22,address:0.0.0.0/0,address:::/0 protocol:tcp,ports:80,address:0.0.0.0/0,address:::/0 protocol:tcp,ports:443,address:0.0.0.0/0,address:::/0 protocol:udp,ports:5684,address:0.0.0.0/0,address:::/0" \ + --outbound-rules="protocol:tcp,ports:all,address:0.0.0.0/0,address:::/0 protocol:udp,ports:all,address:0.0.0.0/0,address:::/0 protocol:icmp,address:0.0.0.0/0,address:::/0" + + echo "create digitalocean server ${name}, may take a couple of minutes to complete." + + # get ssh_key ID from Name + do_ssh_key_id=$(doctl compute ssh-key list --format "ID,Name" | grep ${ssh_key_id} | cut -sd ' ' -f 1) + + doctl compute droplet create ${name} \ + --tag-name ${name} \ + --image "${vmimage}" \ + --enable-ipv6 \ + --region "fra1" \ + --size "${vmsize}" \ + --ssh-keys "${do_ssh_key_id}" \ + --user-data-file "${cloud_config}" \ + --wait + + echo "wait to give vm time to finish the installation!" + + wait_vm_ready + + doctl compute droplet get ${name} --format ID,PublicIPv4,PublicIPv6 + + get_ip + + wait_cloud_init_ready + + echo "use: ssh root@${ip} to login!" +} + +provider_delete_cloud_vm() { + echo "delete digitalocean server ${name}" + + doctl compute droplet delete ${name} + + id=$(doctl compute firewall list --format "ID,Name" | grep ${name} | cut -sd ' ' -f 1) + + if [ -n "${id}" ] ; then + echo "delete digitalocean fw ${id}" + doctl compute firewall delete ${id} + fi + + doctl compute tag delete ${name} + +} + diff --git a/demo-apps/cf-cloud-demo-server/service/cloud-installs/provider-exo.sh b/demo-apps/cf-cloud-demo-server/service/cloud-installs/provider-exo.sh new file mode 100755 index 0000000000..6608359709 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/cloud-installs/provider-exo.sh @@ -0,0 +1,141 @@ +#!/bin/sh + +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# requirements: +# +# - activate account at https://portal.exoscale.com/register (please obey the resulting costs!) +# - install https://community.exoscale.com/documentation/tools/exoscale-command-line-interface/ +# and configure it. That requires also to create an api-key. +# - upload your ssh-key at https://portal.exoscale.com/compute/keypairs +# using the name "cali" or copy a different used name to "ssh_key_id" below. +# +# Adapt the the "vmsize" according your requirements and wanted price. +# See https://www.exoscale.com/pricing/ and +# run `exo compute instance create --help` to see the options. +# +# Available regions: +# run `exo zone` +# +# Available images: +# run `exo compute instance-template list` +# + +vmsize="standard.small" +vmimage="Linux Ubuntu 22.04 LTS 64-bit" + +get_ip() { + ip=$(exo compute instance show ${name} -O text --output-template '{{ .IPAddress }}') + echo "vm-ip : ${ip}" + ipv6=$(exo compute instance show ${name} -O text --output-template '{{ .IPv6Address }}') + echo "vm-ipv6: ${ipv6}" +} + +wait_vm_ready() { + status=$(exo compute instance show ${name} -O text --output-template '{{ .State }}') + while [ "${status}" != "running" ] ; do + echo "vm: ${status}, waiting for running" + sleep 10 + status=$(exo compute instance show ${name} -O text --output-template '{{ .State }}') + done + echo "vm: ${status}" +} + +provider_create_cloud_vm() { + echo "create exoscale server ${name}" + + exo compute security-group create ${name}-group + + exo compute security-group rule add ${name}-group \ + --description "ssh ipv4" \ + --protocol tcp \ + --network "0.0.0.0/0" \ + --port 22 + + exo compute security-group rule add ${name}-group \ + --description "ssh ipv6" \ + --protocol tcp \ + --network "::/0" \ + --port 22 + + exo compute security-group rule add ${name}-group \ + --description "http ipv4" \ + --protocol tcp \ + --network "0.0.0.0/0" \ + --port 80 + + exo compute security-group rule add ${name}-group \ + --description "http ipv6" \ + --protocol tcp \ + --network "::/0" \ + --port 80 + + exo compute security-group rule add ${name}-group \ + --description "https ipv4" \ + --protocol tcp \ + --network "0.0.0.0/0" \ + --port 443 + + exo compute security-group rule add ${name}-group \ + --description "https ipv6" \ + --protocol tcp \ + --network "::/0" \ + --port 443 + + exo compute security-group rule add ${name}-group \ + --description "coaps ipv4" \ + --protocol udp \ + --network "0.0.0.0/0" \ + --port 5684 + + exo compute security-group rule add ${name}-group \ + --description "coaps ipv6" \ + --protocol udp \ + --network "::/0" \ + --port 5684 + + exo compute instance create ${name} \ + --zone de-fra-1 \ + --disk-size 10 \ + --instance-type "${vmsize}" \ + --template "${vmimage}" \ + --ipv6 \ + --ssh-key "${ssh_key_id}" \ + --cloud-init ${cloud_config} \ + --security-group ${name}-group + + echo "wait to give vm time to finish the installation!" + + wait_vm_ready + + get_ip + + wait_cloud_init_ready + + echo "use: ssh root@${ip} to login!" +} + +provider_delete_cloud_vm() { + echo "delete exoscale server ${name}" + + exo compute instance delete ${name} + exo compute security-group delete ${name}-group --force +} + diff --git a/demo-apps/cf-cloud-demo-server/service/demo-devices.txt b/demo-apps/cf-cloud-demo-server/service/demo-devices.txt new file mode 100644 index 0000000000..9399eac902 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/demo-devices.txt @@ -0,0 +1,38 @@ +# Device store for Cloud Demo + +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# # comments +# [.group]= +# # base64 +# [[].psk='',] +# # hexadecimal +# [[].psk='',:0x] +# # plain-text +# [[].psk='',''] +# # base64 +# [[].rpk=] +# # hexadecimal +# [[].rpk=:0x] + +Demo1=Thing +.psk='Client_identity',c2VjcmV0UFNL +.rpk=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQxYO5/M5ie6+3QPOaAy5MD6CkFILZwIb2rOBCX/EWPaocX1H+eynUnaEEbmqxeN6rnI/pH19j4PtsegfHLrzzQ== + diff --git a/demo-apps/cf-cloud-demo-server/service/fail2ban/cali2fail.conf b/demo-apps/cf-cloud-demo-server/service/fail2ban/cali2fail.conf new file mode 100644 index 0000000000..2ec25067b6 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/fail2ban/cali2fail.conf @@ -0,0 +1,50 @@ +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# To install, cp to /etc/fail2ban/jail.d + +[DEFAULT] +bantime = 1800 +findtime = 300 + +[cali-dtls] +enabled = true +port = 5684 +protocol = udp +filter = calidtls +logpath = /home/cali/logs/ban.log + +# https: use nat destination port 8080! + +[cali-https] +enabled = true +port = 8080 +protocol = tcp +filter = calihttps +logpath = /home/cali/logs/ban.log + +[cali-login] +enabled = true +port = 8080 +protocol = tcp +filter = calilogin +logpath = /home/cali/logs/ban.log +bantime = 300 +findtime = 150 +maxretry = 3 diff --git a/demo-apps/cf-cloud-demo-server/service/fail2ban/calidtls.conf b/demo-apps/cf-cloud-demo-server/service/fail2ban/calidtls.conf new file mode 100644 index 0000000000..4794c7126a --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/fail2ban/calidtls.conf @@ -0,0 +1,30 @@ +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# To install, cp to /etc/fail2ban/filter.d + +# cali +[INCLUDES] + +before = common.conf + +[Definition] + +failregex = DTLS\s+Ban:\s+$ + diff --git a/demo-apps/cf-cloud-demo-server/service/fail2ban/calihttps.conf b/demo-apps/cf-cloud-demo-server/service/fail2ban/calihttps.conf new file mode 100644 index 0000000000..084b136205 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/fail2ban/calihttps.conf @@ -0,0 +1,30 @@ +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# To install, cp to /etc/fail2ban/filter.d + +# cali +[INCLUDES] + +before = common.conf + +[Definition] + +failregex = HTTPS\s+Ban:\s+$ + diff --git a/demo-apps/cf-cloud-demo-server/service/fail2ban/calilogin.conf b/demo-apps/cf-cloud-demo-server/service/fail2ban/calilogin.conf new file mode 100644 index 0000000000..a6752c9b84 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/fail2ban/calilogin.conf @@ -0,0 +1,30 @@ +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# To install, cp to /etc/fail2ban/filter.d + +# cali +[INCLUDES] + +before = common.conf + +[Definition] + +failregex = LOGIN\s+Ban:\s+$ + 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..6a7c1125c2 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/iptables-firewall.sh @@ -0,0 +1,59 @@ +#!/bin/sh + +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# 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 -D PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8080 2> /dev/null + ip6tables -t nat -D PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8080 2> /dev/null + echo "start https forwarding ..." + 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 + echo "started https forwarding" +} + +# clear iptables configuration +firewall_stop() { + # Delete https forward + echo "stop https forwarding ..." + 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 + echo "stopped https forwarding" +} + +# execute action +case "$1" in + start|restart) + echo "Starting firewall" + 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..5dbec08553 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/iptables.service @@ -0,0 +1,37 @@ +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# 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/service/letsencrypt.sh b/demo-apps/cf-cloud-demo-server/service/letsencrypt.sh new file mode 100755 index 0000000000..0a45785423 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/letsencrypt.sh @@ -0,0 +1,76 @@ +#!/bin/sh + +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# Adjust permission for let's encrypt https x509 credentials. +# +# See: https://eff-certbot.readthedocs.io/en/stable/using.html#where-are-my-certificates +# +# Requirements: +# - install certbot (see https://certbot.eff.org/instructions?ws=other&os=ubuntufocal) +# - request certificate for +# certbot certonly --standalone --key-type ecdsa --elliptic-curve secp256r1 -d +# +# Usage: ./letsencrypt.sh + +if [ -z "$1" ] ; then + echo "Missing domain!" + exit +fi + + +letsencrypt=/etc/letsencrypt + +if [ ! -d "${letsencrypt}/live/$1" ]; then + echo "Request x509 certificate for $1" + certbot certonly --standalone --key-type ecdsa --elliptic-curve secp256r1 -d $1 + if [ ! -d "${letsencrypt}/live/$1" ]; then + echo "Missing credentials for $1" + exit 1 + fi +fi + +echo "Adjust file-system permissions for let's encrypt credentials" + +chmod go+rx ${letsencrypt}/live + +if [ -d "${letsencrypt}/archive" ]; then + chmod go+rx ${letsencrypt}/archive + if [ -d "${letsencrypt}/archive/$1" ]; then + echo "Add read grants for group cali" + chmod g+r ${letsencrypt}/archive/$1/privkey1.pem + chown root:cali ${letsencrypt}/archive/$1/privkey1.pem + fi +else + echo "Missing credentials archive for $1" + exit 1 +fi + +service=/etc/systemd/system/cali.service +if [ -f "${service}" ]; then + echo "Configure cali.service to use let's encrypt credentials" + sed -i "s!--https-credentials=[^\"\t ]*!--https-credentials=/etc/letsencrypt/live/$1!" ${service} + grep -- "--https-credentials=" ${service} + chmod o-r ${service} + echo "Restart cali.service with let's encrypt credentials" + systemctl daemon-reload + systemctl restart cali +fi + diff --git a/demo-apps/cf-cloud-demo-server/service/permissions.sh b/demo-apps/cf-cloud-demo-server/service/permissions.sh new file mode 100755 index 0000000000..3b10edf58f --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/service/permissions.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +#/******************************************************************************* +# * Copyright (c) 2024 Contributors to the Eclipse Foundation. +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * 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. +# * +# * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause +# * +# ******************************************************************************/ +# +# Adjust permission for several configuration files, which may contain credentials + +chmod o-r /etc/systemd/system/cali.service + +chmod o-r /home/cali/demo-devices.txt +chown cali:cali /home/cali/demo-devices.txt + +chmod o-r /home/cali/privkey.pem +chown cali:cali /home/cali/privkey.pem + 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..40b381783a --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/BaseServer.java @@ -0,0 +1,807 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ********************************************************************************/ +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.PrivateKey; +import java.security.PublicKey; +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 org.eclipse.californium.cloud.http.HttpService; +import org.eclipse.californium.cloud.http.HttpService.CoapProxyHandler; +import org.eclipse.californium.cloud.http.HttpService.ForwardHandler; +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.cloud.util.DeviceGredentialsProvider; +import org.eclipse.californium.cloud.util.DeviceManager; +import org.eclipse.californium.cloud.util.DeviceParser; +import org.eclipse.californium.cloud.util.ResourceStore; +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.Configuration; +import org.eclipse.californium.elements.config.Configuration.DefinitionsProvider; +import org.eclipse.californium.elements.config.IntegerDefinition; +import org.eclipse.californium.elements.config.SystemConfig; +import org.eclipse.californium.elements.config.TimeDefinition; +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.SslContextUtil.Credentials; +import org.eclipse.californium.elements.util.StringUtil; +import org.eclipse.californium.elements.util.SystemResourceMonitors; +import org.eclipse.californium.elements.util.SystemResourceMonitors.SystemResourceMonitor; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.DtlsHealthLogger; +import org.eclipse.californium.scandium.MdcConnectionListener; +import org.eclipse.californium.scandium.auth.ApplicationLevelInfoSupplier; +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.cipher.CipherSuite; +import org.eclipse.californium.scandium.dtls.pskstore.AdvancedPskStore; +import org.eclipse.californium.scandium.dtls.x509.CertificateProvider; +import org.eclipse.californium.scandium.dtls.x509.NewAdvancedCertificateVerifier; +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.12 + */ +public class BaseServer extends CoapServer { + + static { + // only coap + dtls + CoapConfig.register(); + DtlsConfig.register(); + } + + private static final Logger LOGGER = LoggerFactory.getLogger(CoapServer.class); + 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; + + /** + * Name of private key file for DTLS 1.2 (device communication). + */ + public static final String DTLS_PRIVATE_KEY = "privkey.pem"; + /** + * Name of public key file for DTLS 1.2 (device communication). + */ + public static final String DTLS_PUBLIC_KEY = "pubkey.pem"; + + // 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 UDP drops from OS (currently only Linux).", 2000, TimeUnit.MILLISECONDS); + /** + * Maximum device in cache. + */ + public static final IntegerDefinition CACHE_MAX_DEVICES = new IntegerDefinition("CACHE_MAX_DEVICES", + "Cache maximum devices.", 5000, 100); + /** + * Threshold for stale devices. + */ + public static final TimeDefinition CACHE_STALE_DEVICE_THRESHOLD = new TimeDefinition("CACHE_STALE_DEVICE_THRESHOLD", + "Threshold for stale devices. Devices will only get removed for new ones, " + + "if at least for that threshold no messages are exchanged with that device.", + 24, TimeUnit.HOURS); + /** + * Interval to reload HTTPS credentials. + */ + public static final TimeDefinition HTTPS_CREDENTIALS_RELOAD_INTERVAL = new TimeDefinition( + "HTTPS_CREDENTIALS_RELOAD_INTERVAL", + "Reload HTTPS credentials interval. 0 to load credentials only on startup.", 30, TimeUnit.MINUTES); + /** + * Interval to reload device credentials. + */ + public static final TimeDefinition DEVICE_CREDENTIALS_RELOAD_INTERVAL = new TimeDefinition( + "DEVICE_CREDENTIALS_RELOAD_INTERVAL", + "Reload device credentials interval. 0 to load credentials only on startup.", 60, TimeUnit.SECONDS); + + /** + * Default configuration setup. + * + * @see Configuration#createWithFile(File, String, DefinitionsProvider) + */ + 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(CoapConfig.ACK_TIMEOUT, 2500, TimeUnit.MILLISECONDS); + config.set(DtlsConfig.DTLS_ROLE, DtlsRole.SERVER_ONLY); + config.set(DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT, 2500, TimeUnit.MILLISECONDS); + config.set(DtlsConfig.DTLS_ADDITIONAL_ECC_TIMEOUT, 8, TimeUnit.SECONDS); + 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_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); + config.set(CACHE_MAX_DEVICES, 5000); + config.set(CACHE_STALE_DEVICE_THRESHOLD, 24, TimeUnit.HOURS); + config.set(HTTPS_CREDENTIALS_RELOAD_INTERVAL, 30, TimeUnit.MINUTES); + config.set(DEVICE_CREDENTIALS_RELOAD_INTERVAL, 30, TimeUnit.SECONDS); + } + }; + + 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 local wildcard-address for coap endpoints.") + public boolean wildcard; + + @ArgGroup(exclusive = false) + public NetworkSelectConfig selectInterfaces; + } + + public static class NetworkSelectConfig { + + @Option(names = "--no-loopback", negatable = true, description = "enable coap endpoints on loopback network.") + public boolean loopback = true; + + @Option(names = "--no-external", negatable = true, description = "enable coap endpoints on external network.") + public boolean external = true; + + @Option(names = "--no-ipv4", negatable = true, description = "enable coap endpoints for ipv4.") + public boolean ipv4 = true; + + @Option(names = "--no-ipv6", negatable = true, description = "enable coap endpoints for ipv6.") + public boolean ipv6 = true; + + @Option(names = "--interfaces-pattern", split = ",", description = "interface regex patterns for coap 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); + } + } + } + + @ArgGroup(exclusive = false) + public HttpsConfig https; + + public static class HttpsConfig { + + @Option(names = "--https-port", required = true, description = "Port of https service.") + public int port; + + @Option(names = "--https-credentials", required = true, description = "Folder containing https credentials in 'privkey.pem' and 'fullchain.pem'.") + public String credentials; + + @Option(names = "--https-password64", description = "Folder containing https credentials in 'privkey.pem' and 'fullchain.pem'.") + public String password64; + } + + @ArgGroup(exclusive = false) + public CoapsConfig coaps; + + public static class CoapsConfig { + + @Option(names = "--coaps-credentials", required = true, description = "Folder containing coaps credentials in 'privkey.pem' and 'pubkey.pem'") + public String credentials; + + @Option(names = "--coaps-password64", required = false, description = "Password for device store. Base 64 encoded.") + public String password64; + + } + + @ArgGroup(exclusive = false) + public DeviceStore deviceStore; + + public static class DeviceStore { + + @Option(names = "--device-file", required = true, description = "Filename of device store for coap.") + public String file; + + @Option(names = "--device-file-password64", required = false, description = "Password for device store. Base 64 encoded.") + public String password64; + + } + + @ArgGroup(exclusive = false) + public Store store; + + public static class Store { + + @Option(names = "--store-file", required = true, description = "file-store for dtls state.") + public String file; + + @Option(names = "--store-max-age", required = true, description = "maximum age of connections in hours to store dtls state.") + public Integer maxAge; + + @Option(names = "--store-password64", required = false, description = "password to store dtls state. Base 64 encoded.") + public String password64; + + } + + @Option(names = "--diagnose", description = "enable 'diagnose'-resource.") + public boolean diagnose; + + public boolean noCoap; + + /** + * 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; + } + + private static String toLog(PublicKey key) { + byte[] data = key.getEncoded(); + return StringUtil.byteArray2Hex(Arrays.copyOfRange(data, data.length - 6, data.length)); + } + + public static void start(String[] args, String name, ServerConfig cliArguments, BaseServer server) { + + CommandLine cmd = new CommandLine(cliArguments); + 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); + } + + cliArguments.defaults(); + + // 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); + + boolean http = false; + if (cliArguments.https != null) { + if (cliArguments.https.port > 0) { + LOGGER.info("Create HTTPS service at port {}, credentials {}", cliArguments.https.port, + cliArguments.https.credentials); + http = HttpService.createHttpService(cliArguments.https.port, cliArguments.https.credentials, + cliArguments.https.password64, false); + } else { + LOGGER.info("HTTPS service at port {} is not supported! Must be [1-65535]", cliArguments.https.port); + } + } + + // create server + try { + server.initialize(cliArguments); + if (!cliArguments.noCoap && 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 (cliArguments.store != null) { + server.setupPersistence(cliArguments.store); + } + + server.start(); + + LOGGER.info("{} started ...", name); + + if (http) { + if (HttpService.startHttpService()) { + LOGGER.info("HTTPS service at port {} started", cliArguments.https.port); + long interval = server.getConfig().get(HTTPS_CREDENTIALS_RELOAD_INTERVAL, TimeUnit.MINUTES); + SystemResourceMonitor httpsCredentialsMonitor = HttpService.getHttpService().getFileMonitor(); + server.monitors.addOptionalMonitor("https credentials", interval, TimeUnit.MINUTES, + httpsCredentialsMonitor); + } + } + 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 (http) { + HttpService.stopHttpService(); + LOGGER.info("HTTPS service at port {} stopped", cliArguments.https.port); + } + 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; + } + } + } + + protected SystemResourceMonitors monitors; + + protected DeviceGredentialsProvider deviceCredentials; + + public BaseServer(Configuration config) { + super(config); + setVersion(CALIFORNIUM_BUILD_VERSION); + setTag("CLOUD-DEMO"); + } + + @Override + public void start() { + if (!getEndpoints().isEmpty()) { + super.start(); + } + monitors.start(); + } + + @Override + public void stop() { + if (!getEndpoints().isEmpty()) { + super.stop(); + } + monitors.stop(); + } + + /** + * Initialize demo server. + * + * @param cliArguments command line arguments + * @throws SocketException if an I/O error occurred. + */ + public void initialize(ServerConfig cliArguments) throws SocketException { + Configuration config = getConfig(); + // executors + ScheduledExecutorService secondaryExecutor = ExecutorsUtil + .newDefaultSecondaryScheduler("CoapServer(secondary)#"); + + monitors = new SystemResourceMonitors(secondaryExecutor); + + setupDeviceCredentials(cliArguments); + + if (!cliArguments.noCoap) { + addEndpoints(cliArguments); + + ScheduledExecutorService executor = ExecutorsUtil.newScheduledThreadPool(// + config.get(CoapConfig.PROTOCOL_STAGE_THREAD_COUNT), // + new NamedThreadFactory("CoapServer(main)#")); //$NON-NLS-1$ + addResource(cliArguments, executor); + + setExecutors(executor, secondaryExecutor, false); + + // additional health loggers + setupUdpHealthLogger(secondaryExecutor); + setupObserveHealthLogger(); + } + setupHttpService(cliArguments); + + LOGGER.info("{} initialized.", getTag()); + } + + /** + * Setup device credentials. + * + * Load the private and public key of the DTLS 1.2 server for the device + * communication and the device credentials. + * + * @param cliArguments command line arguments. + */ + public void setupDeviceCredentials(ServerConfig cliArguments) { + PrivateKey privateKey = null; + PublicKey publicKey = null; + if (cliArguments.coaps != null) { + try { + String path = cliArguments.coaps.credentials; + if (!path.endsWith("/")) { + path += "/"; + } + String privateKeyPath = path + DTLS_PRIVATE_KEY; + Credentials credentials = SslContextUtil.loadCredentials(privateKeyPath, null, null, null); + privateKey = credentials.getPrivateKey(); + publicKey = credentials.getPublicKey(); + if (privateKey == null) { + LOGGER.info("PEM credentials {}, missing private key!", privateKeyPath); + } else if (publicKey != null) { + LOGGER.info("PEM credentials {}, public key: ...{}", privateKeyPath, toLog(publicKey)); + } else { + String publicKeyPath = path + DTLS_PUBLIC_KEY; + credentials = SslContextUtil.loadCredentials(publicKeyPath, null, null, null); + publicKey = credentials.getPublicKey(); + if (publicKey != null) { + LOGGER.info("PEM credentials {}, public key: ...{}", publicKeyPath, toLog(publicKey)); + } else { + LOGGER.info("PEM credentials {}, missing public key!", publicKeyPath); + } + } + } catch (IOException e) { + LOGGER.info("Loading PEM credentials failed", e); + } catch (GeneralSecurityException e) { + LOGGER.info("Loading PEM credentials failed", e); + } + } + setupDeviceCredentials(cliArguments, privateKey, publicKey); + } + + /** + * Setup device credentials. + * + * Load the device credentials. + * + * @param cliArguments command line arguments. + * @param privateKey private key for DTLS 1.2 device communication. + * @param publicKey public key for DTLS 1.2 device communication. + */ + public void setupDeviceCredentials(ServerConfig cliArguments, PrivateKey privateKey, PublicKey publicKey) { + if (cliArguments.deviceStore != null) { + long interval = getConfig().get(DEVICE_CREDENTIALS_RELOAD_INTERVAL, TimeUnit.SECONDS); + DeviceParser factory = new DeviceParser(true); + ResourceStore configResource = new ResourceStore<>(factory).setTag("Devices "); + configResource.loadAndCreateMonitor(cliArguments.deviceStore.file, cliArguments.deviceStore.password64, + interval > 0); + monitors.addMonitor("Devices", interval, TimeUnit.SECONDS, configResource.getMonitor()); + deviceCredentials = new DeviceManager(configResource, privateKey, publicKey); + } else { + deviceCredentials = new DeviceManager(null, privateKey, publicKey); + } + } + + /** + * Add CoAP endpoints. + * + * @param cliArguments command line arguments. + */ + public void addEndpoints(ServerConfig cliArguments) { + Configuration config = getConfig(); + int coapsPort = config.get(CoapConfig.COAP_SECURE_PORT); + boolean healthLogger = config.get(SystemConfig.HEALTH_STATUS_INTERVAL, TimeUnit.MILLISECONDS) > 0; + + if (deviceCredentials.getCertificateVerifier() == null && deviceCredentials.getPskStore() == null) { + // no device credentials + LOGGER.warn("Missing device credentials!"); + return; + } + + // Context matcher + EndpointContextMatcher customContextMatcher = null; + if (MatcherMode.PRINCIPAL == config.get(CoapConfig.RESPONSE_MATCHING)) { + customContextMatcher = new PrincipalEndpointContextMatcher(true); + } + + // explore network interfaces + Collection localAddresses; + if (cliArguments.network.wildcard) { + localAddresses = Collections.singleton(new InetSocketAddress(0).getAddress()); + } else { + localAddresses = NetworkInterfacesUtil + .getNetworkInterfaces(cliArguments.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); + AdvancedPskStore pskStore = deviceCredentials.getPskStore(); + if (pskStore != null) { + dtlsConfigBuilder.setAdvancedPskStore(pskStore); + } + CertificateProvider certificateProvider = deviceCredentials.getCertificateProvider(); + if (certificateProvider != null) { + dtlsConfigBuilder.setCertificateIdentityProvider(certificateProvider); + } + NewAdvancedCertificateVerifier certificateVerifier = deviceCredentials.getCertificateVerifier(); + if (certificateVerifier != null) { + dtlsConfigBuilder.setAdvancedCertificateVerifier(certificateVerifier); + } + ApplicationLevelInfoSupplier infoSupplier = deviceCredentials.getInfoSupplier(); + if (infoSupplier != null) { + dtlsConfigBuilder.setApplicationLevelInfoSupplier(infoSupplier); + } + 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"); + } + } + + /** + * Add resources to CoAP server. + * + * @param cliArguments command line arguments. + * @param executor primary executor + */ + public void addResource(ServerConfig cliArguments, ScheduledExecutorService executor) { + // add resources to the server + if (cliArguments.diagnose) { + add(new Diagnose(this)); + } + add(new Devices(getConfig())); + add(new MyContext(MyContext.RESOURCE_NAME, CALIFORNIUM_BUILD_VERSION, false)); + } + + /** + * Setup HTTP service. + * + * @param cliArguments command line arguments. + */ + public void setupHttpService(ServerConfig cliArguments) { + HttpService httpService = HttpService.getHttpService(); + if (httpService != null) { + ForwardHandler forward = new ForwardHandler("devices", "Devices:"); + httpService.createContext("/", forward); + CoapProxyHandler proxy = new CoapProxyHandler(getMessageDeliverer(), httpService.getExecutor()); + httpService.createContext(Devices.RESOURCE_NAME, proxy); + if (cliArguments.diagnose) { + httpService.createContext(Diagnose.RESOURCE_NAME, proxy); + } + } + } + + /** + * Setup UDP health logger. + * + * Generate UDP statistic. + * + * @param secondaryExecutor secondary executor for slow interval jobs + */ + 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)); + } + } + + /** + * Setup observe health logger. + * + * Generate observer-notify statistic. + */ + 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); + } + } + } + + /** + * Setup persistence. + * + * Support DTLS 1.2 graceful restart, + * + * @param store store to keep persisted data + */ + 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..2a15b2c0f3 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/DemoServer.java @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ********************************************************************************/ +package org.eclipse.californium.cloud; + +import java.io.File; + +import org.eclipse.californium.cloud.BaseServer.ServerConfig; +import org.eclipse.californium.cloud.option.TimeOption; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.coap.option.MapBasedOptionRegistry; +import org.eclipse.californium.core.coap.option.StandardOptionRegistry; +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.12 + */ +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) 2024, Contributors to the Eclipse Foundation.", footer = { "", + "Examples:", + " DemoServer --no-loopback", + " (DemoServer listening only on external network interfaces.)", + "", + " DemoServer --store-file dtls.bin --store-max-age 168 \\", + " --store-password64 ZVhiRW5pdkx1RUs2dmVoZg== \\", + " --device-file devices.txt", + "", + " (DemoServer with device credentials from file and dtls-graceful restart.", + " Devices/sessions with no exchange for more then a week (168 hours)", + " are skipped when saving.)", + "", }) + public static class Config extends ServerConfig { + + } + + public static void main(String[] args) { + MapBasedOptionRegistry registry = new MapBasedOptionRegistry(StandardOptionRegistry.getDefaultOptionRegistry(), + TimeOption.DEFINITION, TimeOption.DEPRECATED_DEFINITION); + StandardOptionRegistry.setDefaultOptionRegistry(registry); + 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..ffc4a9d1a2 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/EndpointNetSocketObserver.java @@ -0,0 +1,148 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ********************************************************************************/ +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.12 + */ +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; + } + + /** + * Add endpoint. + * + * Add enpoint's local address to UDP network statistic and forward parts of + * that statistic to the endpoint. + * + * @param endpoint endpoint to add + */ + public void add(Endpoint endpoint) { + InetSocketAddress address = endpoint.getAddress(); + if (netSocketStatistic.add(address, getExternalStatistic(endpoint))) { + LOGGER.debug("added {}", address); + } else { + LOGGER.debug("enabled {}", address); + } + } + + /** + * Remove endpoint. + * + * Remove enpoint's local address from UDP network statistic. + * + * @param endpoint endpoint to remove + */ + 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 to forward the dropped UDP message statistic + * @return external statistic to register for forwarding the dropped UDP + * message statistic + */ + protected SimpleCounterStatistic getExternalStatistic(Endpoint endpoint) { + CounterStatisticManager dtlsStatisticManager = getDtlsStatisticManager(endpoint); + return dtlsStatisticManager != null ? dtlsStatisticManager.getByKey(DtlsHealthLogger.DROPPED_UDP_MESSAGES) + : null; + } + + /** + * Get statistic manager of endpoint. + * + * @param endpoint endpoint + * @return statistic manager of endpoint + */ + 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/ManagementStatistic.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/ManagementStatistic.java new file mode 100755 index 0000000000..4d2e45246a --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/ManagementStatistic.java @@ -0,0 +1,177 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ********************************************************************************/ +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.12 + */ +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/http/HtmlGenerator.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/http/HtmlGenerator.java new file mode 100644 index 0000000000..b3ad2be532 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/http/HtmlGenerator.java @@ -0,0 +1,251 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ********************************************************************************/ +package org.eclipse.californium.cloud.http; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Arrays; +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, device-list, and single page application pages. + * + * @since 3.12 + */ +public class HtmlGenerator { + + /** + * Logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(HtmlGenerator.class); + + /** + * Create forwarding page. + * + * @param link link to forward + * @param title title of forwarding page. + * @return forwarding page. + */ + public static String createForwardPage(String link, String title) { + StringBuilder page = new StringBuilder(); + page.append("\n"); + page.append("\n"); + page.append("\n"); + page.append("\n"); + page.append(""); + page.append("Cloudcoap To S3 proxy\n"); + page.append("").append(title).append("\n"); + page.append("\n"); + page.append("\n"); + page.append("

").append(title).append("

\n"); + page.append(""); + page.append(link); + page.append(":"); + 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 linkAttribute attribute for external-link. If {@code null}, if no + * external-link is used. + * @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 linkAttribute, 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"); + if (!links.isEmpty()) { + String[] root = null; + for (WebLink link : links) { + String uri = link.getURI(); + String[] path = uri.split("/"); + if (root == null) { + root = Arrays.copyOf(path, path.length - 1); + } else { + int last = path.length - 1; + if (last > root.length) { + last = root.length; + } + for (int index = 0; index < last; ++index) { + if (!root[index].equals(path[index])) { + last = index; + break; + } + } + if (last < root.length) { + root = Arrays.copyOf(root, last); + } + } + } + int offset = 0; + if (root != null) { + for (String path : root) { + offset += 1 + path.length(); + } + } + for (WebLink link : links) { + String uri = link.getURI(); + String name = link.getAttributes().getTitle(); + if (name == null) { + name = uri.substring(offset); + name = decodeURL(name); + } + uri = link(pagePath, uri); + if (linkAttribute != null) { + String externalLink = link.getAttributes().getFirstAttributeValue(linkAttribute); + if (externalLink != null) { + if (externalLink.startsWith("/")) { + // sub link + uri += "/"; + externalLink = externalLink.substring(1); + } + // append to uri + uri += encodeURL(externalLink); + } + } + LOGGER.debug("add '{}'#'{}': '{}'", base, uri, name); + page.append(""); + page.append(encodeHtml(name)); + page.append(""); + page.append(": "); + for (String attribute : attributes) { + List values = link.getAttributes().getAttributeValues(attribute); + if (!values.isEmpty()) { + for (String value : values) { + page.append(encodeHtml(value)).append(", "); + } + } + } + page.setLength(page.length() - 2); + 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 decodeURL(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; + } + } + + /** + * URL encode the link. + * + * @param link link to encode + * @return encoded link + */ + public static String encodeURL(String link) { + try { + return URLEncoder.encode(link, CoAP.UTF8_CHARSET.name()); + } catch (UnsupportedEncodingException e) { + // UTF-8 must be supported, + // otherwise many functions will fail + return link; + } + } + + private static final char[] ESCAPE; + + static { + ESCAPE = "\"'<>&".toCharArray(); + Arrays.sort(ESCAPE); + } + + /** + * Encode text for HTML. + * + * @param text text to encode + * @return encoded text + */ + public static String encodeHtml(String text) { + StringBuilder out = new StringBuilder(); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c > 127 || Arrays.binarySearch(ESCAPE, c) >= 0) { + out.append("&#").append((int) c).append(';'); + } else { + out.append(c); + } + } + return out.toString(); + } + +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/http/HttpService.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/http/HttpService.java new file mode 100644 index 0000000000..1173e4b150 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/http/HttpService.java @@ -0,0 +1,832 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ********************************************************************************/ +package org.eclipse.californium.cloud.http; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +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.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; + +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.cloud.util.CredentialsStore; +import org.eclipse.californium.cloud.util.CredentialsStore.Observer; +import org.eclipse.californium.core.WebLink; +import org.eclipse.californium.core.coap.CoAP.ResponseCode; +import org.eclipse.californium.core.coap.CoAP.Type; +import org.eclipse.californium.core.coap.LinkFormat; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.coap.OptionSet; +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.core.server.MessageDeliverer; +import org.eclipse.californium.elements.util.Bytes; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.elements.util.SslContextUtil.Credentials; +import org.eclipse.californium.elements.util.StandardCharsets; +import org.eclipse.californium.elements.util.StringUtil; +import org.eclipse.californium.elements.util.SystemResourceMonitors.SystemResourceMonitor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.sun.net.httpserver.Headers; +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. + * + * Provides either forwarding to external site, a single page application in + * javascript, or a simple http access to data sent via coap. + * + * The simple http access is implemented with a simplified http2coap proxy. It + * supports only very limited conversion from http to coap. Allows only GET + * access to resource "diagnose" and "devices" with sub-resources. + * + * For forwarding and data access no authentication is supported! + * + * @since 3.12 + */ +@SuppressWarnings("restriction") +public class HttpService { + + /** + * Logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(HttpService.class); + + /** + * Logger for fail2ban support. + */ + private final static Logger LOGGER_BAN = LoggerFactory.getLogger("org.eclipse.californium.ban"); + + /** + * TLS any protocol version. + */ + private static final String TLS = "TLS"; + /** + * TLS 1.2 protocol version. + */ + private static final String TLS_1_2 = "TLSv1.2"; + /** + * TLS 1.3 protocol version. + */ + private static final String TLS_1_3 = "TLSv1.3"; + private static final String[] TLS_PROTOCOLS = { TLS_1_3, TLS_1_2 }; + private static final String[] TLS_PROTOCOLS_1_2_ONLY = { TLS_1_2 }; + /** + * 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"; + /** + * Service instance. + */ + private static volatile HttpService httpService; + /** + * Local secure address. + */ + private final InetSocketAddress localSecureAddress; + /** + * Supported TLS protocols. + */ + private final String[] protocols; + /** + * Algorithm for SSLContext. + */ + private final String sslContextAlgorithm; + /** + * Ssl credentials for https. + */ + private final CredentialsStore credentialsStore; + /** + * SSL context for https. + */ + private SSLContext context; + /** + * Current node certificate. + */ + private X509Certificate node; + /** + * Executor for http server. + */ + private volatile ExecutorService executor; + /** + * Https server. + */ + private HttpsServer secureServer; + private volatile boolean started; + /** + * Handler map. + */ + private final Map handlers = new ConcurrentHashMap<>(); + + /** + * Create service. + * + * @param localSecureAddress local address for secure endpoint (https). + * @param credentialsStore credentials + * @param sslContextAlgorithm algorithm for SSL context. + * @param protocols list of TLS protocols + */ + public HttpService(InetSocketAddress localSecureAddress, CredentialsStore credentialsStore, + String sslContextAlgorithm, String[] protocols) { + this.localSecureAddress = localSecureAddress; + this.sslContextAlgorithm = sslContextAlgorithm; + this.protocols = protocols == null ? null : protocols.clone(); + this.credentialsStore = credentialsStore; + applyCredentials(credentialsStore.getCredentials()); + this.credentialsStore.setObserver(new Observer() { + + @Override + public void update(Credentials newCredentials) { + if (newCredentials != null) { + applyCredentials(newCredentials); + } + } + }); + } + + public Executor getExecutor() { + return new Executor() { + + @Override + public void execute(Runnable command) { + Executor serverExecutor = HttpService.this.executor; + if (serverExecutor == null) { + throw new RejectedExecutionException("Target executor missing!"); + } + serverExecutor.execute(command); + } + }; + } + + /** + * Set HTTPS configuration from {@link SSLContext}. + * + * @param sslContext context for HTTPS configuration + */ + public void setHttpsConfigurator(SSLContext sslContext) { + if (secureServer != null && sslContext != null) { + secureServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) { + + @Override + public void configure(HttpsParameters parameters) { + parameters.setWantClientAuth(false); + if (protocols != null) { + try { + boolean useTlsProtocols = false; + String[] clientProtocols = parameters.getProtocols(); + if (clientProtocols == null || clientProtocols.length == 0) { + LOGGER.trace("TLS: protocol info not available!"); + useTlsProtocols = true; + } else { + for (String protocol : clientProtocols) { + LOGGER.trace("TLS: {}", protocol); + if (protocol.equals(TLS_1_2)) { + useTlsProtocols = true; + } + } + } + if (useTlsProtocols) { + parameters.setProtocols(protocols); + for (String protocol : protocols) { + LOGGER.trace("TLS: set {}", protocol); + } + } + } catch (Throwable t) { + LOGGER.error("TLS:", t); + } + } + } + + }); + } else { + if (secureServer == null) { + LOGGER.warn("missing https server!"); + } + if (sslContext == null) { + LOGGER.warn("missing TLS context!"); + } + } + } + + public void createContext(String name, HttpHandler handler) { + String url = name; + if (!url.startsWith("/")) { + url = "/" + url; + } + handlers.put(url, handler); + if (secureServer != null) { + secureServer.createContext(url, handler); + } + } + + public void createFileHandler(String resource, String contentType, boolean reload) { + if (!resource.startsWith("http")) { + // download resource from local server + createContext(resource, new FileHandler(resource, contentType, reload)); + } + } + + /** + * Start https service. + */ + public void start() { + executor = Executors.newCachedThreadPool(); + if (localSecureAddress != null && context != null) { + try { + secureServer = HttpsServer.create(localSecureAddress, 10); + setHttpsConfigurator(context); + secureServer.createContext("/favicon.ico", new FileHandler(null, "image/x-icon", false)); + for (Map.Entry context : handlers.entrySet()) { + secureServer.createContext(context.getKey(), context.getValue()); + } + // Thread control is given to executor service. + secureServer.setExecutor(executor); + secureServer.start(); + started = true; + LOGGER.info("starting {} succeeded!", localSecureAddress); + } catch (IOException ex) { + LOGGER.warn("starting {} failed!", localSecureAddress, ex); + } + } else { + if (localSecureAddress == null) { + LOGGER.warn("missing local address!"); + } + if (context == null) { + LOGGER.warn("missing TLS context!"); + } + } + } + + /** + * Stop https service. + */ + public void stop() { + started = false; + if (secureServer != null) { + // stop with 2s delay + secureServer.stop(2); + secureServer = null; + } + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + } + if (executor != null) { + executor.shutdown(); + executor = null; + } + } + + private void applyCredentials(Credentials newCredentials) { + X509Certificate[] certificateChain = newCredentials.getCertificateChain(); + if (certificateChain != null && certificateChain.length > 0) { + if (node == null || !node.equals(certificateChain[0])) { + PrivateKey privateKey = newCredentials.getPrivateKey(); + if (privateKey != null) { + try { + KeyManager[] keyManager = SslContextUtil.createKeyManager("server", privateKey, + certificateChain); + TrustManager[] trustManager = SslContextUtil.createTrustAllManager(); + SSLContext sslContext = SSLContext.getInstance(sslContextAlgorithm); + sslContext.init(keyManager, trustManager, null); + context = sslContext; + node = certificateChain[0]; + if (started) { + LOGGER.info("restart - certificates reloaded."); + stop(); + start(); + } + } catch (GeneralSecurityException ex) { + LOGGER.warn("creating SSLcontext failed", ex); + } + } else { + LOGGER.debug("private key missing."); + } + } else { + LOGGER.debug("certificates not changed."); + } + } else { + LOGGER.warn("missing certificates."); + } + } + + /** + * Create resource monitor for automatic credentials reloading. + * + * @return created resource monitor + */ + public SystemResourceMonitor getFileMonitor() { + return credentialsStore.getMonitor(); + } + + public static void logHeaders(String title, Headers headers) { + for (String key : headers.keySet()) { + LOGGER.debug("{}: {} {}", title, key, headers.getFirst(key)); + } + } + + /** + * Respond http-request. + * + * @param httpExchange http-exchange with request + * @param httpCode http-response code + * @param contentType response content type. May be {@code null}. + * @param payload response payload. May be {@code null}. + * @throws IOException if an i/o-error occurred + */ + public static void respond(HttpExchange httpExchange, int httpCode, String contentType, byte[] payload) + throws IOException { + try { + long length = payload != null && payload.length > 0 ? payload.length : -1; + if (contentType != null) { + httpExchange.getResponseHeaders().set("Content-Type", contentType); + } + if (httpExchange.getRequestMethod().equals("HEAD")) { + httpExchange.getResponseHeaders().set("content-length", Long.toString(length)); + httpExchange.getResponseHeaders().set("connection", "close"); + length = -1; + } + httpExchange.sendResponseHeaders(httpCode, length); + logHeaders("response", httpExchange.getResponseHeaders()); + if (length > 0) { + try (OutputStream out = httpExchange.getResponseBody()) { + out.write(payload); + } + LOGGER.info("respond {} {} {} bytes", httpExchange.getRequestMethod(), httpCode, length); + } else { + LOGGER.info("respond {} {}", httpExchange.getRequestMethod(), httpCode); + } + } catch (IOException e) { + LOGGER.warn("writing response to {} failed!", httpExchange.getRemoteAddress(), e); + } catch (Throwable e) { + LOGGER.warn("respond to {} failed!", httpExchange.getRemoteAddress(), e); + } + } + + public static void ban(HttpExchange httpExchange, String topic) { + if (LOGGER_BAN.isInfoEnabled()) { + String address = httpExchange.getRemoteAddress().getAddress().getHostAddress(); + String protocol = httpExchange.getProtocol(); + String method = httpExchange.getRequestMethod(); + String uri = httpExchange.getRequestURI().toString(); + LOGGER_BAN.info("https: {} {} {} {} Ban: {}", method, uri, protocol, topic, address); + } + } + + /** + * HTTP handler to forward request. + */ + public static class ForwardHandler implements HttpHandler { + + /** + * External meta-element, intended for automatic forwarding. + */ + private final String forwardLink; + /** + * External section, intended for manual forwarding. + */ + private final String forwardTitle; + + public ForwardHandler(String link, String title) { + this.forwardLink = link; + this.forwardTitle = title; + } + + public void handle(final HttpExchange httpExchange) throws IOException { + final URI uri = httpExchange.getRequestURI(); + LOGGER.info("/request: {} {}", httpExchange.getRequestMethod(), uri); + String method = httpExchange.getRequestMethod(); + String contentType = "text/html; charset=utf-8"; + byte[] payload = null; + int httpCode = 405; + + if (method.equals("GET")) { + String page = HtmlGenerator.createForwardPage(forwardLink, forwardTitle); + httpCode = 200; + payload = page.toString().getBytes(StandardCharsets.UTF_8); + } else if (method.equals("HEAD")) { + } else { + payload = "

405 - Method not allowed!

".getBytes(StandardCharsets.UTF_8); + } + respond(httpExchange, httpCode, contentType, payload); + } + } + + /** + * HTTP handler for files, e.g. favicon.ico or java-script. + */ + public static class FileHandler implements HttpHandler { + + private final String file; + private final String path; + private final String contentType; + private final boolean classpath; + private final boolean reload; + private byte[] data; + + public FileHandler(String file, String contentType, boolean reload) { + this.contentType = contentType; + String path = null; + boolean classpath = false; + if (file != null) { + classpath = file.startsWith(SslContextUtil.CLASSPATH_SCHEME); + if (classpath) { + file = file.substring(SslContextUtil.CLASSPATH_SCHEME.length()); + } else { + path = "src/main/resources/"; + File f = new File(path + file); + if (!f.isFile() || !f.canRead()) { + path = ""; + f = new File(file); + if (!f.isFile() || !f.canRead()) { + InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(file); + if (in != null) { + classpath = true; + try { + in.close(); + } catch (IOException e) { + } + } + } + } + } + } + this.classpath = classpath; + this.file = file; + this.path = path; + if (this.classpath) { + this.reload = false; + } else { + this.reload = reload; + } + load(); + } + + public void load() { + byte[] data = Bytes.EMPTY; + if (file != null && !file.isEmpty()) { + InputStream inStream; + try { + if (classpath) { + inStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(file); + } else { + inStream = new FileInputStream(path + file); + } + 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 file {}", file, ex); + } finally { + try { + inStream.close(); + } catch (IOException e) { + } + } + } + } catch (FileNotFoundException e1) { + LOGGER.info("Failure loading file {}", file, e1); + } + } + this.data = data; + LOGGER.info("{} file {} bytes", contentType, data.length); + } + + public void handle(final HttpExchange httpExchange) throws IOException { + final URI uri = httpExchange.getRequestURI(); + LOGGER.info("file-request: {} {}", httpExchange.getRequestMethod(), uri); + String method = httpExchange.getRequestMethod(); + String contentType = "text/html; charset=utf-8"; + byte[] payload = null; + int httpCode = 405; + if (method.equals("GET")) { + if (reload) { + load(); + } + httpCode = 200; + payload = data; + contentType = this.contentType; + if (reload) { + httpExchange.getResponseHeaders().add("Cache-Control", "no-cache"); + } + } else if (method.equals("HEAD")) { + } else { + payload = "

405 - Method not allowed!

".getBytes(StandardCharsets.UTF_8); + } + respond(httpExchange, httpCode, contentType, payload); + } + } + + /** + * HTTP handler for coap-resource. + */ + public static class CoapProxyHandler implements HttpHandler { + + private final MessageDeliverer messageDeliverer; + private final Executor executor; + private final String[] prefix; + + public CoapProxyHandler(MessageDeliverer messageDeliverer, Executor executor, String... prefix) { + this.messageDeliverer = messageDeliverer; + this.executor = executor; + this.prefix = prefix; + } + + public void fillUri(OptionSet options, String uri) { + String[] prefix = this.prefix; + String[] path = uri.split("/"); + int index = 0; + for (index = 0; index < path.length - 1; ++index) { + String element = path[index + 1]; + if (prefix != null && index < prefix.length) { + String pre = prefix[index]; + if (pre.equals(element)) { + continue; + } + prefix = null; + } + options.addUriPath(element); + } + } + + public boolean checkResourcePath(String path) { + return true; + } + + /** + * Simply transformation of http-get-request into coap-get-request. + */ + @Override + public void handle(final HttpExchange httpExchange) throws IOException { + final URI uri = httpExchange.getRequestURI(); + final String method = httpExchange.getRequestMethod(); + final Headers headers = httpExchange.getRequestHeaders(); + Request request = null; + + LOGGER.info("http-request: {} {}", method, uri); + logHeaders("request", headers); + + if (method.equals("GET") || method.equals("HEAD")) { + request = Request.newGet(); + } else { + // use ping to fail ... + request = Request.newPing(); + } + fillUri(request.getOptions(), uri.getPath()); + String coapPath = "/" + request.getOptions().getUriPathString(); + if (checkResourcePath(coapPath)) { + 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(); + if (response.getType() == null) { + Type reqType = request.getType(); + if (request.acknowledge()) { + response.setType(Type.ACK); + } else { + response.setType(reqType); + } + } + request.setResponse(response); + respond(httpExchange, response); + } + }; + messageDeliverer.deliverRequest(coapExchange); + } else { + int httpCode = 404; + String contentType = "text/html; charset=utf-8"; + byte[] payload = "

404 - Not found!

".getBytes(StandardCharsets.UTF_8); + respond(httpExchange, httpCode, contentType, payload); + updateBan(httpExchange); + } + } + + public boolean respond(HttpExchange httpExchange, Response response) { + LOGGER.info("CoAP response: {}", response); + final URI uri = httpExchange.getRequestURI(); + String contentType = "text/html; charset=utf-8"; + byte[] payload = response.getPayload(); + int httpCode = 200; + if (response.isSuccess()) { + 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); + } else { + contentType = "text/plain; charset=utf-8"; + } + List tags = response.getOptions().getETags(); + if (!tags.isEmpty()) { + byte[] etag = tags.get(0); + httpExchange.getResponseHeaders().set("ETag", StringUtil.byteArray2Hex(etag)); + } + } 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; + } + } + try { + respond(httpExchange, httpCode, contentType, payload); + LOGGER.info("HTTP returned {}", response); + } catch (IOException e) { + LOGGER.warn("write response to {} failed!", httpExchange.getRemoteAddress(), e); + } + updateBan(httpExchange); + return true; + } + + public void respond(HttpExchange httpExchange, int httpCode, String contentType, byte[] payload) + throws IOException { + HttpService.respond(httpExchange, httpCode, contentType, payload); + } + + public boolean updateBan(final HttpExchange httpExchange) { + return true; + } + } + + /** + * Create {@link CredentialsStore} 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 + * @param password64 base64 encoded password of credentials. May be + * {@code null}, if credentials are not encrypted. + * @return created credentials store + * @throws IOException if an i/o error occurs + * @throws GeneralSecurityException if an encryption error occurs. + */ + public static CredentialsStore loadCredentials(String credentialsPath, String password64) + throws IOException, GeneralSecurityException { + if (credentialsPath.endsWith("/")) { + credentialsPath = credentialsPath.substring(0, credentialsPath.length() - 1); + } + String privateKey = credentialsPath + "/" + HTTPS_PRIVATE_KEY; + String fullChain = credentialsPath + "/" + HTTPS_FULL_CHAIN; + File directory = new File(credentialsPath); + if (!directory.exists()) { + LOGGER.error("Missing directory {} for https credentials!", credentialsPath); + } else { + File file = new File(fullChain); + if (!file.exists()) { + LOGGER.error("Missing https full-chain {}!", fullChain); + } else if (!file.canRead()) { + LOGGER.error("Missing read permission for https full-chain {}!", fullChain); + } + file = new File(privateKey); + if (!file.exists()) { + LOGGER.error("Missing https private-key {}!", privateKey); + } else if (!file.canRead()) { + LOGGER.error("Missing read permission for https private-key {}!", privateKey); + } + } + CredentialsStore credentialsStore = new CredentialsStore() { + + /** + * {@inheritDoc} + * + * Add check for certificate chain. + */ + @Override + protected boolean complete(Credentials newCredentials) { + return super.complete(newCredentials) && newCredentials.getCertificateChain() != null; + } + }; + credentialsStore.setTag("https "); + credentialsStore.loadAndCreateMonitor(fullChain, privateKey, password64, true); + return credentialsStore; + } + + public static HttpService getHttpService() { + return httpService; + } + + /** + * Create http service. + * + * @param httpsPort server poet + * @param credentialsPath server credentials + * @param password64 base64 encoded password of credentials. May be + * {@code null}, if credentials are not encrypted. + * @param tls12Only use TLS 1.2 only + * @return {@code true} on success, {@code false} otherwise. + */ + public static boolean createHttpService(int httpsPort, String credentialsPath, String password64, + boolean tls12Only) { + try { + CredentialsStore credentials = loadCredentials(credentialsPath, password64); + HttpService service = new HttpService(new InetSocketAddress(httpsPort), credentials, + tls12Only ? TLS_1_2 : TLS, tls12Only ? TLS_PROTOCOLS_1_2_ONLY : TLS_PROTOCOLS); + httpService = service; + return true; + } catch (IOException e) { + LOGGER.error("I/O error", e); + } catch (GeneralSecurityException e) { + LOGGER.error("Crypto error", e); + } + return false; + } + + public static boolean startHttpService() { + HttpService service = httpService; + if (service != null) { + service.start(); + return true; + } else { + LOGGER.error("HTTP service missing!"); + return false; + } + } + + public static boolean stopHttpService() { + HttpService service = httpService; + if (service != null) { + service.stop(); + return true; + } else { + LOGGER.error("HTTP service missing!"); + return false; + } + } +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/ReadEtagOption.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/ReadEtagOption.java new file mode 100644 index 0000000000..39ca6ea740 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/ReadEtagOption.java @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ********************************************************************************/ +package org.eclipse.californium.cloud.option; + +import org.eclipse.californium.core.coap.Option; +import org.eclipse.californium.core.coap.option.OpaqueOptionDefinition; + +/** + * CoAP custom option for etag of combined read. + * + * @since 3.12 + */ +public class ReadEtagOption extends Option { + + /** + * Number of custom option. + */ + public static final int COAP_OPTION_READ_ETAG = 0xfdec; + + public static final OpaqueOptionDefinition DEFINITION = new OpaqueOptionDefinition(COAP_OPTION_READ_ETAG, "Read_Etag", false, 1, 8) { + + @Override + public Option create(byte[] value) { + return new ReadEtagOption(value); + } + + }; + + /** + * Create etag option for combined read. + * + * @param etag etag of combined read + */ + public ReadEtagOption(byte[] etag) { + super(DEFINITION, etag); + } + +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/ReadResponseOption.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/ReadResponseOption.java new file mode 100644 index 0000000000..e6f43d9fc6 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/ReadResponseOption.java @@ -0,0 +1,76 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ********************************************************************************/ +package org.eclipse.californium.cloud.option; + +import org.eclipse.californium.core.coap.CoAP.ResponseCode; +import org.eclipse.californium.core.coap.MessageFormatException; +import org.eclipse.californium.core.coap.option.IntegerOptionDefinition; +import org.eclipse.californium.core.coap.Option; + +/** + * CoAP custom option for response code of combined read. + * + * @since 3.12 + */ +public class ReadResponseOption extends Option { + + /** + * Number of custom option. + */ + public static final int COAP_OPTION_READ_RESPONSE = 0xfdf0; + + public static final IntegerOptionDefinition DEFINITION = new IntegerOptionDefinition(COAP_OPTION_READ_RESPONSE, + "Read_ResponseCode", true, 1, 1) { + + @Override + public Option create(byte[] value) { + return new ReadResponseOption(value); + } + + @Override + public void assertValue(byte[] value) { + int code = value[0] & 0xff; + try { + ResponseCode.valueOf(code); + } catch (MessageFormatException ex) { + throw new IllegalArgumentException(ex.getMessage() + " Value " + value); + } + } + + }; + + /** + * Create response code option for combined read. + * + * @param code response code + */ + public ReadResponseOption(int code) { + super(DEFINITION, code); + } + + /** + * Create response code option for combined read. + * + * @param code response code + */ + public ReadResponseOption(ResponseCode code) { + this(code.value); + } + + public ReadResponseOption(byte[] value) { + super(DEFINITION, value); + } + +} diff --git a/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/TimeOption.java b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/TimeOption.java new file mode 100644 index 0000000000..37b3e33a32 --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/TimeOption.java @@ -0,0 +1,179 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ********************************************************************************/ +package org.eclipse.californium.cloud.option; + +import java.util.concurrent.TimeUnit; + +import org.eclipse.californium.core.coap.Message; +import org.eclipse.californium.core.coap.Option; +import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.coap.option.IntegerOptionDefinition; +import org.eclipse.californium.elements.util.ClockUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * CoAP custom time option. + * + * Used in {@link Request} to indicate the client's system-time in milliseconds. + * If the used value is {@code 0} or differs from the system-time of the server + * more than {@link #MAX_MILLISECONDS_DELTA}, the server adds also a + * {@link TimeOption} to the {@link Response} with the server's system-time in + * milliseconds. + * + * @since 3.12 + */ +public class TimeOption extends Option { + + /** + * Logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(TimeOption.class); + + /** + * Number of custom option. + */ + public static final int COAP_OPTION_TIME = 0xfde8; + + /** + * Maximum delta in milliseconds. If exceeded, {@link #adjust()} returns the + * {@link TimeOption} to be added to the {@link Response}. + */ + public static final long MAX_MILLISECONDS_DELTA = 5000; + + public static final IntegerOptionDefinition DEFINITION = new IntegerOptionDefinition(COAP_OPTION_TIME, "Time", + true) { + + @Override + public Option create(byte[] value) { + return new TimeOption(value); + } + + }; + + /** + * Adjust value, if times differ more than {@link #MAX_MILLISECONDS_DELTA}. + * + * @see #adjust() + */ + private boolean adjustTime; + + /** + * Create time option with current system time. + */ + public TimeOption() { + this(System.currentTimeMillis()); + } + + /** + * Create time option. + * + * @param time time in system milliseconds. + */ + public TimeOption(long time) { + super(DEFINITION, time); + } + + /** + * Create time option. + * + * @param value time in system milliseconds as byte array. + */ + public TimeOption(byte[] value) { + super(DEFINITION, value); + } + + /** + * Get time option to adjust the device time. + * + * Intended to be included in the response message, if indicated. + * + * @return time option to adjust the device time, {@code null}, if the + * device time is already set. + */ + public TimeOption adjust() { + return adjustTime ? new TimeOption((IntegerOptionDefinition) getDefinition()) : null; + } + + /** + * Get time option from message or clock. + * + * If message contains custom time option, return that. Otherwise use the + * system receive time to create a time option to return. + * + * @param message message with custom time option + * @return the time option + */ + public static TimeOption getMessageTime(Message message) { + long delta = ClockUtil.delta(message.getNanoTimestamp(), TimeUnit.MILLISECONDS); + long receiveTime = System.currentTimeMillis() - delta; + Option option = message.getOptions().getOtherOption(DEFINITION); + if (option == null) { + option = message.getOptions().getOtherOption(DEPRECATED_DEFINITION); + } + if (option != null) { + TimeOption time; + IntegerOptionDefinition definition = (IntegerOptionDefinition) option.getDefinition(); + long value = option.getLongValue(); + if (value == 0) { + time = new TimeOption(definition, receiveTime); + time.adjustTime = true; + LOGGER.info("Time: send initial time"); + } else { + time = (TimeOption) option; + delta = value - receiveTime; + if (Math.abs(delta) > MAX_MILLISECONDS_DELTA) { + // difference > 5s => send time fix back. + time.adjustTime = true; + LOGGER.info("Time: {}ms delta => send fix", delta); + } else { + LOGGER.debug("Time: {}ms delta", delta); + } + } + return time; + } else { + LOGGER.debug("Time: localtime"); + return new TimeOption(receiveTime); + } + } + + /** + * Number of custom option. + */ + private static final int DEPRECATED_COAP_OPTION_TIME = 0xff3c; + + public static final IntegerOptionDefinition DEPRECATED_DEFINITION = new IntegerOptionDefinition( + DEPRECATED_COAP_OPTION_TIME, "_Time", true) { + + @Override + public Option create(byte[] value) { + return new TimeOption(DEPRECATED_DEFINITION, value); + } + + }; + + private TimeOption(IntegerOptionDefinition definition) { + super(definition, System.currentTimeMillis()); + } + + private TimeOption(IntegerOptionDefinition definition, long value) { + super(definition, value); + } + + private TimeOption(IntegerOptionDefinition definition, byte[] value) { + super(definition, value); + } +} 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..cd7cd3944c --- /dev/null +++ b/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/resources/Devices.java @@ -0,0 +1,525 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ********************************************************************************/ +package org.eclipse.californium.cloud.resources; + +import static org.eclipse.californium.core.coap.CoAP.ResponseCode.BAD_OPTION; +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 java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +import org.eclipse.californium.cloud.BaseServer; +import org.eclipse.californium.cloud.option.ReadEtagOption; +import org.eclipse.californium.cloud.option.ReadResponseOption; +import org.eclipse.californium.cloud.option.TimeOption; +import org.eclipse.californium.cloud.util.DeviceManager; +import org.eclipse.californium.cloud.util.DeviceManager.DeviceInfo; +import org.eclipse.californium.core.CoapResource; +import org.eclipse.californium.core.WebLink; +import org.eclipse.californium.core.coap.LinkFormat; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.coap.Option; +import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.coap.UriQueryParameter; +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.config.Configuration; +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 device name from + * the additional info of the principal as name of the sub-resource. e.g.: + *

+ * + * + * coaps://${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 + * and returns a list of web-links for the current devices. + *

+ * + *

+ * Supported query parameter: + *

+ * + *
+ *
{@value #URI_QUERY_OPTION_READ}
+ *
Sub resource for combined read. Default argument "config".
+ *
{@value #URI_QUERY_OPTION_SERIES}
+ *
Use sub resource "series" to keep track of parts of the message.
+ *
+ * + * Default argument only applies, if the parameter is provided, but without + * argument. + * + *

+ * Supported custom options: + *

+ * + *
+ *
{@link TimeOption}, {@value TimeOption#COAP_OPTION_TIME}
+ *
Time synchronization.
+ *
{@link ReadResponseOption}, + * {@value ReadResponseOption#COAP_OPTION_READ_RESPONSE}
+ *
Response code of combined read request. See query parameter + * {@value #URI_QUERY_OPTION_READ}
+ *
{@link ReadEtagOption}, + * {@value ReadEtagOption#COAP_OPTION_READ_ETAG}
+ *
ETAG of combined read request. See query parameter + * {@value #URI_QUERY_OPTION_READ}
+ *
+ * + * Example: + * + * + * coaps://${host}/devices?read" POST "Temperature: 25.4°" by principal "dev-1200045" + * + * + *

+ * results in a resource: + *

+ * + * + * "/devices/dev-1200045" with content "Temperature: 25.4°". + * + * + *

+ * + * and returns the content of + *

+ * + * + * "/devices/dev-1200045/config". + * + * + *

+ * (Default for "read" argument is "config".) + *

+ * + * @since 3.12 + */ +public class Devices extends CoapResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(Devices.class); + private static final Logger LOGGER_TRACKER = LoggerFactory.getLogger("org.eclipse.californium.gnss.tracker"); + + private static final SimpleDateFormat ISO_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + public static final int SERIES_MAX_SIZE = 32 * 1024; + + public static final String RESOURCE_NAME = "devices"; + public static final String SUB_RESOURCE_NAME = "series"; + + public static final String DEFAULT_READ_SUB_RESOURCE_NAME = "config"; + + public static final String ATTRIBUTE_TIME = "time"; + public static final String ATTRIBUTE_POSITION = "pos"; + + /** + * URI query parameter to specify a sub-resource to read. + */ + public static final String URI_QUERY_OPTION_READ = "read"; + /** + * URI query parameter to append some lines to a series-resource. + */ + public static final String URI_QUERY_OPTION_SERIES = "series"; + /** + * Supported query parameter. + */ + private static final List SUPPORTED = Arrays.asList(URI_QUERY_OPTION_READ, URI_QUERY_OPTION_SERIES); + + private final LeastRecentlyUpdatedCache keptPosts; + + private final int[] CONTENT_TYPES = { TEXT_PLAIN, APPLICATION_OCTET_STREAM, APPLICATION_JSON, APPLICATION_CBOR, + APPLICATION_XML, APPLICATION_JAVASCRIPT, APPLICATION_LINK_FORMAT }; + + /** + * Create device resource. + * + * @param config configuration + */ + public Devices(Configuration config) { + super(RESOURCE_NAME); + Arrays.sort(CONTENT_TYPES); + getAttributes().setTitle("Resource, which keeps track of POSTing devices."); + getAttributes().addContentTypes(CONTENT_TYPES); + long minutes = config.get(BaseServer.CACHE_STALE_DEVICE_THRESHOLD, TimeUnit.MINUTES); + int maxDevices = config.get(BaseServer.CACHE_MAX_DEVICES); + int minDevices = maxDevices / 10; + if (minDevices < 100) { + minDevices = maxDevices; + } + keptPosts = new LeastRecentlyUpdatedCache<>(minDevices, maxDevices, minutes, TimeUnit.MINUTES); + } + + @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(BAD_OPTION, "only one search query is supported!", 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(); + if (request == null) { + throw new NullPointerException("request must not be null!"); + } + + int format = request.getOptions().getContentFormat(); + if (format != UNDEFINED && Arrays.binarySearch(CONTENT_TYPES, format) < 0) { + Response response = new Response(NOT_ACCEPTABLE); + exchange.respond(response); + return; + } + + boolean updateSeries = false; + String read = null; + try { + UriQueryParameter helper = request.getOptions().getUriQueryParameter(SUPPORTED); + LOGGER.info("URI-Query: {}", request.getOptions().getUriQuery()); + List