verdigris 0.2.0

Browser application to explore, learn and debug CoAP
//! A GUI component that runs a CoAP server, registers it at a given RD for reverse proxying, and
//! displays a single color on a large area.
//!
//! (Think "Turning your browser into a CoAP confiurable RGB spot")

use core::convert::TryInto;
use yew::prelude::*;

use coap_message::{MinimalWritableMessage, ReadableMessage};
use coap_handler_implementations::option_processing::OptionsExt;

use crate::{coapws, coapwsmessage};

/// Lifetime of registrations. Chosen low as the default aiocoap based RD neither has support for
/// infinite lifetimes, nor cancels proxy-based registrations that become inactive the moment the
/// connection is dropped.
static LT: core::time::Duration = core::time::Duration::from_secs(60);

pub struct ColorServer {
    props: Props,
    connection: Box<dyn Bridge<coapws::Connection>>,
    link: ComponentLink<Self>,
    server_uri: String,
    health: Health,
    ep: String,
    color: String,
    _timer: yew::services::interval::IntervalTask,
}

#[derive(Debug)]
pub enum Msg {
    Socket(coapws::Output),
    SetServer(String),
    SetEP(String),
    Connect,
}

#[derive(Properties, Clone)]
pub struct Props {
    pub ep: Option<String>,
    pub rd: Option<String>,
    pub onchange: Callback<(String, String)>,
}

#[derive(Copy, Clone)]
enum Health {
    Connecting,
    Registering,
    Failed,
    Registered,
}

impl core::fmt::Display for Health {
    fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
        f.write_str(match self {
            Health::Connecting => "Connecting",
            Health::Registering => "Registering",
            Health::Failed => "Failed",
            Health::Registered => "Registered",
        })
    }
}

/// Typechecked color value that's always shaped "#xxxxxx" for hex digits x
///
/// Odd detail: This is not normalized, so a user can store secret information on the server by
/// encoding data in the capitalization of hex digits.
struct Color {
    value: String
}

impl core::convert::TryFrom<&str> for Color {
    type Error = ();

    fn try_from(input: &str) -> Result<Self, ()> {
        if input.len() == 7 && input.chars().all(|c| c.is_ascii_hexdigit()) {
            Ok(Color { value: input.to_string() })
        } else {
            Err(())
        }
    }
}

impl core::fmt::Display for Color {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
        f.write_str(&self.value)
    }
}

impl ColorServer {
    // could be
    // fn handle_request(&mut self, request: impl ReadableMessage) {
    // if it were not for the token
    fn handle_request<B: 'static + AsRef<[u8]>>(&mut self, request: coapwsmessage::CoAPWSMessageR<B>) {
        let mut response_message = coapwsmessage::CoAPWSMessageW::new(request.token());

        if let Err(e) = self.handle_request_inner(request, &mut response_message) {
            response_message.set_code(e);
        }

        self.connection.send(coapws::Input::Message(response_message));
    }

    fn handle_request_inner<R>(&mut self, request: R, response: &mut coapwsmessage::CoAPWSMessageW) -> Result<(), u8>
        where R: ReadableMessage
    {
        #[derive(Copy, Clone)]
        enum Path {
            Root,
            WellKnown,
            WellKnownCore,
            Color,
            Other
        }

        use coap_numbers::option::*;
        use coap_numbers::code;

        impl Path {
            fn push(self, component: &str) -> Self {
                match (self, component) {
                    (Path::Root, ".well-known") => Path::WellKnown,
                    (Path::Root, "color") => Path::Color,
                    (Path::WellKnown, "core") => Path::WellKnownCore,
                    _ => Path::Other
                }
            }
        }

        use coap_message::MessageOption;
        let mut path = Path::Root;
        let mut accept: Option<Option<u16>> = None;
        request.options()
            .ignore_uri_host()
            .filter(|o| match o.number().into() {
                coap_numbers::option::ACCEPT => {
                    accept = Some(o.value_uint());
                    false
                },
                _ => true,
            })
            .take_uri_path(|p| path = path.push(p))
            .ignore_elective_others()
            // Any critical option  was left over
            .map_err(|_| code::BAD_REQUEST)?;

        // It'd be tempting to pull accept into the whole match tree down below, but as we don't
        // see in there which part of the match failed, we'd produce terrible response codes, so
        // it's checked manually in each branch
        //
        // This could be phrased more nicely if this were wrapped in something that'd accepted ?
        // returns.
        let accept_is_not = |target| match (target, accept) {
            (_, Some(None)) => true, // It's not even a uint16!
            (_, None) => false, // It's OK for the client not to have any expectations
            (Some(x), Some(Some(y))) if x == y => false,
            _ => true,
        };

        match (path, request.code().into()) {
            (Path::WellKnownCore, code::GET) => {
                response.set_code(code::CONTENT);
                if accept_is_not(Some(40)) { return Err(code::NOT_ACCEPTABLE); }
                response.add_option(CONTENT_FORMAT, &[40]);
                response.set_payload(b"</color>;saul=ACT_LED_RGB;if=core.p,<https://github.com/chrysn/verdigris>;rel=\"impl-info\"");
                Ok(())
            },
            (Path::WellKnownCore, code::PUT) => {
                Err(code::FORBIDDEN)
            }
            (Path::Color, code::GET) => {
                // We should really pick a content format for this
                if accept_is_not(None) { return Err(code::NOT_ACCEPTABLE); }
                response.set_code(code::CONTENT);
                response.set_payload(&self.color.as_bytes());
                Ok(())
            }
            (Path::Color, code::PUT) => {
                if accept_is_not(None) { return Err(code::NOT_ACCEPTABLE); }
                self.color = core::str::from_utf8(request.payload())
                    .map_err(|_| code::BAD_REQUEST)?
                    .try_into()
                    .map_err(|_| code::BAD_REQUEST)?
                    ;
                response.set_code(code::CHANGED);
                Ok(())
            }
            _ => {
                response.set_code(code::NOT_FOUND);
                Ok(())
            }
        }
    }
}

