/* 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/. */

//! A partial implementation of CORS
//! For now this library is XHR-specific.
//! For stuff involving `<img>`, `<iframe>`, `<form>`, etc please check what
//! the request mode should be and compare with the fetch spec
//! This library will eventually become the core of the Fetch crate
//! with CORSRequest being expanded into FetchRequest (etc)

use hyper::client::Request;
use hyper::header::{AccessControlAllowHeaders, AccessControlRequestHeaders};
use hyper::header::{AccessControlAllowMethods, AccessControlRequestMethod};
use hyper::header::{AccessControlAllowOrigin, AccessControlMaxAge};
use hyper::header::{ContentType, Host};
use hyper::header::{HeaderView, Headers};
use hyper::method::Method;
use hyper::mime::{Mime, SubLevel, TopLevel};
use hyper::status::StatusClass::Success;
use net_traits::{AsyncResponseListener, Metadata, NetworkError, ResponseAction};
use network_listener::{NetworkListener, PreInvoke};
use script_runtime::ScriptChan;
use std::ascii::AsciiExt;
use std::borrow::ToOwned;
use std::sync::{Arc, Mutex};
use time::{self, Timespec, now};
use unicase::UniCase;
use url::Url;
use util::thread::spawn_named;

/// Interface for network listeners concerned with CORS checks. Proper network requests
/// should be initiated from this method, based on the response provided.
pub trait AsyncCORSResponseListener {
    fn response_available(&self, response: CORSResponse);
}

#[derive(Clone, HeapSizeOf)]
pub struct CORSRequest {
    pub origin: Url,
    pub destination: Url,
    pub mode: RequestMode,
    #[ignore_heap_size_of = "Defined in hyper"]
    pub method: Method,
    #[ignore_heap_size_of = "Defined in hyper"]
    pub headers: Headers,
    /// CORS preflight flag (https://fetch.spec.whatwg.org/#concept-http-fetch)
    /// Indicates that a CORS preflight request and/or cache check is to be performed
    pub preflight_flag: bool,
}

/// https://fetch.spec.whatwg.org/#concept-request-mode
/// This only covers some of the request modes. The
/// `same-origin` and `no CORS` modes are unnecessary for XHR.
#[derive(PartialEq, Copy, Clone, HeapSizeOf)]
pub enum RequestMode {
    CORS, // CORS
    ForcedPreflight, // CORS-with-forced-preflight
}

impl CORSRequest {
    /// Creates a CORS request if necessary. Will return an error when fetching is forbidden
    pub fn maybe_new(referer: Url,
                     destination: Url,
                     mode: RequestMode,
                     method: Method,
                     headers: Headers,
                     same_origin_data_url_flag: bool)
                     -> Result<Option<CORSRequest>, ()> {
        if referer.origin() == destination.origin() {
            return Ok(None); // Not cross-origin, proceed with a normal fetch
        }
        match destination.scheme() {
            // As per (https://fetch.spec.whatwg.org/#main-fetch 5.1.9), about URLs can be fetched
            // the same as a basic request.
            "about" if destination.path() == "blank" => Ok(None),
            // As per (https://fetch.spec.whatwg.org/#main-fetch 5.1.9), data URLs can be fetched
            // the same as a basic request if the request's method is GET and the
            // same-origin data-URL flag is set.
            "data" if same_origin_data_url_flag && method == Method::Get => Ok(None),
            "http" | "https" => {
                let mut req = CORSRequest::new(referer, destination, mode, method, headers);
                req.preflight_flag = !is_simple_method(&req.method) ||
                                     mode == RequestMode::ForcedPreflight;
                if req.headers.iter().any(|h| !is_simple_header(&h)) {
                    req.preflight_flag = true;
                }
                Ok(Some(req))
            },
            _ => Err(()),
        }
    }

    fn new(mut referer: Url,
           destination: Url,
           mode: RequestMode,
           method: Method,
           headers: Headers)
           -> CORSRequest {
        referer.set_fragment(None);
        referer.set_query(None);
        referer.set_path("");
        CORSRequest {
            origin: referer,
            destination: destination,
            mode: mode,
            method: method,
            headers: headers,
            preflight_flag: false,
        }
    }

