]>
Commit | Line | Data |
---|---|---|
763220ce | 1 | extern crate apitest; |
d8d978eb | 2 | |
6d77fb40 | 3 | use failure::*; |
ef1f0e65 | 4 | use std::collections::HashMap; |
035cce94 | 5 | //use std::io; |
a974251e DM |
6 | //use std::fs; |
7 | use std::path::{PathBuf}; | |
d11f14f7 | 8 | |
c819ec8d | 9 | //use std::collections::HashMap; |
504b3597 | 10 | use lazy_static::lazy_static; |
28e47cea | 11 | |
504b3597 | 12 | //use apitest::json_schema::*; |
886e5ce8 | 13 | use apitest::api_info::*; |
6d77fb40 | 14 | use apitest::json_schema::*; |
886e5ce8 | 15 | |
504b3597 | 16 | //use serde_derive::{Serialize, Deserialize}; |
805aec15 DM |
17 | use serde_json::{json, Value}; |
18 | ||
a974251e | 19 | use futures::future::{self, Either}; |
579fbe7d DM |
20 | //use tokio::prelude::*; |
21 | //use tokio::timer::Delay; | |
22 | use tokio::fs::File; | |
23 | use tokio_codec; | |
24 | //use bytes::{BytesMut, BufMut}; | |
22f0adf2 | 25 | |
c819ec8d | 26 | //use hyper::body::Payload; |
b82472c0 | 27 | use hyper::http::request::Parts; |
28e47cea | 28 | use hyper::{Method, Body, Request, Response, Server, StatusCode}; |
b82472c0 | 29 | use hyper::rt::{Future, Stream}; |
324a5bd0 | 30 | use hyper::service::service_fn; |
3cdec2a0 | 31 | use hyper::header; |
b82472c0 | 32 | |
579fbe7d | 33 | //use std::time::{Duration, Instant}; |
b82472c0 | 34 | |
260c1ee8 | 35 | type BoxFut = Box<Future<Item = Response<Body>, Error = failure::Error> + Send>; |
d8d978eb | 36 | |
260c1ee8 | 37 | macro_rules! error_response { |
28e47cea DM |
38 | ($status:ident, $msg:expr) => {{ |
39 | let mut resp = Response::new(Body::from($msg)); | |
40 | *resp.status_mut() = StatusCode::$status; | |
260c1ee8 | 41 | resp |
b82472c0 DM |
42 | }} |
43 | } | |
260c1ee8 | 44 | |
b82472c0 DM |
45 | macro_rules! http_error_future { |
46 | ($status:ident, $msg:expr) => {{ | |
260c1ee8 | 47 | let resp = error_response!($status, $msg); |
a974251e | 48 | return Box::new(future::ok(resp)); |
28e47cea DM |
49 | }} |
50 | } | |
51 | ||
805aec15 | 52 | fn get_request_parameters_async<'a>( |
c819ec8d DM |
53 | info: &'a ApiMethod, |
54 | parts: Parts, | |
55 | req_body: Body, | |
805aec15 | 56 | ) -> Box<Future<Item = Value, Error = failure::Error> + Send + 'a> |
b82472c0 | 57 | { |
cec9f02e | 58 | let resp = req_body |
a974251e | 59 | .map_err(|err| Error::from(ApiError::new(StatusCode::BAD_REQUEST, format!("Promlems reading request body: {}", err)))) |
cec9f02e DM |
60 | .fold(Vec::new(), |mut acc, chunk| { |
61 | if acc.len() + chunk.len() < 64*1024 { //fimxe: max request body size? | |
62 | acc.extend_from_slice(&*chunk); | |
2c10fd5c | 63 | Ok(acc) |
cec9f02e | 64 | } |
2c10fd5c | 65 | else { Err(Error::from(ApiError::new(StatusCode::BAD_REQUEST, format!("Request body too large")))) } |
cec9f02e | 66 | }) |
260c1ee8 | 67 | .and_then(move |body| { |
b82472c0 | 68 | |
260c1ee8 | 69 | let bytes = String::from_utf8(body.to_vec())?; // why copy?? |
b82472c0 | 70 | |
260c1ee8 | 71 | println!("GOT BODY {:?}", bytes); |
b82472c0 | 72 | |
260c1ee8 | 73 | let mut test_required = true; |
b82472c0 | 74 | |
260c1ee8 | 75 | let mut params = json!({}); |
c819ec8d | 76 | |
260c1ee8 DM |
77 | if bytes.len() > 0 { |
78 | params = parse_query_string(&bytes, &info.parameters, true)?; | |
79 | test_required = false; | |
80 | } | |
c819ec8d | 81 | |
260c1ee8 DM |
82 | if let Some(query_str) = parts.uri.query() { |
83 | let query_params = parse_query_string(query_str, &info.parameters, test_required)?; | |
c819ec8d | 84 | |
260c1ee8 DM |
85 | for (k, v) in query_params.as_object().unwrap() { |
86 | params[k] = v.clone(); // fixme: why clone()?? | |
87 | } | |
b82472c0 | 88 | } |
b82472c0 | 89 | |
805aec15 DM |
90 | println!("GOT PARAMS {}", params); |
91 | Ok(params) | |
92 | }); | |
93 | ||
94 | Box::new(resp) | |
95 | } | |
96 | ||
97 | fn handle_async_api_request<'a>( | |
98 | info: &'a ApiMethod, | |
99 | parts: Parts, | |
100 | req_body: Body, | |
101 | ) -> Box<Future<Item = Response<Body>, Error = failure::Error> + Send + 'a> | |
102 | { | |
103 | let params = get_request_parameters_async(info, parts, req_body); | |
104 | ||
105 | let resp = params | |
106 | .and_then(move |params| { | |
107 | ||
108 | println!("GOT PARAMS {}", params); | |
109 | ||
110 | /* | |
111 | let when = Instant::now() + Duration::from_millis(3000); | |
112 | let task = Delay::new(when).then(|_| { | |
113 | println!("A LAZY TASK"); | |
114 | ok(()) | |
115 | }); | |
116 | ||
117 | tokio::spawn(task); | |
118 | */ | |
119 | ||
120 | (info.async_handler)(params, info) | |
121 | }); | |
122 | ||
123 | Box::new(resp) | |
124 | } | |
125 | ||
126 | fn handle_sync_api_request<'a>( | |
127 | info: &'a ApiMethod, | |
128 | parts: Parts, | |
129 | req_body: Body, | |
130 | ) -> Box<Future<Item = Response<Body>, Error = failure::Error> + Send + 'a> | |
131 | { | |
132 | let params = get_request_parameters_async(info, parts, req_body); | |
133 | ||
134 | let resp = params | |
135 | .and_then(move |params| { | |
136 | ||
260c1ee8 DM |
137 | println!("GOT PARAMS {}", params); |
138 | ||
805aec15 DM |
139 | /* |
140 | let when = Instant::now() + Duration::from_millis(3000); | |
141 | let task = Delay::new(when).then(|_| { | |
142 | println!("A LAZY TASK"); | |
143 | ok(()) | |
144 | }); | |
145 | ||
146 | tokio::spawn(task); | |
147 | */ | |
148 | ||
260c1ee8 | 149 | let res = (info.handler)(params, info)?; |
c819ec8d | 150 | |
260c1ee8 | 151 | Ok(res) |
c819ec8d | 152 | |
805aec15 DM |
153 | }).then(|result| { |
154 | match result { | |
155 | Ok(ref value) => { | |
156 | let json_str = value.to_string(); | |
c819ec8d | 157 | |
805aec15 DM |
158 | Ok(Response::builder() |
159 | .status(StatusCode::OK) | |
3cdec2a0 | 160 | .header(header::CONTENT_TYPE, "application/json") |
805aec15 DM |
161 | .body(Body::from(json_str))?) |
162 | } | |
163 | Err(err) => Ok(error_response!(BAD_REQUEST, err.to_string())) | |
260c1ee8 | 164 | } |
805aec15 | 165 | }); |
b82472c0 DM |
166 | |
167 | Box::new(resp) | |
168 | } | |
169 | ||
a0efdca1 DM |
170 | fn simple_static_file_download(filename: PathBuf) -> BoxFut { |
171 | ||
172 | Box::new(File::open(filename) | |
a974251e | 173 | .map_err(|err| Error::from(ApiError::new(StatusCode::BAD_REQUEST, format!("File open failed: {}", err)))) |
a0efdca1 DM |
174 | .and_then(|file| { |
175 | let buf: Vec<u8> = Vec::new(); | |
176 | tokio::io::read_to_end(file, buf) | |
a974251e | 177 | .map_err(|err| Error::from(ApiError::new(StatusCode::BAD_REQUEST, format!("File read failed: {}", err)))) |
a0efdca1 DM |
178 | .and_then(|data| Ok(Response::new(data.1.into()))) |
179 | })) | |
180 | } | |
181 | ||
182 | fn chuncked_static_file_download(filename: PathBuf) -> BoxFut { | |
183 | ||
184 | Box::new(File::open(filename) | |
a974251e | 185 | .map_err(|err| Error::from(ApiError::new(StatusCode::BAD_REQUEST, format!("File open failed: {}", err)))) |
a0efdca1 DM |
186 | .and_then(|file| { |
187 | let payload = tokio_codec::FramedRead::new(file, tokio_codec::BytesCodec::new()). | |
188 | map(|bytes| { | |
189 | //sigh - howto avoid copy here? or the whole map() ?? | |
190 | hyper::Chunk::from(bytes.to_vec()) | |
191 | }); | |
192 | let body = Body::wrap_stream(payload); | |
193 | // fixme: set content type and other headers | |
194 | Ok(Response::builder() | |
195 | .status(StatusCode::OK) | |
196 | .body(body) | |
197 | .unwrap()) | |
198 | })) | |
199 | } | |
200 | ||
579fbe7d DM |
201 | fn handle_static_file_download(filename: PathBuf) -> BoxFut { |
202 | ||
78d0783b | 203 | let response = tokio::fs::metadata(filename.clone()) |
a974251e | 204 | .map_err(|err| Error::from(ApiError::new(StatusCode::BAD_REQUEST, format!("File access problems: {}", err)))) |
78d0783b | 205 | .and_then(|metadata| { |
a0efdca1 DM |
206 | if metadata.len() < 1024*32 { |
207 | Either::A(simple_static_file_download(filename)) | |
78d0783b | 208 | } else { |
a0efdca1 DM |
209 | Either::B(chuncked_static_file_download(filename)) |
210 | } | |
579fbe7d DM |
211 | }); |
212 | ||
213 | return Box::new(response); | |
214 | } | |
215 | ||
324a5bd0 | 216 | fn handle_request(api: &ApiServer, req: Request<Body>) -> BoxFut { |
b82472c0 DM |
217 | |
218 | let (parts, body) = req.into_parts(); | |
28e47cea | 219 | |
b82472c0 DM |
220 | let method = parts.method.clone(); |
221 | let path = parts.uri.path(); | |
886e5ce8 | 222 | |
579fbe7d | 223 | // normalize path |
198fab6f DM |
224 | // do not allow ".", "..", or hidden files ".XXXX" |
225 | // also remove empty path components | |
226 | ||
227 | let items = path.split('/'); | |
228 | let mut path = String::new(); | |
229 | let mut components = vec![]; | |
230 | ||
231 | for name in items { | |
232 | if name.is_empty() { continue; } | |
233 | if name.starts_with(".") { | |
234 | http_error_future!(BAD_REQUEST, "Path contains illegal components.\n"); | |
235 | } | |
236 | path.push('/'); | |
237 | path.push_str(name); | |
238 | components.push(name); | |
239 | } | |
240 | ||
28e47cea | 241 | let comp_len = components.len(); |
886e5ce8 DM |
242 | |
243 | println!("REQUEST {} {}", method, path); | |
28e47cea DM |
244 | println!("COMPO {:?}", components); |
245 | ||
246 | if comp_len >= 1 && components[0] == "api3" { | |
247 | println!("GOT API REQUEST"); | |
248 | if comp_len >= 2 { | |
249 | let format = components[1]; | |
250 | if format != "json" { | |
6639c14b | 251 | http_error_future!(BAD_REQUEST, format!("Unsupported output format '{}'.", format)) |
28e47cea DM |
252 | } |
253 | ||
324a5bd0 | 254 | if let Some(info) = api.router.find_method(&components[2..]) { |
28e47cea DM |
255 | println!("FOUND INFO"); |
256 | let api_method_opt = match method { | |
b82472c0 DM |
257 | Method::GET => &info.get, |
258 | Method::PUT => &info.put, | |
259 | Method::POST => &info.post, | |
260 | Method::DELETE => &info.delete, | |
0dde2f04 | 261 | _ => &None, |
28e47cea | 262 | }; |
bfcba4fd | 263 | let api_method = match api_method_opt { |
28e47cea | 264 | Some(m) => m, |
6639c14b | 265 | _ => http_error_future!(NOT_FOUND, format!("No such method '{}'.", method)), |
28e47cea DM |
266 | }; |
267 | ||
4beaacb6 | 268 | // fixme: handle auth |
28e47cea | 269 | |
805aec15 DM |
270 | //return handle_sync_api_request(api_method, parts, body); |
271 | return handle_async_api_request(api_method, parts, body); | |
28e47cea DM |
272 | } |
273 | } | |
035cce94 DM |
274 | } else { |
275 | // not Auth for accessing files! | |
276 | ||
277 | let mut prefix = String::new(); | |
278 | let mut filename = PathBuf::from("/var/www"); // fixme | |
279 | if comp_len >= 1 { | |
280 | prefix.push_str(components[0]); | |
324a5bd0 | 281 | if let Some(subdir) = api.aliases.get(&prefix) { |
035cce94 DM |
282 | filename.push(subdir); |
283 | for i in 1..comp_len { filename.push(components[i]) } | |
284 | } | |
285 | } | |
286 | return handle_static_file_download(filename); | |
28e47cea | 287 | } |
886e5ce8 | 288 | |
579fbe7d DM |
289 | |
290 | http_error_future!(NOT_FOUND, "Path not found.") | |
291 | //Box::new(ok(Response::new(Body::from("RETURN WEB GUI\n")))) | |
886e5ce8 DM |
292 | } |
293 | ||
ef1f0e65 DM |
294 | // add default dirs which includes jquery and bootstrap |
295 | // my $base = '/usr/share/libpve-http-server-perl'; | |
296 | // add_dirs($self->{dirs}, '/css/' => "$base/css/"); | |
297 | // add_dirs($self->{dirs}, '/js/' => "$base/js/"); | |
298 | // add_dirs($self->{dirs}, '/fonts/' => "$base/fonts/"); | |
299 | ||
ef1f0e65 | 300 | |
035cce94 | 301 | fn initialize_directory_aliases() -> HashMap<String, PathBuf> { |
ef1f0e65 | 302 | |
035cce94 | 303 | let mut basedirs: HashMap<String, PathBuf> = HashMap::new(); |
ef1f0e65 | 304 | |
035cce94 DM |
305 | let mut add_directory_alias = |name, path| { |
306 | basedirs.insert(String::from(name), PathBuf::from(path)); | |
307 | }; | |
ef1f0e65 | 308 | |
035cce94 DM |
309 | add_directory_alias("novnc", "/usr/share/novnc-pve"); |
310 | add_directory_alias("extjs", "/usr/share/javascript/extjs"); | |
311 | add_directory_alias("fontawesome", "/usr/share/fonts-font-awesome"); | |
312 | add_directory_alias("xtermjs", "/usr/share/pve-xtermjs"); | |
313 | add_directory_alias("widgettoolkit", "/usr/share/javascript/proxmox-widget-toolkit"); | |
ef1f0e65 | 314 | |
035cce94 | 315 | basedirs |
ef1f0e65 DM |
316 | } |
317 | ||
a974251e DM |
318 | struct ApiServer { |
319 | basedir: PathBuf, | |
324a5bd0 DM |
320 | router: &'static MethodInfo, |
321 | aliases: &'static HashMap<String, PathBuf>, | |
504b3597 DM |
322 | } |
323 | ||
d8d978eb DM |
324 | fn main() { |
325 | println!("Fast Static Type Definitions 1"); | |
326 | ||
886e5ce8 DM |
327 | let addr = ([127, 0, 0, 1], 8007).into(); |
328 | ||
329 | let new_svc = || { | |
324a5bd0 DM |
330 | service_fn(|req| { |
331 | ||
332 | lazy_static!{ | |
333 | static ref ALIASES: HashMap<String, PathBuf> = initialize_directory_aliases(); | |
334 | static ref ROUTER: MethodInfo = apitest::api3::router(); | |
335 | } | |
336 | ||
337 | let api_server = ApiServer { | |
338 | basedir: "/var/www". into(), | |
339 | router: &ROUTER, | |
340 | aliases: &ALIASES, | |
341 | }; | |
342 | ||
343 | handle_request(&api_server, req).then(|result| { | |
344 | match result { | |
345 | Ok(res) => Ok::<_,String>(res), | |
346 | Err(err) => { | |
347 | if let Some(apierr) = err.downcast_ref::<ApiError>() { | |
348 | let mut resp = Response::new(Body::from(apierr.message.clone())); | |
349 | *resp.status_mut() = apierr.code; | |
350 | Ok(resp) | |
351 | } else { | |
352 | Ok(error_response!(BAD_REQUEST, err.to_string())) | |
353 | } | |
354 | } | |
355 | } | |
356 | }) | |
357 | }) | |
886e5ce8 DM |
358 | }; |
359 | ||
360 | let server = Server::bind(&addr) | |
361 | .serve(new_svc) | |
362 | .map_err(|e| eprintln!("server error: {}", e)); | |
363 | ||
364 | // Run this server for... forever! | |
365 | hyper::rt::run(server); | |
d8d978eb | 366 | } |