<commit_before>// TODO: Why do I need these two lines "extern crate html5ever" and "extern crate
// html5ever_dom_sink" both here and in main.rs?
extern crate html5ever;
extern crate html5ever_dom_sink;
extern crate rand;
extern crate tendril;

use std::str::FromStr;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread;

use html5ever::Attribute;
use html5ever::tree_builder::interface::TreeSink;
use html5ever_dom_sink::common::NodeEnum;
use html5ever_dom_sink::rcdom::Handle;
use html5ever_dom_sink::rcdom::RcDom;
use regex::Captures;

use wiki::Wiki;

// TODO: massive cleanup, all over this file.

// TODO: doc comments everywhere!
pub struct Page {
    html_body_sender: Sender<String>,
    replaced_body_receiver: Receiver<Result<String, String>>,
}

impl Page {
    pub fn new(title: &str, wiki: Wiki) -> Page {
        let placeholder = format!("WMW_PLACEHOLDER_{}", rand::random::<u64>());
        let current_content_receiver =
            Page::spawn_content_fetch_thread(title, placeholder.clone(), wiki);
        let (html_body_sender, replaced_body_receiver) =
            Page::spawn_replace_body_thread(current_content_receiver, placeholder);
        Page {
            html_body_sender: html_body_sender,
            replaced_body_receiver: replaced_body_receiver,
        }
    }

    pub fn replace_body_and_remove_merge_markers(&self, html_body: String)
                                                 -> Result<String, String> {
        self.html_body_sender.send(html_body);
        try_display!(self.replaced_body_receiver.recv(), "Failed to get data from channel")
    }

    fn spawn_content_fetch_thread(title: &str, placeholder: String, wiki: Wiki)
                                  -> Receiver<Result<String, String>> {
        let (sender, receiver) = channel::<Result<String, String>>();
        let title = title.to_owned().clone();
        thread::spawn(move|| {
            sender.send(
                match wiki.get_current_page_content(&title) {
                    Ok(content) =>
                        replace_node_with_placeholder(&content, "mw-content-text", &placeholder),
                Err(msg) => Err(msg),
            }).unwrap();
        });
        receiver
    }

    // TODO: doc comment
    fn spawn_replace_body_thread(
        page_skeleton_receiver: Receiver<Result<String, String>>, placeholder: String)
        -> (Sender<String>, Receiver<Result<String, String>>) {
        let (html_body_sender, html_body_receiver) = channel::<String>();
        let (replaced_body_sender, replaced_body_receiver) = channel::<Result<String, String>>();
        thread::spawn(move|| {
            match (page_skeleton_receiver.recv(), html_body_receiver.recv()) {
                (Ok(Ok(page_skeleton)), Ok(body)) => {
                    let html_with_merge_markers =
                        remove_merge_markers_from_html(page_skeleton.replace(&placeholder, &body));
                    // TODO: Move this elsewhere, use constants, etc.
                    let start_regex = regex!("\u{E000}([0-9]+)\u{E000}");
                    let end_regex = regex!("\u{E001}[0-9]+\u{E001}");
                    let finished_html =
                        start_regex.replace_all(
                            &end_regex.replace_all(&html_with_merge_markers, ""),
                            |captures: &Captures|
                            format!("",
                                    captures.at(1).unwrap()));
                    replaced_body_sender.send(Ok(finished_html))
                },
                (Ok(Err(msg)), _) => replaced_body_sender.send(Err(msg)),
                (Err(err), _) => replaced_body_sender.send(Err(format!("error: {}", err))),
                (_, Err(err)) => replaced_body_sender.send(Err(format!("error: {}", err))),
            }.unwrap()
        });
        (html_body_sender, replaced_body_receiver)
    }
}

fn replace_node_with_placeholder(original_html: &str, div_id: &str, placeholder: &str)
    -> Result<String, String> {
    let html = tendril::StrTendril::from_str(original_html).unwrap();
    let mut dom: RcDom = html5ever::parse(html5ever::one_input(html), Default::default());

    let handle = try!(find_node_by_id(&dom.get_document(), div_id));
    let child_handles =
        (&handle.borrow().children).into_iter().map(|child| child.clone()).collect::<Vec<_>>();
    for child_handle in child_handles {
        dom.remove_from_parent(child_handle);
    }
    dom.append(handle,
               html5ever::tree_builder::interface::NodeOrText::AppendText(
                   tendril::StrTendril::from_str(placeholder).unwrap()));
    let mut serialized: Vec<u8> = vec![];
    try_display!(
        html5ever::serialize::serialize(&mut serialized, &dom.document, Default::default()),
        "Failed to serialize modified HTML");
    Ok(try_display!(String::from_utf8(serialized),
                    "Error converting serialized HTML to UTF-8 string"))
}

fn find_node_by_id(handle: &Handle, id: &str) -> Result<Handle, String> {
    fn has_matching_id(attributes: &Vec<Attribute>, id: &str) -> bool {
        return attributes.into_iter().any(
            |attribute| attribute.name.local.as_slice() == "id" &&
                format!("{}", attribute.value) == id);
    }

    let node = handle.borrow();
    match node.node {
        NodeEnum::Element(_, ref attributes) if has_matching_id(attributes, id) => Ok(handle.clone()),
        _ => (&node.children).into_iter()
            .map(|child| find_node_by_id(child, id))
            .filter(|result| result.is_ok())
            .map(|result| result.unwrap())
            .next().ok_or(format!("No node with ID {} found", id)),
    }
}

#[cfg(test)]
mod tests {
    use super::{remove_merge_markers_from_html, replace_node_with_placeholder};

    #[test]
    fn test_remove_merge_markers_from_html() {
        let html = format!("{}123{}",
                           ::START_MARKER, ::START_MARKER, ::END_MARKER, ::END_MARKER);
        let expected = "";
        assert_eq!(expected, remove_merge_markers_from_html(html));
    }

    #[test]
    fn test_remove_merge_markers_from_html_keep() {
        let html = format!("{}456{}{}456{}",
                           ::START_MARKER, ::START_MARKER, ::END_MARKER, ::END_MARKER);
        assert_eq!(html.clone(), remove_merge_markers_from_html(html));
    }

    #[test]
    fn test_remove_merge_markers_from_html_keep_one_remove_one() {
        let html = format!(
            "{}234{}text{}234{}{}567{}",
            ::START_MARKER, ::START_MARKER, ::END_MARKER, ::END_MARKER, ::START_MARKER, ::START_MARKER,
            ::END_MARKER, ::END_MARKER);
        let expected = format!(
            "{}234{}text{}234{}",
            ::START_MARKER, ::START_MARKER, ::END_MARKER, ::END_MARKER);
        assert_eq!(expected, remove_merge_markers_from_html(html));
    }

    #[test]
    fn test_replace_html_content() {
        let original_html = "

original text

Other text

";
        let expected_html = "

replaced text

Other text

";
        let processed_html = replace_node_with_placeholder(original_html, "mw-content-text", "replaced text").unwrap();
        assert_eq!(expected_html, processed_html);
    }
}

fn remove_merge_markers_from_html(html: String) -> String {
    // TODO: clean up this whole function. regex[1..4] are not good names.
    // TODO: use START_MARKER and END_MARKER constants here.
    // Finds markers where the end, but not the start, is inside a tag.
    let regex1 = regex!(
        r"\x{E000}[0-9]+\x{E000}([^\x{E001}]*?)<([^>]*?)\x{E001}[0-9]+\x{E001}([^>]*?)>");
    // Finds markers where the start, but not the end, is inside a tag.
    let regex2 = regex!(
        r"<([^>]*?)\x{E000}[0-9]+\x{E000}([^>]*?)>([^\x{E000}]*?)\x{E000}[0-9]+\x{E000}");
    // Finds markers where both the start and end are inside tags.
    let regex3 = regex!(
        r"<([^>]*?)\x{E000}[0-9]+\x{E000}([^>]*?)>([^\x{E000}\x{E001}]*?)<([^>]*?)\x{E001}[0-9]+\x{E001}([^>]*?)>");

    let html1 = regex1.replace_all(
        &html, |captures: &Captures|
        format!("{}<{}{}>", captures.at(1).unwrap(), captures.at(2).unwrap(),
                captures.at(3).unwrap()));
    let html2 = regex2.replace_all(
        &html1, |captures: &Captures|
        format!("<{}{}>{}>", captures.at(1).unwrap(), captures.at(2).unwrap(),
                captures.at(3).unwrap()));
    regex3.replace_all(
        &html2, |captures: &Captures|
        format!("<{}{}>{}<{}{}>", captures.at(1).unwrap(), captures.at(2).unwrap(),
                captures.at(3).unwrap(), captures.at(4).unwrap(), captures.at(5).unwrap()))
}
<commit_msg>Add doc comments to Page and its public functions.<commit_after>//! Contains all HTML-related code. See the `Page` documentation for details.

// TODO: Why do I need these two lines "extern crate html5ever" and "extern crate
// html5ever_dom_sink" both here and in main.rs?
extern crate html5ever;
extern crate html5ever_dom_sink;
extern crate rand;
extern crate tendril;

use std::str::FromStr;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread;

use html5ever::Attribute;
use html5ever::tree_builder::interface::TreeSink;
use html5ever_dom_sink::common::NodeEnum;
use html5ever_dom_sink::rcdom::Handle;
use html5ever_dom_sink::rcdom::RcDom;
use regex::Captures;

use wiki::Wiki;

// TODO: massive cleanup, all over this file.
// TODO: I'm using "body" and even "html_body" to mean "article body" in this file and main.rs,
// which isn't right. Come up with a better name for that and use it everywhere.

/// Represents, and owns all behavior related to, the contents of the HTML page shown to the
/// user. This includes fetching the rendered article from Wikipedia, replacing its contents with
/// the rendered wikitext, and processing/removing merge markers.
///
/// The API to this struct comprises two methods: Page::new() constructs a new Page. It should be
/// called early, so the Page can start fetching article HTML from Wikipedia.
/// Page:replace_body_and_remove_merge_markers() processes the merge markers in the rendered
/// wikitext and puts the header and footer around it.
pub struct Page {
    html_body_sender: Sender<String>,
    replaced_body_receiver: Receiver<Result<String, String>>,
}