    pub fn http_fetch_async(&self,
                            listener: Box<AsyncCORSResponseListener + Send>,
                            script_chan: Box<ScriptChan + Send>) {
        struct CORSContext {
            listener: Box<AsyncCORSResponseListener + Send>,
            response: Option<CORSResponse>,
        }

        // This is shoe-horning the CORSReponse stuff into the rest of the async network
        // framework right now. It would be worth redesigning http_fetch to do this properly.
        impl AsyncResponseListener for CORSContext {
            fn headers_available(&mut self, _metadata: Result<Metadata, NetworkError>) {
            }

            fn data_available(&mut self, _payload: Vec<u8>) {
            }

            fn response_complete(&mut self, _status: Result<(), NetworkError>) {
                let response = self.response.take().unwrap();
                self.listener.response_available(response);
            }
        }
        impl PreInvoke for CORSContext {}

        let context = CORSContext {
            listener: listener,
            response: None,
        };
        let listener = NetworkListener {
            context: Arc::new(Mutex::new(context)),
            script_chan: script_chan,
        };

        // TODO: this exists only to make preflight check non-blocking
        // perhaps should be handled by the resource thread?
        let req = self.clone();
        spawn_named("cors".to_owned(), move || {
            let response = req.http_fetch();
            let mut context = listener.context.lock();
            let context = context.as_mut().unwrap();
            context.response = Some(response);
            listener.notify(ResponseAction::ResponseComplete(Ok(())));
        });
    }

    /// http://fetch.spec.whatwg.org/#concept-http-fetch
    /// This method assumes that the CORS flag is set
    /// This does not perform the full HTTP fetch, rather it handles part of the CORS filtering
    /// if self.mode is ForcedPreflight, then the CORS-with-forced-preflight
    /// fetch flag is set as well
    pub fn http_fetch(&self) -> CORSResponse {
        let response = CORSResponse::new();
        // Step 2: Handle service workers (unimplemented)
        // Step 3
        // Substep 1: Service workers (unimplemented )
        // Substep 2
        let cache = &mut CORSCache(vec!()); // XXXManishearth Should come from user agent
        if self.preflight_flag &&
           !cache.match_method(self, &self.method) &&
           !self.headers.iter().all(|h| is_simple_header(&h) && cache.match_header(self, h.name())) &&
           (!is_simple_method(&self.method) || self.mode == RequestMode::ForcedPreflight) {
            return self.preflight_fetch();
            // Everything after this is part of XHR::fetch()
            // Expect the organization of code to improve once we have a fetch crate
        }
        response
    }

