-
Notifications
You must be signed in to change notification settings - Fork 201
/
mod.rs
597 lines (516 loc) · 21.1 KB
/
mod.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
//! Web interface of cratesfyi
pub mod page;
/// ctry! (cratesfyitry) is extremely similar to try! and itry!
/// except it returns an error page response instead of plain Err.
macro_rules! ctry {
($result:expr) => (match $result {
Ok(v) => v,
Err(e) => {
return $crate::web::page::Page::new(format!("{:?}", e)).title("An error has occured")
.set_status(::iron::status::BadRequest).to_resp("resp");
}
})
}
/// cexpect will check an option and if it's not Some
/// it will return an error page response
macro_rules! cexpect {
($option:expr) => (match $option {
Some(v) => v,
None => {
return $crate::web::page::Page::new("Resource not found".to_owned())
.title("An error has occured")
.set_status(::iron::status::BadRequest).to_resp("resp");
}
})
}
/// Gets an extension from Request
macro_rules! extension {
($req:expr, $ext:ty) => (
cexpect!($req.extensions.get::<$ext>())
)
}
mod rustdoc;
mod releases;
mod crate_details;
mod source;
mod pool;
mod file;
mod builds;
mod error;
mod sitemap;
mod metrics;
use std::{env, fmt};
use std::error::Error;
use std::time::Duration;
use std::path::PathBuf;
use iron::prelude::*;
use iron::{self, Handler, Url, status};
use iron::headers::{Expires, HttpDate, CacheControl, CacheDirective, ContentType};
use iron::modifiers::Redirect;
use router::{Router, NoRoute};
use staticfile::Static;
use handlebars_iron::{HandlebarsEngine, DirectorySource};
use time;
use postgres::Connection;
use semver::{Version, VersionReq};
use rustc_serialize::json::{Json, ToJson};
use std::collections::BTreeMap;
/// Duration of static files for staticfile and DatabaseFileHandler (in seconds)
const STATIC_FILE_CACHE_DURATION: u64 = 60 * 60 * 24 * 30 * 12; // 12 months
const STYLE_CSS: &'static str = include_str!(concat!(env!("OUT_DIR"), "/style.css"));
const OPENSEARCH_XML: &'static [u8] = include_bytes!("opensearch.xml");
struct CratesfyiHandler {
shared_resource_handler: Box<dyn Handler>,
router_handler: Box<dyn Handler>,
database_file_handler: Box<dyn Handler>,
static_handler: Box<dyn Handler>,
}
impl CratesfyiHandler {
fn chain<H: Handler>(base: H) -> Chain {
// TODO: Use DocBuilderOptions for paths
let mut hbse = HandlebarsEngine::new();
hbse.add(Box::new(DirectorySource::new("./templates", ".hbs")));
// load templates
if let Err(e) = hbse.reload() {
panic!("Failed to load handlebar templates: {}", e.description());
}
let mut chain = Chain::new(base);
chain.link_before(pool::Pool::new());
chain.link_after(hbse);
chain
}
pub fn new() -> CratesfyiHandler {
let mut router = Router::new();
router.get("/", releases::home_page, "index");
router.get("/style.css", style_css_handler, "style_css");
router.get("/about", sitemap::about_handler, "about");
router.get("/about/metrics", metrics::metrics_handler, "metrics");
router.get("/robots.txt", sitemap::robots_txt_handler, "robots_txt");
router.get("/sitemap.xml", sitemap::sitemap_handler, "sitemap_xml");
router.get("/opensearch.xml", opensearch_xml_handler, "opensearch_xml");
router.get("/releases", releases::recent_releases_handler, "releases");
router.get("/releases/feed",
releases::releases_feed_handler,
"releases_feed");
router.get("/releases/recent/:page",
releases::recent_releases_handler,
"releases_recent_page");
router.get("/releases/stars", releases::releases_by_stars_handler, "releases_stars");
router.get("/releases/stars/:page",
releases::releases_by_stars_handler,
"releases_stars_page");
router.get("/releases/recent-failures", releases::releases_recent_failures_handler, "releases_recent_failures");
router.get("/releases/recent-failures/:page",
releases::releases_recent_failures_handler,
"releases_recent_failures_page");
router.get("/releases/failures", releases::releases_failures_by_stars_handler, "releases_failures_by_stars");
router.get("/releases/failures/:page",
releases::releases_failures_by_stars_handler,
"releases_failures_by_starts_page");
router.get("/releases/:author",
releases::author_handler,
"releases_author");
router.get("/releases/:author/:page",
releases::author_handler,
"releases_author_page");
router.get("/releases/activity",
releases::activity_handler,
"releases_activity");
router.get("/releases/search",
releases::search_handler,
"releases_search");
router.get("/releases/queue",
releases::build_queue_handler,
"releases_queue");
router.get("/crate/:name",
crate_details::crate_details_handler,
"crate_name");
router.get("/crate/:name/",
crate_details::crate_details_handler,
"crate_name_");
router.get("/crate/:name/:version",
crate_details::crate_details_handler,
"crate_name_version");
router.get("/crate/:name/:version/",
crate_details::crate_details_handler,
"crate_name_version_");
router.get("/crate/:name/:version/builds",
builds::build_list_handler,
"crate_name_version_builds");
router.get("/crate/:name/:version/builds.json",
builds::build_list_handler,
"crate_name_version_builds_json");
router.get("/crate/:name/:version/builds/:id",
builds::build_list_handler,
"crate_name_version_builds_id");
router.get("/crate/:name/:version/source/",
source::source_browser_handler,
"crate_name_version_source");
router.get("/crate/:name/:version/source/*",
source::source_browser_handler,
"crate_name_version_source_");
router.get("/:crate", rustdoc::rustdoc_redirector_handler, "crate");
router.get("/:crate/", rustdoc::rustdoc_redirector_handler, "crate_");
router.get("/:crate/badge.svg", rustdoc::badge_handler, "crate_badge");
router.get("/:crate/:version",
rustdoc::rustdoc_redirector_handler,
"crate_version");
router.get("/:crate/:version/",
rustdoc::rustdoc_redirector_handler,
"crate_version_");
router.get("/:crate/:version/settings.html",
rustdoc::rustdoc_html_server_handler,
"crate_version_settings_html");
router.get("/:crate/:version/all.html",
rustdoc::rustdoc_html_server_handler,
"crate_version_all_html");
router.get("/:crate/:version/:target",
rustdoc::rustdoc_redirector_handler,
"crate_version_target");
router.get("/:crate/:version/:target/",
rustdoc::rustdoc_html_server_handler,
"crate_version_target_");
router.get("/:crate/:version/:target/*.html",
rustdoc::rustdoc_html_server_handler,
"crate_version_target_html");
let shared_resources = Self::chain(rustdoc::SharedResourceHandler);
let router_chain = Self::chain(router);
let prefix = PathBuf::from(env::var("CRATESFYI_PREFIX").unwrap()).join("public_html");
let static_handler = Static::new(prefix)
.cache(Duration::from_secs(STATIC_FILE_CACHE_DURATION));
CratesfyiHandler {
shared_resource_handler: Box::new(shared_resources),
router_handler: Box::new(router_chain),
database_file_handler: Box::new(file::DatabaseFileHandler),
static_handler: Box::new(static_handler),
}
}
}
impl Handler for CratesfyiHandler {
fn handle(&self, req: &mut Request) -> IronResult<Response> {
// try serving shared rustdoc resources first, then router, then db/static file handler
// return 404 if none of them return Ok
self.shared_resource_handler
.handle(req)
.or_else(|e| {
self.router_handler.handle(req).or(Err(e))
})
.or_else(|e| {
// if router fails try to serve files from database first
self.database_file_handler.handle(req).or(Err(e))
})
.or_else(|e| {
// and then try static handler. if all of them fails, return 404
self.static_handler.handle(req).or(Err(e))
})
.or_else(|e| {
let err = if let Some(err) = e.error.downcast::<error::Nope>() {
*err
} else if e.error.downcast::<NoRoute>().is_some() {
error::Nope::ResourceNotFound
} else {
panic!("all cratesfyi errors should be of type Nope");
};
if let error::Nope::ResourceNotFound = err {
// print the path of the URL that triggered a 404 error
struct DebugPath<'a>(&'a iron::Url);
impl<'a> fmt::Display for DebugPath<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for path_elem in self.0.path() {
write!(f, "/{}", path_elem)?;
}
if let Some(query) = self.0.query() {
write!(f, "?{}", query)?;
}
if let Some(hash) = self.0.fragment() {
write!(f, "#{}", hash)?;
}
Ok(())
}
}
debug!("Path not found: {}", DebugPath(&req.url));
}
Self::chain(err).handle(req)
})
}
}
/// Represents the possible results of attempting to load a version requirement.
enum MatchVersion {
/// `match_version` was given an exact version, which matched a saved crate version.
Exact(String),
/// `match_version` was given a semver version requirement, which matched the given saved crate
/// version.
Semver(String),
/// `match_version` was given a version requirement which did not match any saved crate
/// versions.
None,
}
impl MatchVersion {
/// Convert this `MatchVersion` into an `Option`, discarding whether the matched version came
/// from an exact version number or a semver requirement.
pub fn into_option(self) -> Option<String> {
match self {
MatchVersion::Exact(v) | MatchVersion::Semver(v) => Some(v),
MatchVersion::None => None,
}
}
}
/// Checks the database for crate releases that match the given name and version.
///
/// `version` may be an exact version number or loose semver version requirement. The return value
/// will indicate whether the given version exactly matched a version number from the database.
fn match_version(conn: &Connection, name: &str, version: Option<&str>) -> MatchVersion {
// version is an Option<&str> from router::Router::get
// need to decode first
use url::percent_encoding::percent_decode;
let req_version = version.and_then(|v| {
match percent_decode(v.as_bytes()).decode_utf8() {
Ok(p) => Some(p.into_owned()),
Err(_) => None,
}
})
.map(|v| if v == "newest" || v == "latest" { "*".to_owned() } else { v })
.unwrap_or("*".to_string());
let versions = {
let mut versions = Vec::new();
let rows = conn.query("SELECT versions FROM crates WHERE name = $1", &[&name]).unwrap();
if rows.len() == 0 {
return MatchVersion::None;
}
let versions_json: Json = rows.get(0).get(0);
for version in versions_json.as_array().unwrap() {
let version: String = version.as_string().unwrap().to_owned();
versions.push(version);
}
versions
};
// first check for exact match
// we can't expect users to use semver in query
for version in &versions {
if version == &req_version {
return MatchVersion::Exact(version.clone());
}
}
// Now try to match with semver
let req_sem_ver = match VersionReq::parse(&req_version) {
Ok(v) => v,
Err(_) => return MatchVersion::None,
};
// we need to sort versions first
let versions_sem = {
let mut versions_sem: Vec<Version> = Vec::new();
for version in &versions {
// in theory a crate must always have a semver compatible version
// but check result just in case
let version = match Version::parse(&version) {
Ok(v) => v,
Err(_) => return MatchVersion::None,
};
versions_sem.push(version);
}
versions_sem.sort();
versions_sem.reverse();
versions_sem
};
// semver is acting weird for '*' (any) range if a crate only have pre-release versions
// return first version if requested version is '*'
if req_version == "*" && !versions_sem.is_empty() {
return MatchVersion::Semver(format!("{}", versions_sem[0]));
}
for version in &versions_sem {
if req_sem_ver.matches(&version) {
return MatchVersion::Semver(format!("{}", version));
}
}
MatchVersion::None
}
/// Wrapper around the Markdown parser and renderer to render markdown
fn render_markdown(text: &str) -> String {
use comrak::{markdown_to_html, ComrakOptions};
let options = {
let mut options = ComrakOptions::default();
options.safe = true;
options.ext_superscript = true;
options.ext_table = true;
options.ext_autolink = true;
options.ext_tasklist = true;
options.ext_strikethrough = true;
options
};
markdown_to_html(text, &options)
}
/// Returns latest version if required version is not the latest
/// req_version must be an exact version
fn latest_version(versions_json: &Vec<String>, req_version: &str) -> Option<String> {
let req_version = match Version::parse(req_version) {
Ok(v) => v,
Err(_) => return None,
};
let versions = {
let mut versions: Vec<Version> = Vec::new();
for version in versions_json {
let version = match Version::parse(&version) {
Ok(v) => v,
Err(_) => return None,
};
versions.push(version);
}
versions.sort();
versions.reverse();
versions
};
if req_version != versions[0] {
for i in 1..versions.len() {
if req_version == versions[i] {
return Some(format!("{}", versions[0]))
}
}
}
None
}
/// Starts cratesfyi web server
pub fn start_web_server(sock_addr: Option<&str>) {
let cratesfyi = CratesfyiHandler::new();
Iron::new(cratesfyi).http(sock_addr.unwrap_or("localhost:3000")).unwrap();
}
/// Converts Timespec to nice readable relative time string
fn duration_to_str(ts: time::Timespec) -> String {
let tm = time::at(ts);
let delta = time::now() - tm;
if delta.num_days() > 5 {
format!("{}", tm.strftime("%b %d, %Y").unwrap())
} else if delta.num_days() > 1 {
format!("{} days ago", delta.num_days())
} else if delta.num_days() == 1 {
"one day ago".to_string()
} else if delta.num_hours() > 1 {
format!("{} hours ago", delta.num_hours())
} else if delta.num_hours() == 1 {
"an hour ago".to_string()
} else if delta.num_minutes() > 1 {
format!("{} minutes ago", delta.num_minutes())
} else if delta.num_minutes() == 1 {
"one minute ago".to_string()
} else if delta.num_seconds() > 0 {
format!("{} seconds ago", delta.num_seconds())
} else {
"just now".to_string()
}
}
/// Creates a `Response` which redirects to the given path on the scheme/host/port from the given
/// `Request`.
fn redirect(url: Url) -> Response {
let mut resp = Response::with((status::Found, Redirect(url)));
resp.headers.set(Expires(HttpDate(time::now())));
resp
}
pub fn redirect_base(req: &Request) -> String {
// Try to get the scheme from CloudFront first, and then from iron
let scheme = req.headers
.get_raw("cloudfront-forwarded-proto")
.and_then(|values| values.get(0))
.and_then(|value| std::str::from_utf8(value).ok())
.filter(|proto| *proto == "http" || *proto == "https")
.unwrap_or_else(|| req.url.scheme());
// Only include the port if it's needed
let port = req.url.port();
if port == 80 {
format!("{}://{}", scheme, req.url.host())
} else {
format!("{}://{}:{}", scheme, req.url.host(), port)
}
}
fn style_css_handler(_: &mut Request) -> IronResult<Response> {
let mut response = Response::with((status::Ok, STYLE_CSS));
let cache = vec![CacheDirective::Public,
CacheDirective::MaxAge(STATIC_FILE_CACHE_DURATION as u32)];
response.headers.set(ContentType("text/css".parse().unwrap()));
response.headers.set(CacheControl(cache));
Ok(response)
}
fn opensearch_xml_handler(_: &mut Request) -> IronResult<Response> {
let mut response = Response::with((status::Ok, OPENSEARCH_XML));
let cache = vec![CacheDirective::Public,
CacheDirective::MaxAge(STATIC_FILE_CACHE_DURATION as u32)];
response.headers.set(ContentType("application/opensearchdescription+xml".parse().unwrap()));
response.headers.set(CacheControl(cache));
Ok(response)
}
fn ico_handler(req: &mut Request) -> IronResult<Response> {
if let Some(&"favicon.ico") = req.url.path().last() {
// if we're looking for exactly "favicon.ico", we need to defer to the handler that loads
// from `public_html`, so return a 404 here to make the main handler carry on
Err(IronError::new(error::Nope::ResourceNotFound, status::NotFound))
} else {
// if we're looking for something like "favicon-20190317-1.35.0-nightly-c82834e2b.ico",
// redirect to the plain one so that the above branch can trigger with the correct filename
let url = ctry!(Url::parse(&format!("{}/favicon.ico", redirect_base(req))[..]));
Ok(redirect(url))
}
}
/// MetaData used in header
#[derive(Debug)]
pub struct MetaData {
pub name: String,
pub version: String,
pub description: Option<String>,
pub target_name: Option<String>,
pub rustdoc_status: bool,
}
impl MetaData {
pub fn from_crate(conn: &Connection, name: &str, version: &str) -> Option<MetaData> {
for row in &conn.query("SELECT crates.name,
releases.version,
releases.description,
releases.target_name,
releases.rustdoc_status
FROM releases
INNER JOIN crates ON crates.id = releases.crate_id
WHERE crates.name = $1 AND releases.version = $2",
&[&name, &version])
.unwrap() {
return Some(MetaData {
name: row.get(0),
version: row.get(1),
description: row.get(2),
target_name: row.get(3),
rustdoc_status: row.get(4),
});
}
None
}
}
impl ToJson for MetaData {
fn to_json(&self) -> Json {
let mut m: BTreeMap<String, Json> = BTreeMap::new();
m.insert("name".to_owned(), self.name.to_json());
m.insert("version".to_owned(), self.version.to_json());
m.insert("description".to_owned(), self.description.to_json());
m.insert("target_name".to_owned(), self.target_name.to_json());
m.insert("rustdoc_status".to_owned(), self.rustdoc_status.to_json());
m.to_json()
}
}
#[cfg(test)]
mod test {
extern crate env_logger;
use super::*;
#[test]
#[ignore]
fn test_start_web_server() {
// FIXME: This test is doing nothing
let _ = env_logger::try_init();
start_web_server(None);
}
#[test]
fn test_latest_version() {
let versions = vec!["1.0.0".to_string(),
"1.1.0".to_string(),
"0.9.0".to_string(),
"0.9.1".to_string()];
assert_eq!(latest_version(&versions, "1.1.0"), None);
assert_eq!(latest_version(&versions, "1.0.0"), Some("1.1.0".to_owned()));
assert_eq!(latest_version(&versions, "0.9.0"), Some("1.1.0".to_owned()));
assert_eq!(latest_version(&versions, "invalidversion"), None);
}
}