impl Page {
    /// Creates a new Page representing the article at `title`. This kicks off a background thread
    /// that fetches the current article HTML from Wikipedia. Because of that, it should be called
    /// as early as possible (as soon as the title being served is known), so that the page fetch
    /// stays off the critical path for page load.
    pub fn new(title: &str, wiki: Wiki) -> Page {
        let placeholder = format!("WMW_PLACEHOLDER_{}", rand::random::<u64>());
        let current_content_receiver =
            Page::spawn_content_fetch_thread(title, placeholder.clone(), wiki);
        let (html_body_sender, replaced_body_receiver) =
            Page::spawn_replace_body_thread(current_content_receiver, placeholder);
        Page {
            html_body_sender: html_body_sender,
            replaced_body_receiver: replaced_body_receiver,
        }
    }

    // TODO: Why is this done in a separate thread at all? That's unnecessary. Just do the
    // processing on this thread.
    /// This finishes the HTML processing - it replaces the merge markers in `html_body` with HTML
    /// tags, and inserts the resulting HTML into the page skeleton.
    pub fn replace_body_and_remove_merge_markers(&self, html_body: String)
                                                 -> Result<String, String> {
        self.html_body_sender.send(html_body);
        try_display!(self.replaced_body_receiver.recv(), "Failed to get data from channel")
    }

    fn spawn_content_fetch_thread(title: &str, placeholder: String, wiki: Wiki)
                                  -> Receiver<Result<String, String>> {
        let (sender, receiver) = channel::<Result<String, String>>();
        let title = title.to_owned().clone();
        thread::spawn(move|| {
            sender.send(
                match wiki.get_current_page_content(&title) {
                    Ok(content) =>
                        replace_node_with_placeholder(&content, "mw-content-text", &placeholder),
                Err(msg) => Err(msg),
            }).unwrap();
        });
        receiver
    }

    fn spawn_replace_body_thread(
        page_skeleton_receiver: Receiver<Result<String, String>>, placeholder: String)
        -> (Sender<String>, Receiver<Result<String, String>>) {
        let (html_body_sender, html_body_receiver) = channel::<String>();
        let (replaced_body_sender, replaced_body_receiver) = channel::<Result<String, String>>();
        thread::spawn(move|| {
            match (page_skeleton_receiver.recv(), html_body_receiver.recv()) {
                (Ok(Ok(page_skeleton)), Ok(body)) => {
                    // TODO: This should remove the merge markers first, and replace the page
                    // contents second. There are never merge markers in the page skeleton, so it's
                    // useless to look for merge markers there.
                    let html_with_merge_markers =
                        remove_merge_markers_from_html(page_skeleton.replace(&placeholder, &body));
                    // TODO: Move this elsewhere, use constants, etc.
                    let start_regex = regex!("\u{E000}([0-9]+)\u{E000}");
                    let end_regex = regex!("\u{E001}[0-9]+\u{E001}");
                    let finished_html =
                        start_regex.replace_all(
                            &end_regex.replace_all(&html_with_merge_markers, ""),
                            |captures: &Captures|
                            format!("",
                                    captures.at(1).unwrap()));
                    replaced_body_sender.send(Ok(finished_html))
                },
                (Ok(Err(msg)), _) => replaced_body_sender.send(Err(msg)),
                (Err(err), _) => replaced_body_sender.send(Err(format!("error: {}", err))),
                (_, Err(err)) => replaced_body_sender.send(Err(format!("error: {}", err))),
            }.unwrap()
        });
        (html_body_sender, replaced_body_receiver)
    }
}

fn replace_node_with_placeholder(original_html: &str, div_id: &str, placeholder: &str)
    -> Result<String, String> {
    let html = tendril::StrTendril::from_str(original_html).unwrap();
    let mut dom: RcDom = html5ever::parse(html5ever::one_input(html), Default::default());

    let handle = try!(find_node_by_id(&dom.get_document(), div_id));
    let child_handles =
        (&handle.borrow().children).into_iter().map(|child| child.clone()).collect::<Vec<_>>();
    for child_handle in child_handles {
        dom.remove_from_parent(child_handle);
    }
    dom.append(handle,
               html5ever::tree_builder::interface::NodeOrText::AppendText(
                   tendril::StrTendril::from_str(placeholder).unwrap()));
    let mut serialized: Vec<u8> = vec![];
    try_display!(
        html5ever::serialize::serialize(&mut serialized, &dom.document, Default::default()),
        "Failed to serialize modified HTML");
    Ok(try_display!(String::from_utf8(serialized),
                    "Error converting serialized HTML to UTF-8 string"))
}

fn find_node_by_id(handle: &Handle, id: &str) -> Result<Handle, String> {
    fn has_matching_id(attributes: &Vec<Attribute>, id: &str) -> bool {
        return attributes.into_iter().any(
            |attribute| attribute.name.local.as_slice() == "id" &&
                format!("{}", attribute.value) == id);
    }

    let node = handle.borrow();
    match node.node {
        NodeEnum::Element(_, ref attributes) if has_matching_id(attributes, id) => Ok(handle.clone()),
        _ => (&node.children).into_iter()
            .map(|child| find_node_by_id(child, id))
            .filter(|result| result.is_ok())
            .map(|result| result.unwrap())
            .next().ok_or(format!("No node with ID {} found", id)),
    }
}

#[cfg(test)]
mod tests {
    use super::{remove_merge_markers_from_html, replace_node_with_placeholder};

    #[test]
    fn test_remove_merge_markers_from_html() {
        let html = format!("{}123{}",
                           ::START_MARKER, ::START_MARKER, ::END_MARKER, ::END_MARKER);
        let expected = "";
        assert_eq!(expected, remove_merge_markers_from_html(html));
    }

    #[test]
    fn test_remove_merge_markers_from_html_keep() {
        let html = format!("{}456{}{}456{}",
                           ::START_MARKER, ::START_MARKER, ::END_MARKER, ::END_MARKER);
        assert_eq!(html.clone(), remove_merge_markers_from_html(html));
    }

    #[test]
    fn test_remove_merge_markers_from_html_keep_one_remove_one() {
        let html = format!(
            "{}234{}text{}234{}{}567{}",
            ::START_MARKER, ::START_MARKER, ::END_MARKER, ::END_MARKER, ::START_MARKER, ::START_MARKER,
            ::END_MARKER, ::END_MARKER);
        let expected = format!(
            "{}234{}text{}234{}",
            ::START_MARKER, ::START_MARKER, ::END_MARKER, ::END_MARKER);
        assert_eq!(expected, remove_merge_markers_from_html(html));
    }

    #[test]
    fn test_replace_html_content() {
        let original_html = "

original text

Other text

";
        let expected_html = "

replaced text

Other text

";
        let processed_html = replace_node_with_placeholder(original_html, "mw-content-text", "replaced text").unwrap();
        assert_eq!(expected_html, processed_html);
    }
}

fn remove_merge_markers_from_html(html: String) -> String {
    // TODO: clean up this whole function. regex[1..4] are not good names.
    // TODO: use START_MARKER and END_MARKER constants here.
    // Finds markers where the end, but not the start, is inside a tag.
    let regex1 = regex!(
        r"\x{E000}[0-9]+\x{E000}([^\x{E001}]*?)<([^>]*?)\x{E001}[0-9]+\x{E001}([^>]*?)>");
    // Finds markers where the start, but not the end, is inside a tag.
    let regex2 = regex!(
        r"<([^>]*?)\x{E000}[0-9]+\x{E000}([^>]*?)>([^\x{E000}]*?)\x{E000}[0-9]+\x{E000}");
    // Finds markers where both the start and end are inside tags.
    let regex3 = regex!(
        r"<([^>]*?)\x{E000}[0-9]+\x{E000}([^>]*?)>([^\x{E000}\x{E001}]*?)<([^>]*?)\x{E001}[0-9]+\x{E001}([^>]*?)>");

    let html1 = regex1.replace_all(
        &html, |captures: &Captures|
        format!("{}<{}{}>", captures.at(1).unwrap(), captures.at(2).unwrap(),
                captures.at(3).unwrap()));
    let html2 = regex2.replace_all(
        &html1, |captures: &Captures|
        format!("<{}{}>{}>", captures.at(1).unwrap(), captures.at(2).unwrap(),
                captures.at(3).unwrap()));
    regex3.replace_all(
        &html2, |captures: &Captures|
        format!("<{}{}>{}<{}{}>", captures.at(1).unwrap(), captures.at(2).unwrap(),
                captures.at(3).unwrap(), captures.at(4).unwrap(), captures.at(5).unwrap()))
}
<|endoftext|>


<commit_before>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Communication with the PowerDNS server happens through the http
// server.
// See https://doc.powerdns.com/md/authoritative/backend-remote/ for
// details about the various requests and responses.

use config::Config;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use errors::*;
use iron::headers::ContentType;
use iron::prelude::*;
use iron::status::{self, Status};
use serde_json;
use std::io::Read;

#[derive(Deserialize)]
struct PdnsRequestParameters {
    // intialize method
    path: Option<String>,
    timeout: Option<String>,

    // lookup method
    qtype: Option<String>,
    qname: Option<String>,
    #[serde(rename="zone-id")]
    zone_id: Option<i32>,
    remote: Option<String>,
    local: Option<String>,
    real_remote: Option<String>,
}

#[derive(Deserialize)]
struct PdnsRequest {
    method: String,
    parameters: PdnsRequestParameters,
}

#[derive(Serialize)]
pub struct PdnsLookupResponse {
    qtype: String,
    qname: String,
    content: String,
    ttl: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    domain_id: Option<String>,
    #[serde(rename="scopeMask")]
    #[serde(skip_serializing_if = "Option::is_none")]
    scope_mask: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    auth: Option<String>,
}

#[derive(Serialize)]
#[serde(untagged)]
pub enum PdnsResponseParams {
    Lookup(PdnsLookupResponse),
}

#[derive(Serialize)]
pub struct PdnsResponse {
    result: Vec<PdnsResponseParams>,
}

fn pdns_failure(reason: &str) -> IronResult<Response> {
    debug!("pdns_failure: {}", reason);
    let mut response = Response::with("{\"result\":false}");
    response.status = Some(Status::Ok);
    response.headers.set(ContentType::json());
    Ok(response)
}

pub fn pdns_response_as_iron(response: &PdnsResponse) -> IronResult<Response> {
    match serde_json::to_string(response) {
        Ok(serialized) => {
            debug!("Response is: {}", serialized);
            let mut response = Response::with(serialized);
            response.status = Some(Status::Ok);
            response.headers.set(ContentType::json());

            Ok(response)
        }
        Err(err) => {
            error!("{}", err);
            EndpointError::with(status::InternalServerError, 501)
        }
    }
}