    /// https://fetch.spec.whatwg.org/#cors-preflight-fetch
    fn preflight_fetch(&self) -> CORSResponse {
        let error = CORSResponse::new_error();
        let mut cors_response = CORSResponse::new();

        // Step 1
        let mut preflight = self.clone();
        preflight.method = Method::Options;
        preflight.headers = Headers::new();
        // Step 2
        preflight.headers.set(AccessControlRequestMethod(self.method.clone()));

        // Steps 3-5
        let mut header_names = vec![];
        for header in self.headers.iter() {
            header_names.push(header.name().to_owned());
        }
        header_names.sort();
        preflight.headers
                 .set(AccessControlRequestHeaders(header_names.into_iter().map(UniCase).collect()));

        let preflight_request = Request::new(preflight.method, preflight.destination);
        let mut req = match preflight_request {
            Ok(req) => req,
            Err(_) => return error,
        };

        let host = req.headers().get::<Host>().unwrap().clone();
        *req.headers_mut() = preflight.headers.clone();
        req.headers_mut().set(host);
        let stream = match req.start() {
            Ok(s) => s,
            Err(_) => return error,
        };
        // Step 6
        let response = match stream.send() {
            Ok(r) => r,
            Err(_) => return error,
        };

        // Step 7: We don't perform a CORS check here
        // FYI, fn allow_cross_origin_request() performs the CORS check
        match response.status.class() {
            Success => {}
            _ => return error,
        }
        cors_response.headers = response.headers.clone();
        // Substeps 1-3 (parsing rules: https://fetch.spec.whatwg.org/#http-new-header-syntax)
        let methods_substep4 = [self.method.clone()];
        let mut methods = match response.headers.get() {
            Some(&AccessControlAllowMethods(ref v)) => &**v,
            _ => return error,
        };
        let headers = match response.headers.get() {
            Some(&AccessControlAllowHeaders(ref h)) => h,
            _ => return error,
        };
        // Substep 4
        if methods.is_empty() && preflight.mode == RequestMode::ForcedPreflight {
            methods = &methods_substep4;
        }
        // Substep 5
        if !is_simple_method(&self.method) && !methods.iter().any(|m| m == &self.method) {
            return error;
        }
        // Substep 6
        for h in self.headers.iter() {
            if is_simple_header(&h) {
                continue;
            }
            if !headers.iter().any(|ref h2| h.name().eq_ignore_ascii_case(h2)) {
                return error;
            }
        }
        // Substeps 7-8
        let max_age = match response.headers.get() {
            Some(&AccessControlMaxAge(num)) => num,
            None => 0,
        };
        // Substep 9: Impose restrictions on max-age, if any (unimplemented)
        // Substeps 10-12: Add a cache (partially implemented, XXXManishearth)
        // This cache should come from the user agent, creating a new one here to check
        // for compile time errors
        let cache = &mut CORSCache(vec![]);
        for m in methods {
            let cache_match = cache.match_method_and_update(self, m, max_age);
            if !cache_match {
                cache.insert(CORSCacheEntry::new(self.origin.clone(),
                                                 self.destination.clone(),
                                                 max_age,
                                                 false,
                                                 HeaderOrMethod::MethodData(m.clone())));
            }
        }
        // Substeps 13-14
        for h in response.headers.iter() {
            let cache_match = cache.match_header_and_update(self, h.name(), max_age);
            if !cache_match {
                cache.insert(CORSCacheEntry::new(self.origin.clone(),
                                                 self.destination.clone(),
                                                 max_age,
                                                 false,
                                                 HeaderOrMethod::HeaderData(h.to_string())));
            }
        }
        // Substep 15
        cors_response
    }
}


pub struct CORSResponse {
    pub network_error: bool,
    pub headers: Headers,
}

impl CORSResponse {
    fn new() -> CORSResponse {
        CORSResponse {
            network_error: false,
            headers: Headers::new(),
        }
    }

    fn new_error() -> CORSResponse {
        CORSResponse {
            network_error: true,
            headers: Headers::new(),
        }
    }
}

// CORS Cache stuff

/// A CORS cache object. Anchor it somewhere to the user agent.
#[derive(Clone)]
pub struct CORSCache(Vec<CORSCacheEntry>);

/// Union type for CORS cache entries
/// Each entry might pertain to a header or method
#[derive(Clone)]
pub enum HeaderOrMethod {
    HeaderData(String),
    MethodData(Method),
}

impl HeaderOrMethod {
    fn match_header(&self, header_name: &str) -> bool {
        match *self {
            HeaderOrMethod::HeaderData(ref s) => (&**s).eq_ignore_ascii_case(header_name),
            _ => false,
        }
    }

    fn match_method(&self, method: &Method) -> bool {
        match *self {
            HeaderOrMethod::MethodData(ref m) => m == method,
            _ => false,
        }
    }
}

// An entry in the CORS cache
#[derive(Clone)]
pub struct CORSCacheEntry {
    pub origin: Url,
    pub url: Url,
    pub max_age: u32,
    pub credentials: bool,
    pub header_or_method: HeaderOrMethod,
    created: Timespec,
}

impl CORSCacheEntry {
    fn new(origin: Url,
           url: Url,
           max_age: u32,
           credentials: bool,
           header_or_method: HeaderOrMethod)
           -> CORSCacheEntry {
        CORSCacheEntry {
            origin: origin,
            url: url,
            max_age: max_age,
            credentials: credentials,
            header_or_method: header_or_method,
            created: time::now().to_timespec(),
        }
    }
}

impl CORSCache {
    /// https://fetch.spec.whatwg.org/#concept-cache-clear
    #[allow(dead_code)]
    fn clear(&mut self, request: &CORSRequest) {
        let CORSCache(buf) = self.clone();
        let new_buf: Vec<CORSCacheEntry> =
            buf.into_iter()
               .filter(|e| e.origin == request.origin && request.destination == e.url)
               .collect();
        *self = CORSCache(new_buf);
    }