static DEFAULT_SERVER: &'static str = "wss://rd.coap.amsuess.com/.well-known/coap";

impl Component for ColorServer {
    type Message = Msg;
    type Properties = Props;

    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
        let mut connection = coapws::Connection::bridge(link.callback(|o| Msg::Socket(o)));
        connection.send(coapws::Input::Initialize { uri: DEFAULT_SERVER.to_string() });
        let health = Health::Connecting;
        let color = "#43B3AE".to_string();

        let ep = props.ep.as_ref().cloned().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
        let server_uri = props.rd.as_ref().cloned().unwrap_or_else(|| DEFAULT_SERVER.to_string());

        // It's crude to reconnect everything, but it's simple and works
        let timer = yew::services::IntervalService::spawn(LT, link.callback(|()|  Msg::Connect));

        Self { props, connection, link, health, color, server_uri, ep, _timer: timer }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        let mut emit = false;

        match msg {
            Msg::SetServer(s) => { self.server_uri = s; emit = true; }
            Msg::SetEP(s) => { self.ep = s; emit = true; }
            Msg::Connect => {
                let mut connection = coapws::Connection::bridge(self.link.callback(|o| Msg::Socket(o)));
                connection.send(coapws::Input::Initialize { uri: self.server_uri.clone() });
                self.connection = connection;
                self.health = Health::Connecting;
            }
            Msg::Socket(coapws::Output::Connected) => {
                self.health = Health::Registering;
                // We only send one request, can just as well use the empty token here
                let mut msg = coapwsmessage::CoAPWSMessageW::new(b"");
                msg.set_code(coap_numbers::code::POST);
                msg.add_option(coap_numbers::option::URI_PATH, b".well-known");
                msg.add_option(coap_numbers::option::URI_PATH, b"core");
                msg.add_option(coap_numbers::option::URI_QUERY, b"proxy=on");
                let epquery = format!("ep={}", self.ep).into_bytes();
                msg.add_option(coap_numbers::option::URI_QUERY, &epquery);
                let ltquery = format!("lt={}", LT.as_secs()).into_bytes();
                msg.add_option(coap_numbers::option::URI_QUERY, &ltquery);
                self.connection.send(coapws::Input::Message(msg))
            }
            Msg::Socket(coapws::Output::Message(m)) => {
                match coap_numbers::code::classify(m.code().into()) {
                    coap_numbers::code::Range::Response(responseclass) => {
                        if m.token() == b"" {
                            match responseclass {
                                coap_numbers::code::Class::Success => {
                                    self.health = Health::Registered;
                                },
                                _ => {
                                    self.health = Health::Failed;
                                }
                            }
                        } else {
                            // FIXME: Is this sufficient error handling? Should we close the
                            // connection?
                            self.health = Health::Failed;
                        }
                    }
                    coap_numbers::code::Range::Request => {
                        // Escape the nestiing level ;-)
                        self.handle_request(m);
                    }
                    coap_numbers::code::Range::Empty => {}
                    _ => {
                        // FIXME as above: close connection?
                        self.health = Health::Failed;
                    }
                }
            }
            Msg::Socket(coapws::Output::SignalingInfo(_)) => {
                // We don't care, this is a very simple server that has nothing transport
                // dependent, let alone would interpret signaling messages (and there's no need to
                // actually *do* anything, the CoAPWSMessageExchanger took care of that)
            }
            Msg::Socket(coapws::Output::Error(_)) => {
                // FIXME as above (just not as bad -- the connection was already closed, and it's
                // not like we're sending messages of our own)
                self.health = Health::Failed;
            }
        }

        if emit {
            // FIXME This is exactly where callback docs say *not* to emit.
            self.props.onchange.emit((self.ep.clone(), self.server_uri.clone()));
        }

        true
    }

    fn change(&mut self, _: Self::Properties) -> ShouldRender {
        false
    }

    fn view(&self) -> Html {
        html! { <>
            <h1>{"CoAP over WebSockets: Color Panel Server"}</h1>

            <p> { "This server registers its color changing services at the given CoAP-over-WS server using Resource Directory Simple Registration." } </p>
            <p><label> { "Server URI:" } <input
                        type="text"
                        onchange=self.link.callback(move |v| Msg::SetServer(match v {
                                yew::events::ChangeData::Value(u) => u,
                                _ => unreachable!("Text input has value"),
                            }))
                        value=self.server_uri
                    /></label>
                <label> { " as " } <input
                        type="text"
                        onchange=self.link.callback(move |v| Msg::SetEP(match v {
                                yew::events::ChangeData::Value(u) => u,
                                _ => unreachable!("Text input has value"),
                            }))
                        value=self.ep
                    /></label>
                { ": " }{ self.health }{ " " }
                <button
                    onclick=self.link.callback(|_| Msg::Connect)
                    > { "(Re)connect" } </button>
            </p>
            <div style=format!("width: 100%; height: 100vh; background-color:{}", self.color)></div>
            <link
                rel="icon"
                href=format!("data:image/svg+xml;urlencode,<svg xmlns=\"http://www.w3.org/2000/svg\" style=\"background-color: %23{}\"></svg>", &self.color[1..])
                />
        </> }
    }
}