verdigris 0.2.1

Browser application to explore, learn and debug CoAP
//! A GUI component that runs a CoAP client, looks up compatible RGB lighting fixtures in a
//! resource directory, and sets any of them to a picked color.

use yew::prelude::*;
use std::convert::{TryInto, TryFrom};

use crate::{coapws, coapwsmessage};

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

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

pub struct ColorPicker {
    connection: Result<Box<dyn Bridge<coapws::Connection>>, &'static str>,
    link: ComponentLink<Self>,
    connection_uri: String,
    rd_host: String,
    color: String,
    found: Vec<String>,
    // up-counting token for those operations that happen concurrently (discovery and lookup are
    // always done exactly once on a link)
    token: u32,
}

impl ColorPicker {
    fn handle_response(&mut self, m: coapwsmessage::CoAPWSMessageR<impl AsRef<[u8]>>) -> Result<(), &'static str> {
        match m.token() {
            b"d" => {
                m.options()
                    .ignore_elective_others()
                    .expect("Can't handle discovery response option");

                let nextlink = stupid_parse_linkformat(
                    core::str::from_utf8(m.payload())
                    .map_err(|_| "Non-UTF discovery response")?)
                    .next()
                    .ok_or("Nothing found in discovery")?;

                let mut msg = coapwsmessage::CoAPWSMessageW::new(b"l");
                msg.set_code(coap_numbers::code::GET);
                let options = crate::util::OptionFeedBuilder::new()
                    .add_option(coap_numbers::option::URI_QUERY, "saul=ACT_LED_RGB".as_bytes())
                    .add_option(coap_numbers::option::OBSERVE, &[])
                    .finish();
                set_msg_uri(&mut msg, options, nextlink, Some("coap"), Some(&self.rd_host), None)
                    .map_err(|_| "Invalid reference in discovery")?;

                self.connection
                    .as_mut()
                    .expect("coapws violated its message sequence")
                    .send(coapws::Input::Message(msg))
            },
            b"l" => {
                m.options()
                    .ignore_elective_others()
                    .expect("Can't handle lookup response option");

                let parsed = stupid_parse_linkformat(
                    core::str::from_utf8(m.payload())
                    .map_err(|_| "Non-UTF discovery response")?)
                    .map(|s| s.to_string())
                    .collect();

                self.found = parsed;
            }
            [b's', _, _, _, _] => {
                // Ignore set results
            }
            t => {
                log::warn!("Unexpected token {:?} received", t);
                return Err("Unexpected token");
            }
        }

        Ok(())
    }
}

#[derive(Debug)]
pub enum Msg {
    Socket(coapws::Output),
    SetConnection(String),
    SetRdHost(String),
    Discover, // implies a connect and a discovery step
    PickColor(String),
    ApplyAll,
    ApplyTo(String),
}

/// Pick the target URI references out of a link-format document.
///
/// This does terrible parsing, but it's good enough as long as there's nothing too exotic (read:
/// '<' in link attributes), and sufficient as long as all links are in limited link format and
/// adaequately filtered (as an RD produces them).
///
/// This may crash on highly unexpected input (cf. function name).
fn stupid_parse_linkformat<'a>(input: &'a str) -> impl Iterator<Item=&'a str> {
    struct Cursor<'a> {
        s: &'a str,
    }

    impl<'a> Iterator for Cursor<'a> {
        type Item = &'a str;

        fn next(&mut self) -> Option<Self::Item> {
            let index = self.s.find('>')?;
            let (head, tail) = self.s.split_at(index);
            self.s = &tail[1..];
            let index = head.find('<').expect("Format error");
            let (_, ret) = head.split_at(index);
            Some(&ret[1..])
        }
    }

    Cursor { s: input }
}

use crate::util::OptionFeed;

