use hyper::{Request, Body, Method, Uri}; use http::request; use serde::{Serialize, Deserialize}; use std::collections::HashMap; #[derive(Default, Deserialize, Debug, Clone)] pub struct ProxyConfig { pub bind_addr: Option, // 127.0.0.1:9292 pub upstream_addr: Option, // 127.0.0.1:9200 pub index: Vec } #[derive(Deserialize, Debug, Clone)] pub struct IndexConfig { pub name: String, } impl ProxyConfig { pub fn allow_index(&self, name: &str) -> bool { for index in &self.index { if index.name == name { return true } } false } } #[derive(Serialize, Deserialize, Debug)] pub struct ApiRequest { pub method: String, pub path_and_query: String, pub body: Option, } pub struct UrlQueryParams { pub allow_no_indices: Option, pub allow_partial_search_results: Option, pub batched_reduce_size: Option, pub ccs_minimize_roundtrips: Option, pub docvalue_fields: Option, // array of strings, comma-separated pub expand_wildcards: Option, pub explain: Option, pub from: Option, pub ignore_throttled: Option, pub ignore_unavailable: Option, pub max_concurrent_shard_requests: Option, pub pre_filter_shard_size: Option, pub preference: Option, pub q: Option, pub request_cache: Option, pub rest_total_hits_as_int: Option, pub routing: Option, pub scroll: Option, // string is "time value" pub search_type: Option, pub seq_no_primary_term: Option, pub size: Option, pub sort: Option, // array of strings, comma-separated pub _source: Option, // TODO: bool or string pub _source_excludes: Option, // array of strings, comma-separated pub _source_includes: Option, // array of strings, comma-separated pub stats: Option, pub stored_fields: Option, // array of strings, comma-separated pub suggest_field: Option, pub suggest_text: Option, pub terminate_after: Option, pub timeout: Option, // string is "time units" pub track_scores: Option, pub track_total_hits: Option, // XXX: bool or integer pub typed_keys: Option, pub version: Option, } // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html #[derive(Serialize, Deserialize, Debug)] #[serde(deny_unknown_fields)] pub struct SearchBody { pub query: Option, pub highlight: Option, pub collapse: Option, pub post_filter: Option, // TODO: leaf query only? pub rescore: Option, // TODO: single or an array of rescore objects // script_fields disabled // https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html pub sort: Option>, pub slice: Option, pub stored_fields: Option, // array of strings, or "_none_" // overlap with URL query parameters pub docvalue_fields: Option>, pub explain: Option, pub from: Option, pub min_score: Option, pub seq_no_primary_term: Option, pub size: Option, pub _source: Option, // XXX: bool, string, or object pub terminate_after: Option, pub timeout: Option, // string is "time units" } #[derive(Serialize, Deserialize, Debug)] #[serde(deny_unknown_fields)] pub struct ScrollBody { pub scroll_id: String, pub scroll: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct ApiSlice { id: u32, max: u32, field: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct ApiRescore{ pub query: Option, pub window_size: Option, } // TODO: could revert to having query types as an enum, with flattening #[derive(Serialize, Deserialize, Debug)] pub struct ApiQuery { // compound queries #[serde(rename = "bool")] bool_query: Option, boosting: Option, constant_score: Option, // fulltext (leaf) queries // term-level (leaf) queries #[serde(rename = "match")] match_query: Option>, match_phrase: Option>, query_string: Option, // other nested: Option, rescore_query: Option>, } #[derive(Serialize, Deserialize, Debug)] pub struct ApiHighlight{ // TODO: fields could also be an array of strings? fields: HashMap, #[serde(flatten)] settings: HighlightField, } #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum SortMapValue { String(String), Object { order: String, mode: Option }, } #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum SortElement{ String(String), Object(HashMap), } #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum DocValOrString { String(String), Object {field: String, format: Option}, } #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum QueryFieldOrString { Object(QueryField), String(String), } #[derive(Serialize, Deserialize, Debug)] pub struct QueryField{ query: String, fuzziness: Option, slop: Option, boost: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct BoolQuery { must: Option>, filter: Option>, should: Option>, must_not: Option>, minimum_should_match: Option, boost: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct NestedQuery { path: String, query: Box, score_mode: Option, ignore_unmapped: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct BoostingQuery { positive: Box, negative: Box, negative_boost: f64, } #[derive(Serialize, Deserialize, Debug)] pub struct ConstantScoreQuery { filter: Box, boost: Option, } // https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html #[derive(Serialize, Deserialize, Debug)] pub struct HighlightField{ boundary_chars: Option, boundary_max_scan: Option, boundary_scanner: Option, boundary_scanner_locale: Option, encoder: Option, force_source: Option, fragmenter: Option, fragment_offset: Option, fragment_size: Option, highlight_query: Option, matched_fields: Option>, no_match_size: Option, number_of_fragments: Option, order: Option, phrase_limit: Option, pre_tags: Option>, post_tags: Option>, require_field_match: Option, tags_schema: Option, #[serde(rename = "type")] highlight_type: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct ApiCollapse{ field: String, inner_hits: Option, } #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum InnerHitsOneOrMore { Single(InnerHits), Multiple(Vec), } #[derive(Serialize, Deserialize, Debug)] pub struct InnerHits { from: Option, size: Option, sort: Option>, name: Option, } #[derive(Debug)] pub enum ParsedRequest { Malformed(String), ParseError(String), NotAllowed(String), NotSupported(String), NotFound(String), Allowed(Request), } pub fn parse_request(req: Request, config: &ProxyConfig) -> ParsedRequest { let (parts, body) = req.into_parts(); // split path into at most 3 chunks let mut req_path = parts.uri.path(); if req_path.starts_with("/") { req_path = &req_path[1..]; } let path_chunks: Vec<&str> = req_path.split("/").collect(); if path_chunks.len() > 3 { return ParsedRequest::NotSupported("only request paths with up to three segments allowed".to_string()) } println!("{:?}", path_chunks); // this is sort of like a router match (&parts.method, path_chunks.as_slice()) { (&Method::GET, [""]) | (&Method::HEAD, [""]) => { parse_request_basic("", &parts, config) }, (&Method::POST, ["_search", "scroll"]) | (&Method::DELETE, ["_search", "scroll"]) => { parse_request_scroll(None, &parts, body, config) }, (&Method::POST, ["_search", "scroll", key]) | (&Method::DELETE, ["_search", "scroll", key]) => { parse_request_scroll(Some(key), &parts, body, config) }, (&Method::GET, [index, "_search"]) | (&Method::POST, [index, "_search"]) => { parse_request_search(index, &parts, body, config) }, //(Method::GET, [index, "_count"]) => { // parse_request_count(index, "_count", None, &parts, body, config) //}, (&Method::GET, [index, "_doc", key]) | (&Method::GET, [index, "_source", key]) => { parse_request_read(index, path_chunks[1], key, &parts, body, config) }, _ => ParsedRequest::NotSupported("unknown endpoint".to_string()), } } pub fn parse_request_basic(endpoint: &str, parts: &request::Parts, config: &ProxyConfig) -> ParsedRequest { // XXX: partial let upstream_uri = Uri::builder() .scheme("http") .authority(config.upstream_addr.as_ref().unwrap_or(&"localhost:9200".to_string()).as_str()) .path_and_query(format!("/{}", endpoint).as_str()) .build() .unwrap(); println!("{:?}", upstream_uri); let upstream_req = Request::builder() .uri(upstream_uri) .method(&parts.method) .body(Body::empty()) .unwrap(); ParsedRequest::Allowed(upstream_req) } pub fn parse_request_scroll(key: Option<&str>, parts: &request::Parts, body: Body, config: &ProxyConfig) -> ParsedRequest { ParsedRequest::NotSupported("not yet implemented".to_string()) } pub fn parse_request_search(index: &str, parts: &request::Parts, body: Body, config: &ProxyConfig) -> ParsedRequest { ParsedRequest::NotSupported("not yet implemented".to_string()) } pub fn parse_request_read(index: &str, endpoint: &str, key: &str, parts: &request::Parts, body: Body, config: &ProxyConfig) -> ParsedRequest { ParsedRequest::NotSupported("not yet implemented".to_string()) }