// Returns a SOA record for a given qname.
pub fn soa_response(qname: &str, config: &Config) -> PdnsLookupResponse {
    PdnsLookupResponse {
        qtype: "SOA".to_owned(),
        qname: qname.to_owned(),
        content: config.soa_content.to_owned(),
        ttl: config.dns_ttl,
        domain_id: None,
        scope_mask: None,
        auth: None,
    }
}

pub fn pakegite_query(qname: &str, qtype: &str, config: &Config) -> IronResult<Response> {
    // Pagekite sends dns requests to qnames like:
    // dd7251eef7c773a192feb06c0e07ac6020ac.tc730a6b9e2f28f407bb3871e98d3fe4e60c.625558ecb0d283a5b058ba88fb3d9aa11d48.https-4443.fabrice.box.knilxof.org.box.knilxof.org
    // See https://pagekite.net/wiki/Howto/DnsBasedAuthentication
    debug!("PageKite query for {} {}", qtype, qname);

    let mut pdns_response = PdnsResponse { result: Vec::new() };

    if qtype == "SOA" {
        pdns_response
            .result
            .push(PdnsResponseParams::Lookup(soa_response(qname, config)));
        return pdns_response_as_iron(&pdns_response);
    }

    if qtype != "A" && qtype != "ANY" {
        return pdns_failure(&format!("Unsupported PageKite request type: {}", qtype));
    }

    // Split up the qname.
    let parts: Vec<&str> = qname.split('.').collect();
    let subdomain = format!("{}.box.{}.", parts[4], config.domain);
    let ip = match config
              .domain_db
              .get_record_by_name(&subdomain)
              .recv()
              .unwrap() {
        Ok(record) => {
            let srand = parts[0];
            let token = parts[1];
            let sign = parts[2];
            let proto = parts[3];
            let kite_domain = format!("{}.box.{}", parts[4], config.domain);
            let payload = format!("{}:{}:{}:{}", proto, kite_domain, srand, token);
            let salt = sign[..8].to_owned();

            debug!("{} {} {} {} {}", srand, token, sign, proto, kite_domain);

            let mut hasher = Sha1::new();
            hasher.input_str(&format!("{}{}{}", record.token, payload, salt));
            let calc = hasher.result_str();

            let calc_sub = calc[..28].to_owned();
            let sign_sub = sign[8..36].to_owned();

            debug!("Signatures: {} {}", calc_sub, sign_sub);

            if calc_sub == sign_sub {
                "255.255.254.255"
            } else {
                "255.255.255.1"
            }
        }
        Err(_) => {
            // Return 255.255.255.0 to PageKite to indicate failure.
            "255.255.255.0"
        }
    };

    let ns_record = PdnsLookupResponse {
        qtype: "A".to_owned(),
        qname: qname.to_owned(),
        content: ip.to_owned(),
        ttl: config.dns_ttl,
        domain_id: None,
        scope_mask: None,
        auth: None,
    };
    pdns_response
        .result
        .push(PdnsResponseParams::Lookup(ns_record));

    pdns_response_as_iron(&pdns_response)
}