/// Write the Uri-Host, -Port, -Path ... and Proxy-Scheme into a message.
/// 
/// This can only use full URIs or path-absolute references, and thus takes the base values
/// pre-parsed in arguments. (Where giving no scheme but a relative reference results in an error).
///
/// Refactoring this to take &'a references and produce a `impl 'a + OptionFeed` should a) get us
/// rid of the need to take a mixin rather than producing one (avoiding the need to let the mixin
/// work before adding options), and b) may allow us to rely on the produced mixin to not need to
/// manually sort things in the first place.
fn set_msg_uri(msg: &mut impl MinimalWritableMessage, mut mixins: impl OptionFeed<u16>, reference: &str, scheme: Option<&str>, host: Option<&str>, port: Option<u16>) -> Result<(), &'static str> {
    use coap_numbers::option::*;

    // This indicates a clear shortcoming in the coap-message system
    let uri_host = URI_HOST.try_into().map_err(|_| ()).expect("Message type can't express Uri-Host");
    let uri_port = URI_PORT.try_into().map_err(|_| ()).expect("Message type can't express Uri-Port");
    let uri_path = || URI_PATH.try_into().map_err(|_| ()).expect("Message type can't express Uri-Path");
    let proxy_scheme = PROXY_SCHEME.try_into().map_err(|_| ()).expect("Message type can't express Proxy-Scheme");

    let parsed = uriparse::URIReference::try_from(reference).map_err(|_| "URI parse error")?;
    mixins.apply_to_message_limited(msg, URI_HOST).map_err(|_| ()).expect("Feeder options unprocessable");
    if let Some(a) = parsed.authority() {
        if a.has_username() || a.has_password() {
            return Err("User/password in authority");
        }
        // FIXME Not trying to fix any percent encoding or something at all as even the library's
        // .normalize() function won't remove all of them
        msg.add_option(uri_host, a.to_string().as_bytes());
    } else {
        if let Some(h) = host {
            msg.add_option(uri_host, h.as_bytes());
        }
        if let Some(p) = port {
            mixins.apply_to_message_limited(msg, URI_PORT).map_err(|_| ()).expect("Feeder options unprocessable");
            msg.add_option_uint(uri_port, p);
        }
    }

    mixins.apply_to_message_limited(msg, URI_PATH).map_err(|_| ()).expect("Feeder options unprocessable");
    for p in parsed.path().segments() {
        msg.add_option(uri_path(), p.as_str().as_bytes());
    }

    if let Some(_) = parsed.query() {
        unimplemented!()

    }

    mixins.apply_to_message_limited(msg, PROXY_SCHEME).map_err(|_| ()).expect("Feeder options unprocessable");
    if let Some(s) = parsed.scheme() {
        msg.add_option(proxy_scheme, s.as_str().as_bytes())
    } else if let Some(s) = scheme {
        msg.add_option(proxy_scheme, s.as_bytes())
    } else {
        return Err("Relative reference without base scheme");
    }

    Ok(())
}

impl Component for ColorPicker {
    type Message = Msg;
    type Properties = ();

    fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
        let color = "#43B3AE".to_string();
        let connection_uri = DEFAULT_CONNECTION_URI.to_string();
        let rd_host = DEFAULT_RD_HOST.to_string();

        let mut s = Self { connection: Err("Not started yet"), link, color, found: vec![], connection_uri, rd_host, token: 0 };
        // FIXME may this be used like that?
        s.update(Msg::Discover);
        s
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        let options_for_rgb = || crate::util::OptionFeedBuilder::new()
            .add_option_uint(coap_numbers::option::CONTENT_FORMAT, 65362u16)
            .finish();

