From 6dd549359ff5ad048d3c12ea1abcf4225fa7b5a2 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 12 Jan 2024 12:46:52 +0100 Subject: [PATCH] Move network related code into its own file. (#458) --- src/jaguar.toit | 211 +---------------------------------------------- src/network.toit | 210 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 209 deletions(-) create mode 100644 src/network.toit diff --git a/src/jaguar.toit b/src/jaguar.toit index ea9656ff..45e94da2 100644 --- a/src/jaguar.toit +++ b/src/jaguar.toit @@ -4,14 +4,10 @@ import http import log -import net -import net.udp -import net.tcp import reader import uuid import monitor -import encoding.ubjson import encoding.tison import system @@ -20,26 +16,12 @@ import system.containers import system.firmware import .container-registry - -HTTP-PORT ::= 9000 -IDENTIFY-PORT ::= 1990 -IDENTIFY-ADDRESS ::= net.IpAddress.parse "255.255.255.255" -STATUS-OK-JSON ::= """{ "status": "OK" }""" - -HEADER-DEVICE-ID ::= "X-Jaguar-Device-ID" -HEADER-SDK-VERSION ::= "X-Jaguar-SDK-Version" -HEADER-DISABLED ::= "X-Jaguar-Disabled" -HEADER-CONTAINER-NAME ::= "X-Jaguar-Container-Name" -HEADER-CONTAINER-TIMEOUT ::= "X-Jaguar-Container-Timeout" +import .network // Defines recognized by Jaguar for /run and /install requests. JAG-DISABLED ::= "jag.disabled" JAG-TIMEOUT ::= "jag.timeout" -// Assets for the mini-webpage that the device serves up on $HTTP_PORT. -CHIP-IMAGE ::= "https://toitlang.github.io/jaguar/device-files/chip.svg" -STYLE-CSS ::= "https://toitlang.github.io/jaguar/device-files/style.css" - logger ::= log.Logger log.INFO-LEVEL log.DefaultTarget --name="jaguar" flash-mutex ::= monitor.Mutex @@ -174,34 +156,7 @@ class Device: --chip=chip or "unknown" run device/Device: - network ::= net.open - socket/tcp.ServerSocket? := null - try: - socket = network.tcp-listen device.port - address := "http://$network.address:$socket.local-address.port" - logger.info "running Jaguar device '$device.name' (id: '$device.id') on '$address'" - - // We've successfully connected to the network, so we consider - // the current firmware functional. Go ahead and validate the - // firmware if requested to do so. - if firmware-is-validation-pending: - if firmware.validate: - logger.info "firmware update validated after connecting to network" - firmware-is-validation-pending = false - else: - logger.error "firmware update failed to validate" - - // We run two tasks concurrently: One broadcasts the device identity - // via UDP and one serves incoming HTTP requests. We run the tasks - // in a group so if one of them terminates, we take the other one down - // and clean up nicely. - Task.group --required=1 [ - :: broadcast-identity network device address, - :: serve-incoming-requests socket device address, - ] - finally: - if socket: socket.close - network.close + run-network device flash-image image-size/int reader/reader.Reader name/string? defines/Map -> uuid.Uuid: with-timeout --ms=120_000: flash-mutex.do: @@ -319,165 +274,3 @@ install-firmware firmware-size/int reader/reader.Reader -> none: logger.info "installed firmware; ready to update on chip reset" finally: writer.close - -identity-payload device/Device address/string -> ByteArray: - identity := """ - { "method": "jaguar.identify", - "payload": { - "name": "$device.name", - "id": "$device.id", - "chip": "$device.chip", - "sdkVersion": "$system.vm-sdk-version", - "address": "$address", - "wordSize": $system.BYTES-PER-WORD - } - } - """ - return identity.to-byte-array - -broadcast-identity network/net.Interface device/Device address/string -> none: - payload ::= identity-payload device address - datagram ::= udp.Datagram - payload - net.SocketAddress IDENTIFY-ADDRESS IDENTIFY-PORT - socket := network.udp-open - try: - socket.broadcast = true - while not network.is-closed: - socket.send datagram - sleep --ms=200 - finally: - socket.close - -handle-browser-request name/string request/http.Request writer/http.ResponseWriter -> none: - path := request.path - if path == "/": path = "index.html" - if path.starts-with "/": path = path[1..] - - if path == "index.html": - uptime ::= Duration --s=Time.monotonic-us / Duration.MICROSECONDS-PER-SECOND - - writer.headers.set "Content-Type" "text/html" - writer.write """ - - - - $name (Jaguar device) - - -
-
- Picture of an embedded device -
-

$name

-

Jaguar device

-

-
-

Uptime

-

$uptime

-

SDK

-

$system.vm-sdk-version

-
-

-

Run code on this device using

- > jag run -d $name hello.toit -

Monitor the serial port console using

-

> jag monitor

-
- - - """ - else if path == "favicon.ico": - writer.redirect http.STATUS-FOUND CHIP-IMAGE - else: - writer.headers.set "Content-Type" "text/plain" - writer.write-headers http.STATUS-NOT-FOUND - writer.write "Not found: $path" - -serve-incoming-requests socket/tcp.ServerSocket device/Device address/string -> none: - self := Task.current - - server := http.Server --logger=logger --read-timeout=(Duration --s=3) - - server.listen socket:: | request/http.Request writer/http.ResponseWriter | - headers ::= request.headers - device-id := "$device.id" - device-id-header := headers.single HEADER-DEVICE-ID - sdk-version-header := headers.single HEADER-SDK-VERSION - path := request.path - - // Handle identification requests before validation, as the caller doesn't know that information yet. - if path == "/identify" and request.method == http.GET: - writer.headers.set "Content-Type" "application/json" - result := identity-payload device address - writer.headers.set "Content-Length" result.size.stringify - writer.write result - - else if path == "/" or path.ends-with ".html" or path.ends-with ".css" or path.ends-with ".ico": - handle-browser-request device.name request writer - - // Validate device ID. - else if device-id-header != device-id: - logger.info "denied request, header: '$HEADER-DEVICE-ID' was '$device-id-header' not '$device-id'" - writer.write-headers http.STATUS-FORBIDDEN --message="Device has id '$device-id', jag is trying to talk to '$device-id-header'" - - // Handle pings. - else if path == "/ping" and request.method == http.GET: - respond-ok writer - - // Handle listing containers. - else if path == "/list" and request.method == http.GET: - result := ubjson.encode registry_.entries - writer.headers.set "Content-Type" "application/ubjson" - writer.headers.set "Content-Length" result.size.stringify - writer.write result - - // Handle uninstalling containers. - else if path == "/uninstall" and request.method == http.PUT: - container-name ::= headers.single HEADER-CONTAINER-NAME - uninstall-image container-name - respond-ok writer - - // Handle firmware updates. - else if path == "/firmware" and request.method == http.PUT: - install-firmware request.content-length request.body - respond-ok writer - // Mark the firmware as having a pending upgrade and close - // the server socket to force the HTTP server loop to stop. - firmware-is-upgrade-pending = true - socket.close - - // Validate SDK version before attempting to install containers or run code. - else if sdk-version-header != system.vm-sdk-version: - logger.info "denied request, header: '$HEADER-SDK-VERSION' was '$sdk-version-header' not '$system.vm-sdk-version'" - writer.write-headers http.STATUS-NOT-ACCEPTABLE --message="Device has $system.vm-sdk-version, jag has $sdk-version-header" - - // Handle installing containers. - else if path == "/install" and request.method == "PUT": - container-name ::= headers.single HEADER-CONTAINER-NAME - defines ::= extract-defines headers - install-image request.content-length request.body container-name defines - respond-ok writer - - // Handle code running. - else if path == "/run" and request.method == "PUT": - defines ::= extract-defines headers - run-code request.content-length request.body defines - respond-ok writer - // If the code needs to run with Jaguar disabled, we close - // the server socket to force the HTTP server loop to stop. - if disabled: socket.close - -extract-defines headers/http.Headers -> Map: - defines := {:} - if headers.single HEADER-DISABLED: - defines[JAG-DISABLED] = true - if header := headers.single HEADER-CONTAINER-TIMEOUT: - timeout := int.parse header --on-error=: null - if timeout: defines[JAG-TIMEOUT] = timeout - return defines - -respond-ok writer/http.ResponseWriter -> none: - writer.headers.set "Content-Type" "application/json" - writer.headers.set "Content-Length" STATUS-OK-JSON.size.stringify - writer.write STATUS-OK-JSON diff --git a/src/network.toit b/src/network.toit new file mode 100644 index 00000000..0d91100a --- /dev/null +++ b/src/network.toit @@ -0,0 +1,210 @@ +// Copyright (C) 2024 Toitware ApS. All rights reserved. +// Use of this source code is governed by an MIT-style license that can be +// found in the LICENSE file. + +import encoding.ubjson +import http +import net +import net.udp +import net.tcp +import system +import system.firmware + +import .jaguar + +HTTP-PORT ::= 9000 +IDENTIFY-PORT ::= 1990 +IDENTIFY-ADDRESS ::= net.IpAddress.parse "255.255.255.255" +STATUS-OK-JSON ::= """{ "status": "OK" }""" + +HEADER-DEVICE-ID ::= "X-Jaguar-Device-ID" +HEADER-SDK-VERSION ::= "X-Jaguar-SDK-Version" +HEADER-DISABLED ::= "X-Jaguar-Disabled" +HEADER-CONTAINER-NAME ::= "X-Jaguar-Container-Name" +HEADER-CONTAINER-TIMEOUT ::= "X-Jaguar-Container-Timeout" + +// Assets for the mini-webpage that the device serves up on $HTTP_PORT. +CHIP-IMAGE ::= "https://toitlang.github.io/jaguar/device-files/chip.svg" +STYLE-CSS ::= "https://toitlang.github.io/jaguar/device-files/style.css" + +run-network device/Device: + network ::= net.open + socket/tcp.ServerSocket? := null + try: + socket = network.tcp-listen device.port + address := "http://$network.address:$socket.local-address.port" + logger.info "running Jaguar device '$device.name' (id: '$device.id') on '$address'" + + // We run two tasks concurrently: One broadcasts the device identity + // via UDP and one serves incoming HTTP requests. We run the tasks + // in a group so if one of them terminates, we take the other one down + // and clean up nicely. + Task.group --required=1 [ + :: broadcast-identity network device address, + :: serve-incoming-requests socket device address, + ] + finally: + if socket: socket.close + network.close + +identity-payload device/Device address/string -> ByteArray: + identity := """ + { "method": "jaguar.identify", + "payload": { + "name": "$device.name", + "id": "$device.id", + "chip": "$device.chip", + "sdkVersion": "$system.vm-sdk-version", + "address": "$address", + "wordSize": $system.BYTES-PER-WORD + } + } + """ + return identity.to-byte-array + +broadcast-identity network/net.Interface device/Device address/string -> none: + payload ::= identity-payload device address + datagram ::= udp.Datagram + payload + net.SocketAddress IDENTIFY-ADDRESS IDENTIFY-PORT + socket := network.udp-open + try: + socket.broadcast = true + while not network.is-closed: + socket.send datagram + sleep --ms=200 + finally: + socket.close + +handle-browser-request name/string request/http.Request writer/http.ResponseWriter -> none: + path := request.path + if path == "/": path = "index.html" + if path.starts-with "/": path = path[1..] + + if path == "index.html": + uptime ::= Duration --s=Time.monotonic-us / Duration.MICROSECONDS-PER-SECOND + + writer.headers.set "Content-Type" "text/html" + writer.write """ + + + + $name (Jaguar device) + + +
+
+ Picture of an embedded device +
+

$name

+

Jaguar device

+

+
+

Uptime

+

$uptime

+

SDK

+

$system.vm-sdk-version

+
+

+

Run code on this device using

+ > jag run -d $name hello.toit +

Monitor the serial port console using

+

> jag monitor

+
+ + + """ + else if path == "favicon.ico": + writer.redirect http.STATUS-FOUND CHIP-IMAGE + else: + writer.headers.set "Content-Type" "text/plain" + writer.write-headers http.STATUS-NOT-FOUND + writer.write "Not found: $path" + +serve-incoming-requests socket/tcp.ServerSocket device/Device address/string -> none: + self := Task.current + + server := http.Server --logger=logger --read-timeout=(Duration --s=3) + + server.listen socket:: | request/http.Request writer/http.ResponseWriter | + headers ::= request.headers + device-id := "$device.id" + device-id-header := headers.single HEADER-DEVICE-ID + sdk-version-header := headers.single HEADER-SDK-VERSION + path := request.path + + // Handle identification requests before validation, as the caller doesn't know that information yet. + if path == "/identify" and request.method == http.GET: + writer.headers.set "Content-Type" "application/json" + result := identity-payload device address + writer.headers.set "Content-Length" result.size.stringify + writer.write result + + else if path == "/" or path.ends-with ".html" or path.ends-with ".css" or path.ends-with ".ico": + handle-browser-request device.name request writer + + // Validate device ID. + else if device-id-header != device-id: + logger.info "denied request, header: '$HEADER-DEVICE-ID' was '$device-id-header' not '$device-id'" + writer.write-headers http.STATUS-FORBIDDEN --message="Device has id '$device-id', jag is trying to talk to '$device-id-header'" + + // Handle pings. + else if path == "/ping" and request.method == http.GET: + respond-ok writer + + // Handle listing containers. + else if path == "/list" and request.method == http.GET: + result := ubjson.encode registry_.entries + writer.headers.set "Content-Type" "application/ubjson" + writer.headers.set "Content-Length" result.size.stringify + writer.write result + + // Handle uninstalling containers. + else if path == "/uninstall" and request.method == http.PUT: + container-name ::= headers.single HEADER-CONTAINER-NAME + uninstall-image container-name + respond-ok writer + + // Handle firmware updates. + else if path == "/firmware" and request.method == http.PUT: + install-firmware request.content-length request.body + respond-ok writer + // Mark the firmware as having a pending upgrade and close + // the server socket to force the HTTP server loop to stop. + firmware-is-upgrade-pending = true + socket.close + + // Validate SDK version before attempting to install containers or run code. + else if sdk-version-header != system.vm-sdk-version: + logger.info "denied request, header: '$HEADER-SDK-VERSION' was '$sdk-version-header' not '$system.vm-sdk-version'" + writer.write-headers http.STATUS-NOT-ACCEPTABLE --message="Device has $system.vm-sdk-version, jag has $sdk-version-header" + + // Handle installing containers. + else if path == "/install" and request.method == "PUT": + container-name ::= headers.single HEADER-CONTAINER-NAME + defines ::= extract-defines headers + install-image request.content-length request.body container-name defines + respond-ok writer + + // Handle code running. + else if path == "/run" and request.method == "PUT": + defines ::= extract-defines headers + run-code request.content-length request.body defines + respond-ok writer + // If the code needs to run with Jaguar disabled, we close + // the server socket to force the HTTP server loop to stop. + if disabled: socket.close + +extract-defines headers/http.Headers -> Map: + defines := {:} + if headers.single HEADER-DISABLED: + defines[JAG-DISABLED] = true + if header := headers.single HEADER-CONTAINER-TIMEOUT: + timeout := int.parse header --on-error=: null + if timeout: defines[JAG-TIMEOUT] = timeout + return defines + +respond-ok writer/http.ResponseWriter -> none: + writer.headers.set "Content-Type" "application/json" + writer.headers.set "Content-Length" STATUS-OK-JSON.size.stringify + writer.write STATUS-OK-JSON