pub fn pdns_endpoint(req: &mut Request, config: &Config) -> IronResult<Response> {
    use std::net::SocketAddr::V4;
    use std::net::Ipv4Addr;

    info!("GET /pdns");
    // Only allow clients from localhost.
    match req.remote_addr {
        V4(addr) => {
            if addr.ip() != &Ipv4Addr::new(127, 0, 0, 1) {
                return EndpointError::with(status::BadRequest, 400);
            }
        }
        _ => return EndpointError::with(status::BadRequest, 400),
    }

    // Read the request from the json body.
    let mut s = String::new();
    itry!(req.body.read_to_string(&mut s));

    debug!("Body is: {}", s);

    let input: PdnsRequest = match serde_json::from_str(&s) {
        Ok(value) => value,
        Err(err) => {
            error!("Bad request: {}", err);
            return EndpointError::with(status::BadRequest, 400);
        }
    };

    debug!("pdns request is {}", input.method);

    if input.method == "lookup" {
        let original_qname = input.parameters.qname.unwrap().to_lowercase();
        let mut qname = original_qname.clone();
        let qtype = input.parameters.qtype.unwrap();
        debug!("lookup for qtype={} qname={}", qtype, original_qname);

        // Example payload:
        //
        // {"method": "lookup",
        //  "parameters": {"local": "0.0.0.0",
        //                 "qname": "fabrice.box.knilxof.org.",
        //                 "qtype": "SOA",
        //                 "real-remote": "63.245.221.198/32",
        //                 "remote": "63.245.221.198",
        //                 "zone-id": -1}}

        // If the qname ends up with .box.$domain.box.$domain. we consider that it's a
        // PageKite request and process it separately.
        if qname.ends_with(&format!(".box.{}.box.{}.", config.domain, config.domain)) {
            return pakegite_query(&qname, &qtype, config);
        }

        // If the qname starts with `_acme-challenge.` this is a DNS-01 challenge verification,
        // so remove that part of the domain to retrieve our record.
        // See https://tools.ietf.org/html/draft-ietf-acme-acme-06#section-8.4
        if qname.starts_with("_acme-challenge.") {
            qname = qname[16..].to_owned();
        }

        debug!("final qname={}", qname);

        // Look for a record with for the qname.
        match config
                  .domain_db
                  .get_record_by_name(&qname)
                  .recv()
                  .unwrap() {
            Ok(record) => {
                if record.local_ip.is_none() && qtype == "A" {
                    // No info on this domain, bail out.
                    return pdns_failure("No local_ip");
                }

                // Choose either the local or public ip based on the parameters.remote one.
                let a_record = if record.public_ip.is_some() &&
                                  input.parameters.remote.unwrap() == record.public_ip.unwrap() {
                    // We are inside of the home network, return the local ip for the A record.
                    record.local_ip.unwrap()
                } else {
                    // We are outside of the home network, return the ip of the tunnel
                    // for the A record.
                    config.tunnel_ip.to_owned()
                };

                let mut pdns_response = PdnsResponse { result: Vec::new() };

                if qtype == "SOA" {
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(soa_response(&original_qname, config)));
                }

                if qtype == "ANY" || qtype == "A" {
                    // Add an "A" record.
                    let ns_record = PdnsLookupResponse {
                        qtype: "A".to_owned(),
                        qname: original_qname.to_owned(),
                        content: a_record,
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                if (qtype == "ANY" || qtype == "TXT") && record.dns_challenge.is_some() {
                    // Add a "TXT" record with the dns challenge content.
                    let ns_record = PdnsLookupResponse {
                        qtype: "TXT".to_owned(),
                        qname: original_qname.to_owned(),
                        content: record.dns_challenge.unwrap(),
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                return pdns_response_as_iron(&pdns_response);
            }
            Err(_) => {
                // No such domain, return a `false` result to PowerDNS.
                return pdns_failure("No record for this name.");
            }
        }
    }

    pdns_failure(&format!("Unsupported method: {}", input.method))
}
<commit_msg>Split out the http specific parts of DNS requests<commit_after>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Communication with the PowerDNS server happens through the http
// server.
// See https://doc.powerdns.com/md/authoritative/backend-remote/ for
// details about the various requests and responses.

use config::Config;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use errors::*;
use iron::headers::ContentType;
use iron::prelude::*;
use iron::status::{self, Status};
use serde_json;
use std::io::Read;

#[derive(Deserialize)]
struct PdnsRequestParameters {
    // intialize method
    path: Option<String>,
    timeout: Option<String>,

    // lookup method
    qtype: Option<String>,
    qname: Option<String>,
    #[serde(rename="zone-id")]
    zone_id: Option<i32>,
    remote: Option<String>,
    local: Option<String>,
    real_remote: Option<String>,
}

#[derive(Deserialize)]
struct PdnsRequest {
    method: String,
    parameters: PdnsRequestParameters,
}

#[derive(Serialize)]
pub struct PdnsLookupResponse {
    qtype: String,
    qname: String,
    content: String,
    ttl: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    domain_id: Option<String>,
    #[serde(rename="scopeMask")]
    #[serde(skip_serializing_if = "Option::is_none")]
    scope_mask: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    auth: Option<String>,
}

#[derive(Serialize)]
#[serde(untagged)]
pub enum PdnsResponseParams {
    Lookup(PdnsLookupResponse),
}

#[derive(Serialize)]
pub struct PdnsResponse {
    result: Vec<PdnsResponseParams>,
}

fn pdns_failure_as_iron(reason: &str) -> IronResult<Response> {
    debug!("pdns_failure: {}", reason);
    let mut response = Response::with("{\"result\":false}");
    response.status = Some(Status::Ok);
    response.headers.set(ContentType::json());
    Ok(response)
}

pub fn pdns_response_as_iron(response: &PdnsResponse) -> IronResult<Response> {
    match serde_json::to_string(response) {
        Ok(serialized) => {
            debug!("Response is: {}", serialized);
            let mut response = Response::with(serialized);
            response.status = Some(Status::Ok);
            response.headers.set(ContentType::json());

            Ok(response)
        }
        Err(err) => {
            error!("{}", err);
            EndpointError::with(status::InternalServerError, 501)
        }
    }
}

// Returns a SOA record for a given qname.
pub fn soa_response(qname: &str, config: &Config) -> PdnsLookupResponse {
    PdnsLookupResponse {
        qtype: "SOA".to_owned(),
        qname: qname.to_owned(),
        content: config.soa_content.to_owned(),
        ttl: config.dns_ttl,
        domain_id: None,
        scope_mask: None,
        auth: None,
    }
}

pub fn pakegite_query(qname: &str, qtype: &str, config: &Config) -> Result<PdnsResponse, String> {
    // Pagekite sends dns requests to qnames like:
    // dd7251eef7c773a192feb06c0e07ac6020ac.tc730a6b9e2f28f407bb3871e98d3fe4e60c.625558ecb0d283a5b058ba88fb3d9aa11d48.https-4443.fabrice.box.knilxof.org.box.knilxof.org
    // See https://pagekite.net/wiki/Howto/DnsBasedAuthentication
    debug!("PageKite query for {} {}", qtype, qname);

    let mut pdns_response = PdnsResponse { result: Vec::new() };

    if qtype == "SOA" {
        pdns_response
            .result
            .push(PdnsResponseParams::Lookup(soa_response(qname, config)));
        return Ok(pdns_response);
    }

    if qtype != "A" && qtype != "ANY" {
        return Err(format!("Unsupported PageKite request type: {}", qtype));
    }

    // Split up the qname.
    let parts: Vec<&str> = qname.split('.').collect();
    let subdomain = format!("{}.box.{}.", parts[4], config.domain);
    let ip = match config
              .domain_db
              .get_record_by_name(&subdomain)
              .recv()
              .unwrap() {
        Ok(record) => {
            let srand = parts[0];
            let token = parts[1];
            let sign = parts[2];
            let proto = parts[3];
            let kite_domain = format!("{}.box.{}", parts[4], config.domain);
            let payload = format!("{}:{}:{}:{}", proto, kite_domain, srand, token);
            let salt = sign[..8].to_owned();

            debug!("{} {} {} {} {}", srand, token, sign, proto, kite_domain);

            let mut hasher = Sha1::new();
            hasher.input_str(&format!("{}{}{}", record.token, payload, salt));
            let calc = hasher.result_str();

            let calc_sub = calc[..28].to_owned();
            let sign_sub = sign[8..36].to_owned();

            debug!("Signatures: {} {}", calc_sub, sign_sub);

            if calc_sub == sign_sub {
                "255.255.254.255"
            } else {
                "255.255.255.1"
            }
        }
        Err(_) => {
            // Return 255.255.255.0 to PageKite to indicate failure.
            "255.255.255.0"
        }
    };

    let ns_record = PdnsLookupResponse {
        qtype: "A".to_owned(),
        qname: qname.to_owned(),
        content: ip.to_owned(),
        ttl: config.dns_ttl,
        domain_id: None,
        scope_mask: None,
        auth: None,
    };
    pdns_response
        .result
        .push(PdnsResponseParams::Lookup(ns_record));

    Ok(pdns_response)
}

fn process_request(req: PdnsRequest, config: &Config) -> Result<PdnsResponse, String> {
    debug!("pdns request is {}", req.method);

    if req.method == "lookup" {
        let original_qname = req.parameters.qname.unwrap().to_lowercase();
        let mut qname = original_qname.clone();
        let qtype = req.parameters.qtype.unwrap();
        debug!("lookup for qtype={} qname={}", qtype, original_qname);

        // Example payload:
        //
        // {"method": "lookup",
        //  "parameters": {"local": "0.0.0.0",
        //                 "qname": "fabrice.box.knilxof.org.",
        //                 "qtype": "SOA",
        //                 "real-remote": "63.245.221.198/32",
        //                 "remote": "63.245.221.198",
        //                 "zone-id": -1}}

        // If the qname ends up with .box.$domain.box.$domain. we consider that it's a
        // PageKite request and process it separately.
        if qname.ends_with(&format!(".box.{}.box.{}.", config.domain, config.domain)) {
            return pakegite_query(&qname, &qtype, config);
        }

        // If the qname starts with `_acme-challenge.` this is a DNS-01 challenge verification,
        // so remove that part of the domain to retrieve our record.
        // See https://tools.ietf.org/html/draft-ietf-acme-acme-06#section-8.4
        if qname.starts_with("_acme-challenge.") {
            qname = qname[16..].to_owned();
        }

        debug!("final qname={}", qname);

        // Look for a record with for the qname.
        match config
                  .domain_db
                  .get_record_by_name(&qname)
                  .recv()
                  .unwrap() {
            Ok(record) => {
                if record.local_ip.is_none() && qtype == "A" {
                    // No info on this domain, bail out.
                    return Err("No local_ip".to_owned());
                }

                // Choose either the local or public ip based on the parameters.remote one.
                let a_record = if record.public_ip.is_some() &&
                                  req.parameters.remote.unwrap() == record.public_ip.unwrap() {
                    // We are inside of the home network, return the local ip for the A record.
                    record.local_ip.unwrap()
                } else {
                    // We are outside of the home network, return the ip of the tunnel
                    // for the A record.
                    config.tunnel_ip.to_owned()
                };

                let mut pdns_response = PdnsResponse { result: Vec::new() };

                if qtype == "SOA" {
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(soa_response(&original_qname, config)));
                }

                if qtype == "ANY" || qtype == "A" {
                    // Add an "A" record.
                    let ns_record = PdnsLookupResponse {
                        qtype: "A".to_owned(),
                        qname: original_qname.to_owned(),
                        content: a_record,
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                if (qtype == "ANY" || qtype == "TXT") && record.dns_challenge.is_some() {
                    // Add a "TXT" record with the dns challenge content.
                    let ns_record = PdnsLookupResponse {
                        qtype: "TXT".to_owned(),
                        qname: original_qname.to_owned(),
                        content: record.dns_challenge.unwrap(),
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                return Ok(pdns_response);
            }
            Err(_) => {
                // No such domain, return a `false` result to PowerDNS.
                return Err("No record for this name.".to_owned());
            }
        }
    }

    Err(format!("Unsupported method: {}", req.method))
}

pub fn pdns_endpoint(req: &mut Request, config: &Config) -> IronResult<Response> {
    use std::net::SocketAddr::V4;
    use std::net::Ipv4Addr;

    info!("GET /pdns");
    // Only allow clients from localhost.
    match req.remote_addr {
        V4(addr) => {
            if addr.ip() != &Ipv4Addr::new(127, 0, 0, 1) {
                return EndpointError::with(status::BadRequest, 400);
            }
        }
        _ => return EndpointError::with(status::BadRequest, 400),
    }

    // Read the request from the json body.
    let mut s = String::new();
    itry!(req.body.read_to_string(&mut s));

    debug!("Body is: {}", s);

    let input: PdnsRequest = match serde_json::from_str(&s) {
        Ok(value) => value,
        Err(err) => {
            error!("Bad request: {}", err);
            return EndpointError::with(status::BadRequest, 400);
        }
    };

    match process_request(input, config) {
        Ok(ref response) => pdns_response_as_iron(response),
        Err(err) => pdns_failure_as_iron(&err),
    }

}
<|endoftext|>


<commit_before>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Communication with the PowerDNS server happens through the http
// server.
// See https://doc.powerdns.com/md/authoritative/backend-remote/ for
// details about the various requests and responses.

use config::Config;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use errors::*;
use iron::headers::ContentType;
use iron::prelude::*;
use iron::status::{self, Status};
use serde_json;
use std::io::Read;

#[derive(Deserialize)]
struct PdnsRequestParameters {
    // intialize method
    path: Option<String>,
    timeout: Option<String>,

    // lookup method
    qtype: Option<String>,
    qname: Option<String>,
    #[serde(rename="zone-id")]
    zone_id: Option<i32>,
    remote: Option<String>,
    local: Option<String>,
    real_remote: Option<String>,
}

#[derive(Deserialize)]
struct PdnsRequest {
    method: String,
    parameters: PdnsRequestParameters,
}

#[derive(Serialize)]
pub struct PdnsLookupResponse {
    qtype: String,
    qname: String,
    content: String,
    ttl: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    domain_id: Option<String>,
    #[serde(rename="scopeMask")]
    #[serde(skip_serializing_if = "Option::is_none")]
    scope_mask: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    auth: Option<String>,
}

#[derive(Serialize)]
#[serde(untagged)]
pub enum PdnsResponseParams {
    Lookup(PdnsLookupResponse),
}

#[derive(Serialize)]
pub struct PdnsResponse {
    result: Vec<PdnsResponseParams>,
}

fn pdns_failure(reason: &str) -> IronResult<Response> {
    debug!("pdns_failure: {}", reason);
    let mut response = Response::with("{\"result\":false}");
    response.status = Some(Status::Ok);
    response.headers.set(ContentType::json());
    Ok(response)
}

pub fn pdns_response_as_iron(response: &PdnsResponse) -> IronResult<Response> {
    match serde_json::to_string(response) {
        Ok(serialized) => {
            debug!("Response is: {}", serialized);
            let mut response = Response::with(serialized);
            response.status = Some(Status::Ok);
            response.headers.set(ContentType::json());

            Ok(response)
        }
        Err(err) => {
            error!("{}", err);
            EndpointError::with(status::InternalServerError, 501)
        }
    }
}

pub fn pakegite_query(qname: &str, qtype: &str, config: &Config) -> IronResult<Response> {
    // Pagekite sends dns requests like:
    // dd7251eef7c773a192feb06c0e07ac6020ac.tc730a6b9e2f28f407bb3871e98d3fe4e60c.625558ecb0d283a5b058ba88fb3d9aa11d48.https-4443.fabrice.box.knilxof.org.box.knilxof.org
    // See https://pagekite.net/wiki/Howto/DnsBasedAuthentication
    debug!("PageKite query for {} {}", qtype, qname);

    let mut pdns_response = PdnsResponse { result: Vec::new() };

    if qtype == "SOA" {
        // Add a "SOA" record.
        // TODO: don't hardcode the content of this record!
        let ns_record = PdnsLookupResponse {
            qtype: "SOA".to_owned(),
            qname: qname.to_owned(),
            content: "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800"
                .to_owned(),
            ttl: config.dns_ttl,
            domain_id: None,
            scope_mask: None,
            auth: None,
        };
        pdns_response
            .result
            .push(PdnsResponseParams::Lookup(ns_record));
        return pdns_response_as_iron(&pdns_response);
    }

    if qtype != "A" && qtype != "ANY" {
        return pdns_failure(&format!("Unsupported PageKite request type: {}", qtype));
    }

    // Split up the qname.
    let parts: Vec<&str> = qname.split('.').collect();
    let subdomain = format!("{}.box.{}.", parts[4], config.domain);
    let ip = match config
              .domain_db
              .get_record_by_name(&subdomain)
              .recv()
              .unwrap() {
        Ok(record) => {
            let srand = parts[0];
            let token = parts[1];
            let sign = parts[2];
            let proto = parts[3];
            let kite_domain = format!("{}.box.{}", parts[4], config.domain);
            let payload = format!("{}:{}:{}:{}", proto, kite_domain, srand, token);
            let salt = sign[..8].to_owned();

            debug!("{} {} {} {} {}", srand, token, sign, proto, kite_domain);

            let mut hasher = Sha1::new();
            hasher.input_str(&format!("{}{}{}", record.token, payload, salt));
            let calc = hasher.result_str();

            let calc_sub = calc[..28].to_owned();
            let sign_sub = sign[8..36].to_owned();

            debug!("Signatures: {} {}", calc_sub, sign_sub);

            if calc_sub == sign_sub {
                "255.255.254.255"
            } else {
                "255.255.255.1"
            }
        }
        Err(_) => {
            // Return 255.255.255.0 to PageKite to indicate failure.
            "255.255.255.0"
        }
    };

    let ns_record = PdnsLookupResponse {
        qtype: "A".to_owned(),
        qname: qname.to_owned(),
        content: ip.to_owned(),
        ttl: config.dns_ttl,
        domain_id: None,
        scope_mask: None,
        auth: None,
    };
    pdns_response
        .result
        .push(PdnsResponseParams::Lookup(ns_record));

    pdns_response_as_iron(&pdns_response)
}

pub fn pdns_endpoint(req: &mut Request, config: &Config) -> IronResult<Response> {
    use std::net::SocketAddr::V4;
    use std::net::Ipv4Addr;

    info!("GET /pdns");
    // Only allow clients from localhost.
    match req.remote_addr {
        V4(addr) => {
            if addr.ip() != &Ipv4Addr::new(127, 0, 0, 1) {
                return EndpointError::with(status::BadRequest, 400)
            }
        },
        _ => return EndpointError::with(status::BadRequest, 400)
    }

    // Read the request from the json body.
    let mut s = String::new();
    itry!(req.body.read_to_string(&mut s));

    debug!("Body is: {}", s);

    let input: PdnsRequest = match serde_json::from_str(&s) {
        Ok(value) => value,
        Err(err) => {
            error!("Bad request: {}", err);
            return EndpointError::with(status::BadRequest, 400);
        }
    };

    debug!("pdns request is {}", input.method);

    if input.method == "lookup" {
        let original_qname = input.parameters.qname.unwrap().to_lowercase();
        let mut qname = original_qname.clone();
        let qtype = input.parameters.qtype.unwrap();
        debug!("lookup for qtype={} qname={}", qtype, original_qname);

        // Example payload:
        //
        // {"method": "lookup",
        //  "parameters": {"local": "0.0.0.0",
        //                 "qname": "fabrice.box.knilxof.org.",
        //                 "qtype": "SOA",
        //                 "real-remote": "63.245.221.198/32",
        //                 "remote": "63.245.221.198",
        //                 "zone-id": -1}}

        // If the qname ends up with .box.$domain.box.$domain. we consider that it's a PageKite request.
        // When finding such a request, treat it separately.
        if qname.ends_with(&format!(".box.{}.box.{}.", config.domain, config.domain)) {
            return pakegite_query(&qname, &qtype, config);
        }

        // If the qname starts with `_acme-challenge.` this is a DNS-01 challenge verification,
        // so remove that part of the domain to retrieve our record.
        // See https://tools.ietf.org/html/draft-ietf-acme-acme-06#section-8.4
        if qname.starts_with("_acme-challenge.") {
            qname = qname[16..].to_owned();
        }

        debug!("final qname={}", qname);

        // Look for a record with for the qname.
        match config
                  .domain_db
                  .get_record_by_name(&qname)
                  .recv()
                  .unwrap() {
            Ok(record) => {
                if record.local_ip.is_none() && qtype == "A" {
                    // No info on this domain, bail out.
                    return pdns_failure("No local_ip");
                }

                // Choose either the local or public ip based on the parameters.remote one.
                let a_record = if record.public_ip.is_some() &&
                                  input.parameters.remote.unwrap() == record.public_ip.unwrap() {
                    // We are inside of the home network, return the local ip for the A record.
                    record.local_ip.unwrap()
                } else {
                    // We are outside of the home network, return the ip of the tunnel
                    // for the A record.
                    config.tunnel_ip.to_owned()
                };

                let mut pdns_response = PdnsResponse { result: Vec::new() };

                if qtype == "SOA" {
                    // Add a "SOA" record.
                    // TODO: don't hardcode the content of this record!
                    let ns_record = PdnsLookupResponse {
                        qtype: "SOA".to_owned(),
                        qname: original_qname.to_owned(),
                        content: "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800"
                            .to_owned(),
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                if qtype == "ANY" || qtype == "A" {
                    // Add an "A" record.
                    let ns_record = PdnsLookupResponse {
                        qtype: "A".to_owned(),
                        qname: original_qname.to_owned(),
                        content: a_record,
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                if (qtype == "ANY" || qtype == "TXT") && record.dns_challenge.is_some() {
                    // Add a "TXT" record with the dns challenge content.
                    let ns_record = PdnsLookupResponse {
                        qtype: "TXT".to_owned(),
                        qname: original_qname.to_owned(),
                        content: record.dns_challenge.unwrap(),
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                return pdns_response_as_iron(&pdns_response);
            }
            Err(_) => {
                // No such domain, return a `false` result to PowerDNS.
                return pdns_failure("No record for this name.");
            }
        }
    }

    pdns_failure(&format!("Unsupported method: {}", input.method))
}
<commit_msg>Share the SOA record building code<commit_after>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Communication with the PowerDNS server happens through the http
// server.
// See https://doc.powerdns.com/md/authoritative/backend-remote/ for
// details about the various requests and responses.

use config::Config;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use errors::*;
use iron::headers::ContentType;
use iron::prelude::*;
use iron::status::{self, Status};
use serde_json;
use std::io::Read;

#[derive(Deserialize)]
struct PdnsRequestParameters {
    // intialize method
    path: Option<String>,
    timeout: Option<String>,

    // lookup method
    qtype: Option<String>,
    qname: Option<String>,
    #[serde(rename="zone-id")]
    zone_id: Option<i32>,
    remote: Option<String>,
    local: Option<String>,
    real_remote: Option<String>,
}

#[derive(Deserialize)]
struct PdnsRequest {
    method: String,
    parameters: PdnsRequestParameters,
}

#[derive(Serialize)]
pub struct PdnsLookupResponse {
    qtype: String,
    qname: String,
    content: String,
    ttl: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    domain_id: Option<String>,
    #[serde(rename="scopeMask")]
    #[serde(skip_serializing_if = "Option::is_none")]
    scope_mask: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    auth: Option<String>,
}

#[derive(Serialize)]
#[serde(untagged)]
pub enum PdnsResponseParams {
    Lookup(PdnsLookupResponse),
}

#[derive(Serialize)]
pub struct PdnsResponse {
    result: Vec<PdnsResponseParams>,
}

fn pdns_failure(reason: &str) -> IronResult<Response> {
    debug!("pdns_failure: {}", reason);
    let mut response = Response::with("{\"result\":false}");
    response.status = Some(Status::Ok);
    response.headers.set(ContentType::json());
    Ok(response)
}

pub fn pdns_response_as_iron(response: &PdnsResponse) -> IronResult<Response> {
    match serde_json::to_string(response) {
        Ok(serialized) => {
            debug!("Response is: {}", serialized);
            let mut response = Response::with(serialized);
            response.status = Some(Status::Ok);
            response.headers.set(ContentType::json());

            Ok(response)
        }
        Err(err) => {
            error!("{}", err);
            EndpointError::with(status::InternalServerError, 501)
        }
    }
}

// Returns a SOA record for a given qname.
// TODO: Don't hardcode the content of this record!
pub fn soa_response(qname: &str, config: &Config) -> PdnsLookupResponse {
    PdnsLookupResponse {
        qtype: "SOA".to_owned(),
        qname: qname.to_owned(),
        content: "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800"
            .to_owned(),
        ttl: config.dns_ttl,
        domain_id: None,
        scope_mask: None,
        auth: None,
    }
}

pub fn pakegite_query(qname: &str, qtype: &str, config: &Config) -> IronResult<Response> {
    // Pagekite sends dns requests to qnames like:
    // dd7251eef7c773a192feb06c0e07ac6020ac.tc730a6b9e2f28f407bb3871e98d3fe4e60c.625558ecb0d283a5b058ba88fb3d9aa11d48.https-4443.fabrice.box.knilxof.org.box.knilxof.org
    // See https://pagekite.net/wiki/Howto/DnsBasedAuthentication
    debug!("PageKite query for {} {}", qtype, qname);

    let mut pdns_response = PdnsResponse { result: Vec::new() };

    if qtype == "SOA" {
        pdns_response
            .result
            .push(PdnsResponseParams::Lookup(soa_response(qname, config)));
        return pdns_response_as_iron(&pdns_response);
    }

    if qtype != "A" && qtype != "ANY" {
        return pdns_failure(&format!("Unsupported PageKite request type: {}", qtype));
    }

    // Split up the qname.
    let parts: Vec<&str> = qname.split('.').collect();
    let subdomain = format!("{}.box.{}.", parts[4], config.domain);
    let ip = match config
              .domain_db
              .get_record_by_name(&subdomain)
              .recv()
              .unwrap() {
        Ok(record) => {
            let srand = parts[0];
            let token = parts[1];
            let sign = parts[2];
            let proto = parts[3];
            let kite_domain = format!("{}.box.{}", parts[4], config.domain);
            let payload = format!("{}:{}:{}:{}", proto, kite_domain, srand, token);
            let salt = sign[..8].to_owned();

            debug!("{} {} {} {} {}", srand, token, sign, proto, kite_domain);

            let mut hasher = Sha1::new();
            hasher.input_str(&format!("{}{}{}", record.token, payload, salt));
            let calc = hasher.result_str();

            let calc_sub = calc[..28].to_owned();
            let sign_sub = sign[8..36].to_owned();

            debug!("Signatures: {} {}", calc_sub, sign_sub);

            if calc_sub == sign_sub {
                "255.255.254.255"
            } else {
                "255.255.255.1"
            }
        }
        Err(_) => {
            // Return 255.255.255.0 to PageKite to indicate failure.
            "255.255.255.0"
        }
    };

    let ns_record = PdnsLookupResponse {
        qtype: "A".to_owned(),
        qname: qname.to_owned(),
        content: ip.to_owned(),
        ttl: config.dns_ttl,
        domain_id: None,
        scope_mask: None,
        auth: None,
    };
    pdns_response
        .result
        .push(PdnsResponseParams::Lookup(ns_record));

    pdns_response_as_iron(&pdns_response)
}

pub fn pdns_endpoint(req: &mut Request, config: &Config) -> IronResult<Response> {
    use std::net::SocketAddr::V4;
    use std::net::Ipv4Addr;

    info!("GET /pdns");
    // Only allow clients from localhost.
    match req.remote_addr {
        V4(addr) => {
            if addr.ip() != &Ipv4Addr::new(127, 0, 0, 1) {
                return EndpointError::with(status::BadRequest, 400);
            }
        }
        _ => return EndpointError::with(status::BadRequest, 400),
    }

    // Read the request from the json body.
    let mut s = String::new();
    itry!(req.body.read_to_string(&mut s));

    debug!("Body is: {}", s);

    let input: PdnsRequest = match serde_json::from_str(&s) {
        Ok(value) => value,
        Err(err) => {
            error!("Bad request: {}", err);
            return EndpointError::with(status::BadRequest, 400);
        }
    };

    debug!("pdns request is {}", input.method);

    if input.method == "lookup" {
        let original_qname = input.parameters.qname.unwrap().to_lowercase();
        let mut qname = original_qname.clone();
        let qtype = input.parameters.qtype.unwrap();
        debug!("lookup for qtype={} qname={}", qtype, original_qname);

        // Example payload:
        //
        // {"method": "lookup",
        //  "parameters": {"local": "0.0.0.0",
        //                 "qname": "fabrice.box.knilxof.org.",
        //                 "qtype": "SOA",
        //                 "real-remote": "63.245.221.198/32",
        //                 "remote": "63.245.221.198",
        //                 "zone-id": -1}}

        // If the qname ends up with .box.$domain.box.$domain. we consider that it's a
        // PageKite request and process it separately.
        if qname.ends_with(&format!(".box.{}.box.{}.", config.domain, config.domain)) {
            return pakegite_query(&qname, &qtype, config);
        }

        // If the qname starts with `_acme-challenge.` this is a DNS-01 challenge verification,
        // so remove that part of the domain to retrieve our record.
        // See https://tools.ietf.org/html/draft-ietf-acme-acme-06#section-8.4
        if qname.starts_with("_acme-challenge.") {
            qname = qname[16..].to_owned();
        }

        debug!("final qname={}", qname);

        // Look for a record with for the qname.
        match config
                  .domain_db
                  .get_record_by_name(&qname)
                  .recv()
                  .unwrap() {
            Ok(record) => {
                if record.local_ip.is_none() && qtype == "A" {
                    // No info on this domain, bail out.
                    return pdns_failure("No local_ip");
                }

                // Choose either the local or public ip based on the parameters.remote one.
                let a_record = if record.public_ip.is_some() &&
                                  input.parameters.remote.unwrap() == record.public_ip.unwrap() {
                    // We are inside of the home network, return the local ip for the A record.
                    record.local_ip.unwrap()
                } else {
                    // We are outside of the home network, return the ip of the tunnel
                    // for the A record.
                    config.tunnel_ip.to_owned()
                };

                let mut pdns_response = PdnsResponse { result: Vec::new() };

                if qtype == "SOA" {
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(soa_response(&original_qname, config)));
                }

                if qtype == "ANY" || qtype == "A" {
                    // Add an "A" record.
                    let ns_record = PdnsLookupResponse {
                        qtype: "A".to_owned(),
                        qname: original_qname.to_owned(),
                        content: a_record,
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                if (qtype == "ANY" || qtype == "TXT") && record.dns_challenge.is_some() {
                    // Add a "TXT" record with the dns challenge content.
                    let ns_record = PdnsLookupResponse {
                        qtype: "TXT".to_owned(),
                        qname: original_qname.to_owned(),
                        content: record.dns_challenge.unwrap(),
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                return pdns_response_as_iron(&pdns_response);
            }
            Err(_) => {
                // No such domain, return a `false` result to PowerDNS.
                return pdns_failure("No record for this name.");
            }
        }
    }

    pdns_failure(&format!("Unsupported method: {}", input.method))
}
<|endoftext|>


<commit_before>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Communication with the PowerDNS server happens through the http
// server.
// See https://doc.powerdns.com/md/authoritative/backend-remote/ for
// details about the various requests and responses.

use config::Config;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use errors::*;
use iron::headers::ContentType;
use iron::prelude::*;
use iron::status::{self, Status};
use serde_json;
use std::io::Read;

#[derive(Deserialize)]
struct PdnsRequestParameters {
    // intialize method
    path: Option<String>,
    timeout: Option<String>,

    // lookup method
    qtype: Option<String>,
    qname: Option<String>,
    #[serde(rename="zone-id")]
    zone_id: Option<i32>,
    remote: Option<String>,
    local: Option<String>,
    real_remote: Option<String>,
}

#[derive(Deserialize)]
struct PdnsRequest {
    method: String,
    parameters: PdnsRequestParameters,
}

#[derive(Serialize)]
pub struct PdnsLookupResponse {
    qtype: String,
    qname: String,
    content: String,
    ttl: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    domain_id: Option<String>,
    #[serde(rename="scopeMask")]
    #[serde(skip_serializing_if = "Option::is_none")]
    scope_mask: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    auth: Option<String>,
}

#[derive(Serialize)]
#[serde(untagged)]
pub enum PdnsResponseParams {
    Lookup(PdnsLookupResponse),
}

#[derive(Serialize)]
pub struct PdnsResponse {
    result: Vec<PdnsResponseParams>,
}

fn pdns_failure(reason: &str) -> IronResult<Response> {
    debug!("pdns_failure: {}", reason);
    let mut response = Response::with("{\"result\":false}");
    response.status = Some(Status::Ok);
    response.headers.set(ContentType::json());
    Ok(response)
}

pub fn pdns_response_as_iron(response: &PdnsResponse) -> IronResult<Response> {
    match serde_json::to_string(response) {
        Ok(serialized) => {
            debug!("Response is: {}", serialized);
            let mut response = Response::with(serialized);
            response.status = Some(Status::Ok);
            response.headers.set(ContentType::json());

            Ok(response)
        }
        Err(err) => {
            error!("{}", err);
            EndpointError::with(status::InternalServerError, 501)
        }
    }
}

pub fn pakegite_query(qname: &str, qtype: &str, config: &Config) -> IronResult<Response> {
    // Pagekite sends dns requests like:
    // dd7251eef7c773a192feb06c0e07ac6020ac.tc730a6b9e2f28f407bb3871e98d3fe4e60c.625558ecb0d283a5b058ba88fb3d9aa11d48.https-4443.fabrice.box.knilxof.org.box.knilxof.org
    // See https://pagekite.net/wiki/Howto/DnsBasedAuthentication
    debug!("PageKite query for {} {}", qtype, qname);

    let mut pdns_response = PdnsResponse { result: Vec::new() };

    if qtype == "SOA" {
        // Add a "SOA" record.
        // TODO: don't hardcode the content of this record!
        let ns_record = PdnsLookupResponse {
            qtype: "SOA".to_owned(),
            qname: qname.to_owned(),
            content: "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800"
                .to_owned(),
            ttl: config.dns_ttl,
            domain_id: None,
            scope_mask: None,
            auth: None,
        };
        pdns_response
            .result
            .push(PdnsResponseParams::Lookup(ns_record));
        return pdns_response_as_iron(&pdns_response);
    }

    if qtype != "A" && qtype != "ANY" {
        return pdns_failure(&format!("Unsupported PageKite request type: {}", qtype));
    }

    // Split up the qname.
    let parts: Vec<&str> = qname.split('.').collect();
    let subdomain = format!("{}.box.{}.", parts[4], config.domain);
    let ip = match config
              .domain_db
              .get_record_by_name(&subdomain)
              .recv()
              .unwrap() {
        Ok(record) => {
            let srand = parts[0];
            let token = parts[1];
            let sign = parts[2];
            let proto = parts[3];
            let kite_domain = format!("{}.box.{}", parts[4], config.domain);
            let payload = format!("{}:{}:{}:{}", proto, kite_domain, srand, token);
            let salt = sign[..8].to_owned();

            debug!("{} {} {} {} {}", srand, token, sign, proto, kite_domain);

            let mut hasher = Sha1::new();
            hasher.input_str(&format!("{}{}{}", record.token, payload, salt));
            let calc = hasher.result_str();

            let calc_sub = calc[..28].to_owned();
            let sign_sub = sign[8..36].to_owned();

            debug!("Signatures: {} {}", calc_sub, sign_sub);

            if calc_sub == sign_sub {
                "255.255.254.255"
            } else {
                "255.255.255.1"
            }
        }
        Err(_) => {
            // Return 255.255.255.0 to PageKite to indicate failure.
            "255.255.255.0"
        }
    };

    let ns_record = PdnsLookupResponse {
        qtype: "A".to_owned(),
        qname: qname.to_owned(),
        content: ip.to_owned(),
        ttl: config.dns_ttl,
        domain_id: None,
        scope_mask: None,
        auth: None,
    };
    pdns_response
        .result
        .push(PdnsResponseParams::Lookup(ns_record));

    pdns_response_as_iron(&pdns_response)
}

pub fn pdns_endpoint(req: &mut Request, config: &Config) -> IronResult<Response> {
    // TODO: check where the request is coming from and only allow from pdns.

    info!("GET /pdns");

    // Read the request from the json body.
    let mut s = String::new();
    itry!(req.body.read_to_string(&mut s));

    debug!("Body is: {}", s);

    let input: PdnsRequest = match serde_json::from_str(&s) {
        Ok(value) => value,
        Err(err) => {
            error!("Bad request: {}", err);
            return EndpointError::with(status::BadRequest, 400);
        }
    };

    debug!("pdns request is {}", input.method);

    if input.method == "lookup" {
        let original_qname = input.parameters.qname.unwrap().to_lowercase();
        let mut qname = original_qname.clone();
        let qtype = input.parameters.qtype.unwrap();
        debug!("lookup for qtype={} qname={}", qtype, original_qname);

        // Example payload:
        //
        // {"method": "lookup",
        //  "parameters": {"local": "0.0.0.0",
        //                 "qname": "fabrice.box.knilxof.org.",
        //                 "qtype": "SOA",
        //                 "real-remote": "63.245.221.198/32",
        //                 "remote": "63.245.221.198",
        //                 "zone-id": -1}}

        // If the qname ends up with .box.$domain.box.$domain. we consider that it's a PageKite request.
        // When finding such a request, treat it separately.
        if qname.ends_with(&format!(".box.{}.box.{}.", config.domain, config.domain)) {
            return pakegite_query(&qname, &qtype, config);
        }

        // If the qname starts with `_acme-challenge.` this is a DNS-01 challenge verification,
        // so remove that part of the domain to retrieve our record.
        // See https://tools.ietf.org/html/draft-ietf-acme-acme-06#section-8.4
        if qname.starts_with("_acme-challenge.") {
            qname = qname[16..].to_owned();
        }

        debug!("final qname={}", qname);

        // Look for a record with for the qname.
        match config
                  .domain_db
                  .get_record_by_name(&qname)
                  .recv()
                  .unwrap() {
            Ok(record) => {
                if record.local_ip.is_none() && qtype == "A" {
                    // No info on this domain, bail out.
                    return pdns_failure("No local_ip");
                }

                // Choose either the local or public ip based on the parameters.remote one.
                let a_record = if record.public_ip.is_some() &&
                                  input.parameters.remote.unwrap() == record.public_ip.unwrap() {
                    // We are inside of the home network, return the local ip for the A record.
                    record.local_ip.unwrap()
                } else {
                    // We are outside of the home network, return the ip of the tunnel
                    // for the A record.
                    config.tunnel_ip.to_owned()
                };

                let mut pdns_response = PdnsResponse { result: Vec::new() };

                if qtype == "SOA" {
                    // Add a "SOA" record.
                    // TODO: don't hardcode the content of this record!
                    let ns_record = PdnsLookupResponse {
                        qtype: "SOA".to_owned(),
                        qname: original_qname.to_owned(),
                        content: "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800"
                            .to_owned(),
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                if qtype == "ANY" || qtype == "A" {
                    // Add an "A" record.
                    let ns_record = PdnsLookupResponse {
                        qtype: "A".to_owned(),
                        qname: original_qname.to_owned(),
                        content: a_record,
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                if (qtype == "ANY" || qtype == "TXT") && record.dns_challenge.is_some() {
                    // Add a "TXT" record with the dns challenge content.
                    let ns_record = PdnsLookupResponse {
                        qtype: "TXT".to_owned(),
                        qname: original_qname.to_owned(),
                        content: record.dns_challenge.unwrap(),
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                return pdns_response_as_iron(&pdns_response);
            }
            Err(_) => {
                // No such domain, return a `false` result to PowerDNS.
                return pdns_failure("No record for this name.");
            }
        }
    }

    pdns_failure(&format!("Unsupported method: {}", input.method))
}
<commit_msg>Only allow pdns requests from localhost<commit_after>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Communication with the PowerDNS server happens through the http
// server.
// See https://doc.powerdns.com/md/authoritative/backend-remote/ for
// details about the various requests and responses.

use config::Config;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use errors::*;
use iron::headers::ContentType;
use iron::prelude::*;
use iron::status::{self, Status};
use serde_json;
use std::io::Read;

#[derive(Deserialize)]
struct PdnsRequestParameters {
    // intialize method
    path: Option<String>,
    timeout: Option<String>,

    // lookup method
    qtype: Option<String>,
    qname: Option<String>,
    #[serde(rename="zone-id")]
    zone_id: Option<i32>,
    remote: Option<String>,
    local: Option<String>,
    real_remote: Option<String>,
}

#[derive(Deserialize)]
struct PdnsRequest {
    method: String,
    parameters: PdnsRequestParameters,
}

#[derive(Serialize)]
pub struct PdnsLookupResponse {
    qtype: String,
    qname: String,
    content: String,
    ttl: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    domain_id: Option<String>,
    #[serde(rename="scopeMask")]
    #[serde(skip_serializing_if = "Option::is_none")]
    scope_mask: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    auth: Option<String>,
}

#[derive(Serialize)]
#[serde(untagged)]
pub enum PdnsResponseParams {
    Lookup(PdnsLookupResponse),
}

#[derive(Serialize)]
pub struct PdnsResponse {
    result: Vec<PdnsResponseParams>,
}

fn pdns_failure(reason: &str) -> IronResult<Response> {
    debug!("pdns_failure: {}", reason);
    let mut response = Response::with("{\"result\":false}");
    response.status = Some(Status::Ok);
    response.headers.set(ContentType::json());
    Ok(response)
}

pub fn pdns_response_as_iron(response: &PdnsResponse) -> IronResult<Response> {
    match serde_json::to_string(response) {
        Ok(serialized) => {
            debug!("Response is: {}", serialized);
            let mut response = Response::with(serialized);
            response.status = Some(Status::Ok);
            response.headers.set(ContentType::json());

            Ok(response)
        }
        Err(err) => {
            error!("{}", err);
            EndpointError::with(status::InternalServerError, 501)
        }
    }
}

pub fn pakegite_query(qname: &str, qtype: &str, config: &Config) -> IronResult<Response> {
    // Pagekite sends dns requests like:
    // dd7251eef7c773a192feb06c0e07ac6020ac.tc730a6b9e2f28f407bb3871e98d3fe4e60c.625558ecb0d283a5b058ba88fb3d9aa11d48.https-4443.fabrice.box.knilxof.org.box.knilxof.org
    // See https://pagekite.net/wiki/Howto/DnsBasedAuthentication
    debug!("PageKite query for {} {}", qtype, qname);

    let mut pdns_response = PdnsResponse { result: Vec::new() };

    if qtype == "SOA" {
        // Add a "SOA" record.
        // TODO: don't hardcode the content of this record!
        let ns_record = PdnsLookupResponse {
            qtype: "SOA".to_owned(),
            qname: qname.to_owned(),
            content: "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800"
                .to_owned(),
            ttl: config.dns_ttl,
            domain_id: None,
            scope_mask: None,
            auth: None,
        };
        pdns_response
            .result
            .push(PdnsResponseParams::Lookup(ns_record));
        return pdns_response_as_iron(&pdns_response);
    }

    if qtype != "A" && qtype != "ANY" {
        return pdns_failure(&format!("Unsupported PageKite request type: {}", qtype));
    }

    // Split up the qname.
    let parts: Vec<&str> = qname.split('.').collect();
    let subdomain = format!("{}.box.{}.", parts[4], config.domain);
    let ip = match config
              .domain_db
              .get_record_by_name(&subdomain)
              .recv()
              .unwrap() {
        Ok(record) => {
            let srand = parts[0];
            let token = parts[1];
            let sign = parts[2];
            let proto = parts[3];
            let kite_domain = format!("{}.box.{}", parts[4], config.domain);
            let payload = format!("{}:{}:{}:{}", proto, kite_domain, srand, token);
            let salt = sign[..8].to_owned();

            debug!("{} {} {} {} {}", srand, token, sign, proto, kite_domain);

            let mut hasher = Sha1::new();
            hasher.input_str(&format!("{}{}{}", record.token, payload, salt));
            let calc = hasher.result_str();

            let calc_sub = calc[..28].to_owned();
            let sign_sub = sign[8..36].to_owned();

            debug!("Signatures: {} {}", calc_sub, sign_sub);

            if calc_sub == sign_sub {
                "255.255.254.255"
            } else {
                "255.255.255.1"
            }
        }
        Err(_) => {
            // Return 255.255.255.0 to PageKite to indicate failure.
            "255.255.255.0"
        }
    };

    let ns_record = PdnsLookupResponse {
        qtype: "A".to_owned(),
        qname: qname.to_owned(),
        content: ip.to_owned(),
        ttl: config.dns_ttl,
        domain_id: None,
        scope_mask: None,
        auth: None,
    };
    pdns_response
        .result
        .push(PdnsResponseParams::Lookup(ns_record));

    pdns_response_as_iron(&pdns_response)
}

pub fn pdns_endpoint(req: &mut Request, config: &Config) -> IronResult<Response> {
    use std::net::SocketAddr::V4;
    use std::net::Ipv4Addr;

    info!("GET /pdns");
    // Only allow clients from localhost.
    match req.remote_addr {
        V4(addr) => {
            if addr.ip() != &Ipv4Addr::new(127, 0, 0, 1) {
                return EndpointError::with(status::BadRequest, 400)
            }
        },
        _ => return EndpointError::with(status::BadRequest, 400)
    }

    // Read the request from the json body.
    let mut s = String::new();
    itry!(req.body.read_to_string(&mut s));

    debug!("Body is: {}", s);

    let input: PdnsRequest = match serde_json::from_str(&s) {
        Ok(value) => value,
        Err(err) => {
            error!("Bad request: {}", err);
            return EndpointError::with(status::BadRequest, 400);
        }
    };

    debug!("pdns request is {}", input.method);

    if input.method == "lookup" {
        let original_qname = input.parameters.qname.unwrap().to_lowercase();
        let mut qname = original_qname.clone();
        let qtype = input.parameters.qtype.unwrap();
        debug!("lookup for qtype={} qname={}", qtype, original_qname);

        // Example payload:
        //
        // {"method": "lookup",
        //  "parameters": {"local": "0.0.0.0",
        //                 "qname": "fabrice.box.knilxof.org.",
        //                 "qtype": "SOA",
        //                 "real-remote": "63.245.221.198/32",
        //                 "remote": "63.245.221.198",
        //                 "zone-id": -1}}

        // If the qname ends up with .box.$domain.box.$domain. we consider that it's a PageKite request.
        // When finding such a request, treat it separately.
        if qname.ends_with(&format!(".box.{}.box.{}.", config.domain, config.domain)) {
            return pakegite_query(&qname, &qtype, config);
        }

        // If the qname starts with `_acme-challenge.` this is a DNS-01 challenge verification,
        // so remove that part of the domain to retrieve our record.
        // See https://tools.ietf.org/html/draft-ietf-acme-acme-06#section-8.4
        if qname.starts_with("_acme-challenge.") {
            qname = qname[16..].to_owned();
        }

        debug!("final qname={}", qname);

        // Look for a record with for the qname.
        match config
                  .domain_db
                  .get_record_by_name(&qname)
                  .recv()
                  .unwrap() {
            Ok(record) => {
                if record.local_ip.is_none() && qtype == "A" {
                    // No info on this domain, bail out.
                    return pdns_failure("No local_ip");
                }

                // Choose either the local or public ip based on the parameters.remote one.
                let a_record = if record.public_ip.is_some() &&
                                  input.parameters.remote.unwrap() == record.public_ip.unwrap() {
                    // We are inside of the home network, return the local ip for the A record.
                    record.local_ip.unwrap()
                } else {
                    // We are outside of the home network, return the ip of the tunnel
                    // for the A record.
                    config.tunnel_ip.to_owned()
                };

                let mut pdns_response = PdnsResponse { result: Vec::new() };

                if qtype == "SOA" {
                    // Add a "SOA" record.
                    // TODO: don't hardcode the content of this record!
                    let ns_record = PdnsLookupResponse {
                        qtype: "SOA".to_owned(),
                        qname: original_qname.to_owned(),
                        content: "a.dns.gandi.net hostmaster.gandi.net 1476196782 10800 3600 604800 10800"
                            .to_owned(),
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                if qtype == "ANY" || qtype == "A" {
                    // Add an "A" record.
                    let ns_record = PdnsLookupResponse {
                        qtype: "A".to_owned(),
                        qname: original_qname.to_owned(),
                        content: a_record,
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                if (qtype == "ANY" || qtype == "TXT") && record.dns_challenge.is_some() {
                    // Add a "TXT" record with the dns challenge content.
                    let ns_record = PdnsLookupResponse {
                        qtype: "TXT".to_owned(),
                        qname: original_qname.to_owned(),
                        content: record.dns_challenge.unwrap(),
                        ttl: config.dns_ttl,
                        domain_id: None,
                        scope_mask: None,
                        auth: None,
                    };
                    pdns_response
                        .result
                        .push(PdnsResponseParams::Lookup(ns_record));
                }

                return pdns_response_as_iron(&pdns_response);
            }
            Err(_) => {
                // No such domain, return a `false` result to PowerDNS.
                return pdns_failure("No record for this name.");
            }
        }
    }

    pdns_failure(&format!("Unsupported method: {}", input.method))
}
<|endoftext|>


<commit_before>use std::collections::VecDeque;
use secp256k1::key::PublicKey;

#[derive(Clone)]
struct Peer<'a> {
    ltvk: &'a PublicKey,
    kepks: VecDeque<PublicKey>,
}

impl<'a> Peer<'a> {
    pub fn new(ltvk: &'a PublicKey) -> Self {
        Peer {
            ltvk: ltvk,
            kepks: VecDeque::with_capacity(2),
        }
    }

    pub fn push_kepk(&mut self, kepk: PublicKey) {
        self.kepks.push_back(kepk);
        assert!(self.kepks.len() <= 2);
    }

    pub fn get_kepk(&self) -> &PublicKey {
        self.kepks.get(0).unwrap()
    }

    pub fn shift_keys(&mut self) {
        self.kepks.pop_front().unwrap();
    }
}
<commit_msg>Remove obsolete file<commit_after><|endoftext|>


<commit_before>//!
//! Desync pipes provide a way to generate and process streams via a `Desync` object
//!

use super::desync::*;

use futures::*;

use std::result::Result;

impl<Core: 'static+Send> Desync<Core> {
    ///
    /// Pipes a stream into this object. Whenever an item becomes available on the stream, the
    /// processing function is called asynchronously with the item that was received.
    ///
    pub fn pipe_in<S, ProcessFn>(&self, stream: S, process: ProcessFn)
    where   S:          Stream,
            ProcessFn:  Send+FnMut(&mut Core, Result<S::Item, S::Error>) -> () {
        unimplemented!()
    }

    ///
    /// Pipes a stream into this object. Whenever an item becomes available on the strema, the
    /// processing function is called asynchronously with the item that was received. The
    /// return value is placed onto the output stream.
    ///
    pub fn pipe<S, Output, OutputErr, ProcessFn>(&self, stream: S, process: ProcessFn) -> Box<dyn Stream<Item=Output, Error=OutputErr>>
    where   S:          Stream,
            Output:     Send,
            OutputErr:  Send,
            ProcessFn:  Send+FnMut(&mut Core, Result<S::Item, S::Error>) -> Result<Output, OutputErr> {
        unimplemented!()
    }
}
<commit_msg>First pass at implementing the pipe_in functions<commit_after>//!
//! Desync pipes provide a way to generate and process streams via a `Desync` object
//!

use super::desync::*;

use futures::*;
use futures::executor;

use std::mem;
use std::sync::*;
use std::thread;
use std::thread::JoinHandle;
use std::result::Result;
use std::collections::{HashMap, HashSet};

lazy_static! {
    /// The shared poll thread that's used to schedule events from pipe streams
    static ref POLL_THREAD: PollThread = PollThread::new();
}

///
/// Pipes a stream into a desync object. Whenever an item becomes available on the stream, the
/// processing function is called asynchronously with the item that was received.
///
pub fn pipe_in<Core, S, ProcessFn>(desync: Arc<Desync<Core>>, stream: S, process: ProcessFn)
where   Core:       'static+Send,
        S:          'static+Send+Stream,
        S::Item:    Send,
        S::Error:   Send,
        ProcessFn:  'static+Send+FnMut(&mut Core, Result<S::Item, S::Error>) -> () {

    // Need a mutable version of the stream
    let mut stream = stream;

    // Wrap the process fn up so we can call it asynchronously
    // (it doesn't really need to be in a mutex as it's only called by our object but we need to make it pass Rust's checks and we don't have a way to specify this at the moment)
    let process = Arc::new(Mutex::new(process));

    // Poll the stream on the poll thread
    POLL_THREAD.monitor(move || {
        // Read from the stream and create a new reference to the callback function
        let next        = stream.poll();
        let process     = Arc::clone(&process);

        match next {
            // Just wait if the stream is not ready
            Ok(Async::NotReady) => true,

            // Stream returned a value
            Ok(Async::Ready(Some(next))) => {
                // Process the value on the stream
                desync.async(move |core| {
                    let mut process = process.lock().unwrap();
                    let process     = &mut *process;
                    process(core, Ok(next));
                });

                true
            },

            // Stream returned an error
            Err(e) => {
                // Process the error on the stream
                desync.async(move |core| {
                    let mut process = process.lock().unwrap();
                    let process     = &mut *process;
                    process(core, Err(e));
                });

                true
            },

            // Stream finished
            Ok(Async::Ready(None)) => false
        }
    });
}

/*
///
/// Pipes a stream into this object. Whenever an item becomes available on the strema, the
/// processing function is called asynchronously with the item that was received. The
/// return value is placed onto the output stream.
///
pub fn pipe<Core, S, Output, OutputErr, ProcessFn>(desync: Arc<Desync<Core>>, stream: S, process: ProcessFn) -> Box<dyn Stream<Item=Output, Error=OutputErr>>
where   Core:       'static+Send,
        S:          'static+Send+Stream,
        S::Item:    Send,
        S::Error:   Send,
        Output:     Send,
        OutputErr:  Send,
        ProcessFn:  'static+Send+FnMut(&mut Core, Result<S::Item, S::Error>) -> Result<Output, OutputErr> {
    unimplemented!()
}
*/

///
/// In order to implement the polling functions, we need a thread that runs the executor
/// for any streams that we're currently piping (calling pipe_in or pipe will effectively
/// need to spawn the relevant stream).
///
/// This represents that thread. There's a bit of a limitation in that the `poll` methods
/// for the various streams will block the thread so this may start to bottleneck at times of
/// high load or with streams with poll methods that take significant time to execute.
///
struct PollThread {
    /// The poll functions that are being monitored by this thread
    notifications: Arc<Mutex<PollNotifications>>,

    /// The function that should be called for every notification ID
    poll_functions: Arc<Mutex<HashMap<u32, Box<dyn FnMut() -> bool + Send>>>>,

    /// The joinhandle of the running thread
    thread: Arc<JoinHandle<()>>
}

///
/// Stores things being monitored
///
struct PollNotifications {
    /// Next available ID for a polling function
    next_id: u32,

    /// Poll functions where the notify handle has been set
    notified_ids: HashSet<u32>
}

///
/// Provides the 'Notify' interface for a polling function with a particular ID
///
struct PollNotify {
    /// The ID that should be marked as notified when the callback is made
    id: u32,

    /// The structure where the notifications are stored
    notifications: Arc<Mutex<PollNotifications>>,

    /// The thread that should be notified when this notification occurs
    thread: thread::Thread
}

impl PollThread {
    ///
    /// Creates a new poll thread
    ///
    pub fn new() -> PollThread {
        // Create the monitors for the new thread
        let notifications = PollNotifications {
            next_id:        0,
            notified_ids:   HashSet::new()
        };
        let notifications = Arc::new(Mutex::new(notifications));

        // Create the set of polling functions
        let poll_functions = Arc::new(Mutex::new(HashMap::new()));

        // Run the thread with the monitors
        let thread = Self::run(Arc::clone(&notifications), Arc::clone(&poll_functions));
        let thread = Arc::new(thread);

        // Generate the thread object
        let thread = PollThread {
            notifications:  notifications,
            poll_functions: poll_functions,
            thread:         thread
        };

        thread
    }

    ///
    /// Starts the poll thread running (poll threads cannot currently be stopped)
    ///
    fn run(notifications: Arc<Mutex<PollNotifications>>, functions: Arc<Mutex<HashMap<u32, Box<dyn FnMut() -> bool + Send>>>>) -> JoinHandle<()> {
        thread::spawn(move || {
            loop {
                // Park the thread until there is something to do
                thread::park();

                // Fetch the list of notified functions we should call
                let to_notify = {
                    let mut notifications   = notifications.lock().unwrap();
                    let mut to_notify       = HashSet::new();

                    mem::swap(&mut to_notify, &mut notifications.notified_ids);

                    to_notify
                };

                // Notify each function in turn
                let mut functions   = functions.lock().unwrap();
                let thread          = thread::current();

                for function_id in to_notify {
                    // Fetch the function. If it returns false, we need to remove it from the list
                    let keep_function = functions.get_mut(&function_id)
                        .map(|poll_function| {
                            // Create the notification structure
                            let notify = PollNotify {
                                id:             function_id,
                                notifications:  Arc::clone(&notifications),
                                thread:         thread.clone()
                            };

                            // Call the polling function
                            executor::with_notify(&Arc::new(notify), 0, move || poll_function())
                        });

                    // If the function exists and returns false, then remove it from the set that we're polling
                    if keep_function == Some(false) {
                        functions.remove(&function_id);
                    }
                }
            }
        })
    }

    ///
    /// Adds a polling function to the current thread. It will be called using the futures
    /// notification system (ie, can call things like the stream poll function)
    ///
    pub fn monitor<PollFn>(&self, poll_fn: PollFn)
    where PollFn: 'static+Send+FnMut() -> bool {
        let mut functions       = self.poll_functions.lock().unwrap();
        let mut notifications   = self.notifications.lock().unwrap();

        // Get an ID for this monitor
        let id = notifications.next_id;
        notifications.next_id += 1;

        // Store this item
        functions.insert(id, Box::new(poll_fn));

        // Mark it as notified
        notifications.notified_ids.insert(id);

        // Wake the thread
        self.thread.thread().unpark();
    }
}

impl executor::Notify for PollNotify {
    fn notify(&self, _id: usize) {
        // Add our ID to the notification list
        let mut notifications = self.notifications.lock().unwrap();
        notifications.notified_ids.insert(self.id);

        // Wake the thread
        self.thread.unpark();
    }
}
<|endoftext|>