        match msg {
            Msg::SetConnection(s) => { self.connection_uri = s; }
            Msg::SetRdHost(s) => { self.rd_host = s; }
            Msg::Discover => {
                let mut connection = coapws::Connection::bridge(self.link.callback(|o| Msg::Socket(o)));
                connection.send(coapws::Input::Initialize { uri: self.connection_uri.clone() });
                self.connection = Ok(connection);
                self.found.clear();
            }
            Msg::PickColor(c) => {
                self.color = c;
            }
            Msg::ApplyAll => {
                let c = match self.connection.as_mut() {
                    Ok(c) => c,
                    // FIXME silently?
                    _ => return false
                };

                for f in self.found.iter() {
                    self.token += 1;
                    let mut token = vec![b's'];
                    token.extend_from_slice(&self.token.to_be_bytes());
                    let mut msg = coapwsmessage::CoAPWSMessageW::new(&token);
                    msg.set_code(coap_numbers::code::PUT);
                    if set_msg_uri(&mut msg, options_for_rgb(), &f, None, None, None).is_err() {
                        // FIXME silent.y?
                        continue;
                    }
                    msg.set_payload(self.color.as_bytes());
                    c.send(coapws::Input::Message(msg));
                }
            }
            Msg::ApplyTo(s) => {
                self.token += 1;
                let mut token = vec![b's'];
                token.extend_from_slice(&self.token.to_be_bytes());
                let mut msg = coap_message::heapmessage::HeapMessage::new();
                msg.set_code(coap_numbers::code::PUT);

                if set_msg_uri(&mut msg, options_for_rgb(), &s, None, None, None).is_err() {
                    // FIXME silent.y?
                    return false;
                }
                msg.set_payload(self.color.as_bytes());

                let mut actualmsg = coapwsmessage::CoAPWSMessageW::new(&token);
                actualmsg.set_from_message(&msg);
                self.connection.as_mut().map(|c| c.send(coapws::Input::Message(actualmsg)))
                // FIXME silently ignoring error
                    .ok();
            }
            Msg::Socket(coapws::Output::Connected) => {
                // token 'd' for a discovery request
                let mut msg = coapwsmessage::CoAPWSMessageW::new(b"d");
                msg.set_code(coap_numbers::code::GET);
                msg.add_option(coap_numbers::option::URI_HOST, self.rd_host.as_bytes());
                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"rt=core.rd-lookup-res");
                msg.add_option(coap_numbers::option::PROXY_SCHEME, b"coap");
                self.connection
                    .as_mut()
                    .expect("coapws violated its message sequence")
                    .send(coapws::Input::Message(msg))
            }
            Msg::Socket(coapws::Output::Message(m)) => {
                match coap_numbers::code::classify(m.code().into()) {
                    coap_numbers::code::Range::Response(coap_numbers::code::Class::Success) => {
                        match self.handle_response(m) {
                            Ok(()) => {}
                            Err(e) => {
                                self.connection = Err(e);
                            }
                        }
                    }
                    coap_numbers::code::Range::Response(_) => {
                        // Implicitly closing the connection
                        self.connection = Err("Unsuccessful response received");
                    }
                    coap_numbers::code::Range::Request => {
                        log::warn!("Odd, we're being requested");
                        let mut response = coapwsmessage::CoAPWSMessageW::new( m.token());
                        response.set_code(coap_numbers::code::NOT_IMPLEMENTED);
                        self.connection
                            .as_mut()
                            .expect("Connection should be here")
                            .send(coapws::Input::Message(response));
                    }
                    coap_numbers::code::Range::Empty => {}
                    _ => {
                        self.connection = Err("Socket produced unidentified event");
                    }
                }
            }
            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(_)) => {
                self.connection = Err("Socket produced error");
            }
        }
        true
    }

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

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

            <p> { "This client queries a resource directory and scans it for compatible, settable color resources. It allows setting them to a selected color. (Compatible here currently means 'the Color Server from the other tab', or any RGB LED on a RIOT board using " } <a href="https://gitlab.com/etonomy/riot-examples/-/tree/master/coap_saulserver"> { "the Rust RIOT demo" } </a> { ")." } </p>
            <p><label> { "Contact CoAP-over-WS proxy at " } <input
                        type="text"
                        onchange=self.link.callback(move |v| Msg::SetConnection(match v {
                                yew::events::ChangeData::Value(u) => u,
                                _ => unreachable!("Text input has value"),
                            }))
                        value=self.connection_uri
                    /></label>
                <label> { " and discover the RD starting from " } <input
                        type="text"
                        onchange=self.link.callback(move |v| Msg::SetRdHost(match v {
                                yew::events::ChangeData::Value(u) => u,
                                _ => unreachable!("Text input has value"),
                            }))
                        value=self.rd_host
                    /></label>
                { format!(" ({}) ", self.connection.as_ref().map(|_| "Active").unwrap_or_else(|e| e)) }
                <button
                    onclick=self.link.callback(|_| Msg::Discover)
                    > { "Discover" } </button>
            </p>
            <p> <label> { "Color: " } <input type="color"
                    value=self.color
                    onchange=self.link.callback(move |v| Msg::PickColor(match v {
                            yew::events::ChangeData::Value(u) => u,
                            _ => unreachable!("Color input has value"),
                        }))
            /></label> { &self.color }
                <button onclick=self.link.callback(|_| Msg::ApplyAll) > { "Set all" } </button>
            </p>
            <ul>
            { for self.found.iter().map(|f| { let f = f.clone(); html! {
                <li> { &f } <button onclick=self.link.callback(move |_| Msg::ApplyTo(f.clone())) > { "Apply" } </button> </li>
            } } ) }
            </ul>
        </> }
    }
}