    // Remove old entries
    fn cleanup(&mut self) {
        let CORSCache(buf) = self.clone();
        let now = time::now().to_timespec();
        let new_buf: Vec<CORSCacheEntry> = buf.into_iter()
                                              .filter(|e| now.sec > e.created.sec + e.max_age as i64)
                                              .collect();
        *self = CORSCache(new_buf);
    }

    /// https://fetch.spec.whatwg.org/#concept-cache-match-header
    fn find_entry_by_header<'a>(&'a mut self,
                                request: &CORSRequest,
                                header_name: &str)
                                -> Option<&'a mut CORSCacheEntry> {
        self.cleanup();
        // Credentials are not yet implemented here
        self.0.iter_mut().find(|e| {
            e.origin.scheme() == request.origin.scheme() &&
            e.origin.host_str() == request.origin.host_str() &&
            e.origin.port() == request.origin.port() &&
            e.url == request.destination &&
            e.header_or_method.match_header(header_name)
        })
    }

    fn match_header(&mut self, request: &CORSRequest, header_name: &str) -> bool {
        self.find_entry_by_header(request, header_name).is_some()
    }

    fn match_header_and_update(&mut self,
                               request: &CORSRequest,
                               header_name: &str,
                               new_max_age: u32)
                               -> bool {
        self.find_entry_by_header(request, header_name).map(|e| e.max_age = new_max_age).is_some()
    }

    fn find_entry_by_method<'a>(&'a mut self,
                                request: &CORSRequest,
                                method: &Method)
                                -> Option<&'a mut CORSCacheEntry> {
        // we can take the method from CORSRequest itself
        self.cleanup();
        // Credentials are not yet implemented here
        self.0.iter_mut().find(|e| {
            e.origin.scheme() == request.origin.scheme() &&
            e.origin.host_str() == request.origin.host_str() &&
            e.origin.port() == request.origin.port() &&
            e.url == request.destination &&
            e.header_or_method.match_method(method)
        })
    }

    /// https://fetch.spec.whatwg.org/#concept-cache-match-method
    fn match_method(&mut self, request: &CORSRequest, method: &Method) -> bool {
        self.find_entry_by_method(request, method).is_some()
    }

    fn match_method_and_update(&mut self,
                               request: &CORSRequest,
                               method: &Method,
                               new_max_age: u32)
                               -> bool {
        self.find_entry_by_method(request, method).map(|e| e.max_age = new_max_age).is_some()
    }

    fn insert(&mut self, entry: CORSCacheEntry) {
        self.cleanup();
        self.0.push(entry);
    }
}

fn is_simple_header(h: &HeaderView) -> bool {
    // FIXME: use h.is::<HeaderType>() when AcceptLanguage and
    // ContentLanguage headers exist
    match &*h.name().to_ascii_lowercase() {
        "accept" | "accept-language" | "content-language" => true,
        "content-type" => match h.value() {
            Some(&ContentType(Mime(TopLevel::Text, SubLevel::Plain, _))) |
            Some(&ContentType(Mime(TopLevel::Application, SubLevel::WwwFormUrlEncoded, _))) |
            Some(&ContentType(Mime(TopLevel::Multipart, SubLevel::FormData, _))) => true,

            _ => false,

        },
        _ => false,
    }
}

fn is_simple_method(m: &Method) -> bool {
    match *m {
        Method::Get | Method::Head | Method::Post => true,
        _ => false,
    }
}

/// Perform a CORS check on a header list and CORS request
/// https://fetch.spec.whatwg.org/#cors-check
pub fn allow_cross_origin_request(req: &CORSRequest, headers: &Headers) -> bool {
    match headers.get::<AccessControlAllowOrigin>() {
        Some(&AccessControlAllowOrigin::Any) => true, // Not always true, depends on credentials mode
        Some(&AccessControlAllowOrigin::Value(ref url)) => req.origin.as_str() == *url,
        Some(&AccessControlAllowOrigin::Null) |
        None => false,
    }
}