Skip to content

Commit 31b70e1

Browse files
committed
Allow per-crate max upload sizes
There's still a global limit on the nginx server but each crate can now have its own maximum limit as well which is larger than the standard limit. Closes #40 Closes #195
1 parent 555a75b commit 31b70e1

File tree

7 files changed

+113
-56
lines changed

7 files changed

+113
-56
lines changed

config/nginx.conf.erb

+2-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ http {
2525
default_type application/octet-stream;
2626
sendfile on;
2727

28-
#Must read the body in 5 seconds.
29-
client_body_timeout 5;
30-
client_max_body_size 10m;
28+
client_body_timeout 30;
29+
client_max_body_size 50m;
3130

3231
upstream app_server {
3332
server localhost:8888 fail_timeout=0;

src/bin/migrate.rs

+2
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,8 @@ fn migrations() -> Vec<Migration> {
483483
UNIQUE (owner_id, crate_id)", &[]));
484484
Ok(())
485485
}),
486+
Migration::add_column(20151118135514, "crates", "max_upload_size",
487+
"INTEGER"),
486488
];
487489
// NOTE: Generate a new id via `date +"%Y%m%d%H%M%S"`
488490

src/bin/update-downloads.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ mod test {
162162
let user = user(&tx);
163163
let krate = Crate::find_or_insert(&tx, "foo", user.id, &None, &None,
164164
&None, &None, &[], &None, &None,
165-
&None).unwrap();
165+
&None, None).unwrap();
166166
let version = Version::insert(&tx, krate.id,
167167
&semver::Version::parse("1.0.0").unwrap(),
168168
&HashMap::new(), &[]).unwrap();
@@ -189,7 +189,7 @@ mod test {
189189
let user = user(&tx);
190190
let krate = Crate::find_or_insert(&tx, "foo", user.id, &None,
191191
&None, &None, &None, &[], &None,
192-
&None, &None).unwrap();
192+
&None, &None, None).unwrap();
193193
let version = Version::insert(&tx, krate.id,
194194
&semver::Version::parse("1.0.0").unwrap(),
195195
&HashMap::new(), &[]).unwrap();
@@ -212,7 +212,7 @@ mod test {
212212
let user = user(&tx);
213213
let krate = Crate::find_or_insert(&tx, "foo", user.id, &None,
214214
&None, &None, &None, &[], &None,
215-
&None, &None).unwrap();
215+
&None, &None, None).unwrap();
216216
let version = Version::insert(&tx, krate.id,
217217
&semver::Version::parse("1.0.0").unwrap(),
218218
&HashMap::new(), &[]).unwrap();

src/krate.rs

+26-18
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use std::cmp;
33
use std::collections::HashMap;
44
use std::io::prelude::*;
55
use std::io;
6-
use std::iter::repeat;
76
use std::mem;
87
use std::sync::Arc;
98

@@ -52,6 +51,7 @@ pub struct Crate {
5251
pub keywords: Vec<String>,
5352
pub license: Option<String>,
5453
pub repository: Option<String>,
54+
pub max_upload_size: Option<i32>,
5555
}
5656

5757
#[derive(RustcEncodable, RustcDecodable)]
@@ -101,7 +101,9 @@ impl Crate {
101101
keywords: &[String],
102102
repository: &Option<String>,
103103
license: &Option<String>,
104-
license_file: &Option<String>) -> CargoResult<Crate> {
104+
license_file: &Option<String>,
105+
max_upload_size: Option<i32>)
106+
-> CargoResult<Crate> {
105107
let description = description.as_ref().map(|s| &s[..]);
106108
let homepage = homepage.as_ref().map(|s| &s[..]);
107109
let documentation = documentation.as_ref().map(|s| &s[..]);
@@ -161,15 +163,16 @@ impl Crate {
161163
(name, user_id, created_at,
162164
updated_at, downloads, max_version,
163165
description, homepage, documentation,
164-
readme, keywords, repository, license)
166+
readme, keywords, repository, license,
167+
max_upload_size)
165168
VALUES ($1, $2, $3, $3, 0, '0.0.0',
166-
$4, $5, $6, $7, $8, $9, $10)
169+
$4, $5, $6, $7, $8, $9, $10, $11)
167170
RETURNING *"));
168171
let now = ::now();
169172
let rows = try!(stmt.query(&[&name, &user_id, &now,
170173
&description, &homepage,
171174
&documentation, &readme, &keywords,
172-
&repository, &license]));
175+
&repository, &license, &max_upload_size]));
173176
let ret: Crate = Model::from_row(&try!(rows.iter().next().chain_error(|| {
174177
internal("no crate returned")
175178
})));
@@ -241,7 +244,7 @@ impl Crate {
241244
let Crate {
242245
name, created_at, updated_at, downloads, max_version, description,
243246
homepage, documentation, keywords, license, repository,
244-
readme: _, id: _, user_id: _,
247+
readme: _, id: _, user_id: _, max_upload_size: _,
245248
} = self;
246249
let versions_link = match versions {
247250
Some(..) => None,
@@ -453,6 +456,7 @@ impl Model for Crate {
453456
.map(|s| s.to_string()).collect(),
454457
license: row.get("license"),
455458
repository: row.get("repository"),
459+
max_upload_size: row.get("max_upload_size"),
456460
}
457461
}
458462
fn table_name(_: Option<Crate>) -> &'static str { "crates" }
@@ -675,7 +679,8 @@ pub fn new(req: &mut Request) -> CargoResult<Response> {
675679
&keywords,
676680
&new_crate.repository,
677681
&new_crate.license,
678-
&new_crate.license_file));
682+
&new_crate.license_file,
683+
None));
679684

680685
let owners = try!(krate.owners(try!(req.tx())));
681686
if try!(rights(req.app(), &owners, &user)) < Rights::Publish {
@@ -687,6 +692,15 @@ pub fn new(req: &mut Request) -> CargoResult<Response> {
687692
return Err(human(format!("crate was previously named `{}`", krate.name)))
688693
}
689694

695+
let length = try!(req.content_length().chain_error(|| {
696+
human("missing header: Content-Length")
697+
}));
698+
let max = krate.max_upload_size.map(|m| m as u64)
699+
.unwrap_or(app.config.max_upload_size);
700+
if length > max {
701+
return Err(human(format!("max upload size is: {}", max)))
702+
}
703+
690704
// Persist the new version of this crate
691705
let mut version = try!(krate.add_version(try!(req.tx()), vers, &features,
692706
&new_crate.authors));
@@ -706,7 +720,7 @@ pub fn new(req: &mut Request) -> CargoResult<Response> {
706720
let path = krate.s3_path(&vers.to_string());
707721
let (resp, cksum) = {
708722
let length = try!(read_le_u32(req.body()));
709-
let body = LimitErrorReader::new(req.body(), app.config.max_upload_size);
723+
let body = LimitErrorReader::new(req.body(), max);
710724
let mut body = HashingReader::new(body);
711725
let resp = {
712726
let s3req = app.bucket.put(&mut handle, &path, &mut body,
@@ -761,19 +775,13 @@ pub fn new(req: &mut Request) -> CargoResult<Response> {
761775
}
762776

763777
fn parse_new_headers(req: &mut Request) -> CargoResult<(upload::NewCrate, User)> {
764-
// Make sure the tarball being uploaded looks sane
765-
let length = try!(req.content_length().chain_error(|| {
766-
human("missing header: Content-Length")
767-
}));
778+
// Read the json upload request
779+
let amt = try!(read_le_u32(req.body())) as u64;
768780
let max = req.app().config.max_upload_size;
769-
if length > max {
781+
if amt > max {
770782
return Err(human(format!("max upload size is: {}", max)))
771783
}
772-
773-
// Read the json upload request
774-
let amt = try!(read_le_u32(req.body())) as u64;
775-
if amt > max { return Err(human(format!("max upload size is: {}", max))) }
776-
let mut json = repeat(0).take(amt as usize).collect::<Vec<_>>();
784+
let mut json = vec![0; amt as usize];
777785
try!(read_fill(req.body(), &mut json));
778786
let json = try!(String::from_utf8(json).map_err(|_| {
779787
human("json body was not valid utf-8")

src/tests/all.rs

+19-11
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ fn krate(name: &str) -> Crate {
207207
keywords: Vec::new(),
208208
license: None,
209209
repository: None,
210+
max_upload_size: None,
210211
}
211212
}
212213

@@ -230,14 +231,15 @@ fn mock_crate_vers(req: &mut Request, krate: Crate, v: &semver::Version)
230231
-> (Crate, Version) {
231232
let user = req.extensions().find::<User>().unwrap();
232233
let mut krate = Crate::find_or_insert(req.tx().unwrap(), &krate.name,
233-
user.id, &krate.description,
234-
&krate.homepage,
235-
&krate.documentation,
236-
&krate.readme,
237-
&krate.keywords,
238-
&krate.repository,
239-
&krate.license,
240-
&None).unwrap();
234+
user.id, &krate.description,
235+
&krate.homepage,
236+
&krate.documentation,
237+
&krate.readme,
238+
&krate.keywords,
239+
&krate.repository,
240+
&krate.license,
241+
&None,
242+
krate.max_upload_size).unwrap();
241243
Keyword::update_crate(req.tx().unwrap(), &krate,
242244
&krate.keywords).unwrap();
243245
let v = krate.add_version(req.tx().unwrap(), v, &HashMap::new(), &[]);
@@ -292,10 +294,10 @@ fn new_req_body(krate: Crate, version: &str, deps: Vec<u::CrateDependency>)
292294
license: Some("MIT".to_string()),
293295
license_file: None,
294296
repository: krate.repository,
295-
})
297+
}, &[])
296298
}
297299

298-
fn new_crate_to_body(new_crate: &u::NewCrate) -> Vec<u8> {
300+
fn new_crate_to_body(new_crate: &u::NewCrate, krate: &[u8]) -> Vec<u8> {
299301
let json = json::encode(&new_crate).unwrap();
300302
let mut body = Vec::new();
301303
body.extend([
@@ -305,6 +307,12 @@ fn new_crate_to_body(new_crate: &u::NewCrate) -> Vec<u8> {
305307
(json.len() >> 24) as u8,
306308
].iter().cloned());
307309
body.extend(json.as_bytes().iter().cloned());
308-
body.extend([0, 0, 0, 0].iter().cloned());
310+
body.extend(&[
311+
(krate.len() >> 0) as u8,
312+
(krate.len() >> 8) as u8,
313+
(krate.len() >> 16) as u8,
314+
(krate.len() >> 24) as u8,
315+
]);
316+
body.extend(krate);
309317
body
310318
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
===REQUEST 2334
2+
PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-1.1.0.crate HTTP/1.1
3+
Accept: */*
4+
Proxy-Connection: Keep-Alive
5+
Content-Type: application/x-tar
6+
Date: Wed, 18 Nov 2015 14:29:12 -0800
7+
Authorization: AWS AKIAI3HZFJJPEQRCEPYQ:WQPwbR4xY3GNBJC7TQ+sys8zVKw=
8+
Host: alexcrichton-test.s3.amazonaws.com
9+
Content-Length: 2000
10+
11+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
12+
===RESPONSE 258
13+
HTTP/1.1 200
14+
server: AmazonS3
15+
etag: "7c1c566ab4cdb11ac8971191694e8bec"
16+
content-length: 0
17+
date: Wed, 18 Nov 2015 22:29:13 GMT
18+
x-amz-request-id: 0F61951F1BCD0727
19+
x-amz-id-2: JLgACuYs2DRQQf3vqYu6a3Bv0+PBuor+HcEGwKsnQjYtupRNAn/X/7KSytGWnTJS7VH+BPLFVJU=
20+
21+

src/tests/krate.rs

+40-21
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use std::collections::HashMap;
22
use std::io::prelude::*;
33
use std::fs::{self, File};
4-
use std::iter::repeat;
54

65
use conduit::{Handler, Request, Method};
76
use conduit_test::MockRequest;
@@ -35,6 +34,24 @@ struct RevDeps { dependencies: Vec<EncodableDependency>, meta: CrateMeta }
3534
#[derive(RustcDecodable)]
3635
struct Downloads { version_downloads: Vec<EncodableVersionDownload> }
3736

37+
fn new_crate(name: &str) -> u::NewCrate {
38+
u::NewCrate {
39+
name: u::CrateName(name.to_string()),
40+
vers: u::CrateVersion(semver::Version::parse("1.1.0").unwrap()),
41+
features: HashMap::new(),
42+
deps: Vec::new(),
43+
authors: vec!["foo".to_string()],
44+
description: Some("desc".to_string()),
45+
homepage: None,
46+
documentation: None,
47+
readme: None,
48+
keywords: None,
49+
license: Some("MIT".to_string()),
50+
license_file: None,
51+
repository: None,
52+
}
53+
}
54+
3855
#[test]
3956
fn index() {
4057
let (_b, _app, mut middle) = ::app();
@@ -332,8 +349,21 @@ fn new_krate_too_big() {
332349
let (_b, app, middle) = ::app();
333350
let mut req = ::new_req(app, "foo", "1.0.0");
334351
::mock_user(&mut req, ::user("foo"));
335-
req.with_body(repeat("a").take(1000 * 1000).collect::<String>().as_bytes());
336-
bad_resp!(middle.call(&mut req));
352+
let body = ::new_crate_to_body(&new_crate("foo"), &[b'a'; 2000]);
353+
bad_resp!(middle.call(req.with_body(&body)));
354+
}
355+
356+
#[test]
357+
fn new_krate_too_big_but_whitelisted() {
358+
let (_b, app, middle) = ::app();
359+
let mut req = ::new_req(app, "foo", "1.1.0");
360+
::mock_user(&mut req, ::user("foo"));
361+
let mut krate = ::krate("foo");
362+
krate.max_upload_size = Some(2 * 1000 * 1000);
363+
::mock_crate(&mut req, krate);
364+
let body = ::new_crate_to_body(&new_crate("foo"), &[b'a'; 2000]);
365+
let mut response = ok_resp!(middle.call(req.with_body(&body)));
366+
::json::<GoodCrate>(&mut response);
337367
}
338368

339369
#[test]
@@ -759,22 +789,11 @@ fn author_license_and_description_required() {
759789
::user("foo");
760790

761791
let mut req = ::req(app, Method::Put, "/api/v1/crates/new");
762-
let mut new_crate = u::NewCrate {
763-
name: u::CrateName("foo".to_string()),
764-
vers: u::CrateVersion(semver::Version::parse("1.0.0").unwrap()),
765-
features: HashMap::new(),
766-
deps: Vec::new(),
767-
authors: Vec::new(),
768-
description: None,
769-
homepage: None,
770-
documentation: None,
771-
readme: None,
772-
keywords: None,
773-
license: None,
774-
license_file: None,
775-
repository: None,
776-
};
777-
req.with_body(&::new_crate_to_body(&new_crate));
792+
let mut new_crate = new_crate("foo");
793+
new_crate.license = None;
794+
new_crate.description = None;
795+
new_crate.authors = Vec::new();
796+
req.with_body(&::new_crate_to_body(&new_crate, &[]));
778797
let json = bad_resp!(middle.call(&mut req));
779798
assert!(json.errors[0].detail.contains("author") &&
780799
json.errors[0].detail.contains("description") &&
@@ -783,7 +802,7 @@ fn author_license_and_description_required() {
783802

784803
new_crate.license = Some("MIT".to_string());
785804
new_crate.authors.push("".to_string());
786-
req.with_body(&::new_crate_to_body(&new_crate));
805+
req.with_body(&::new_crate_to_body(&new_crate, &[]));
787806
let json = bad_resp!(middle.call(&mut req));
788807
assert!(json.errors[0].detail.contains("author") &&
789808
json.errors[0].detail.contains("description") &&
@@ -793,7 +812,7 @@ fn author_license_and_description_required() {
793812
new_crate.license = None;
794813
new_crate.license_file = Some("foo".to_string());
795814
new_crate.authors.push("foo".to_string());
796-
req.with_body(&::new_crate_to_body(&new_crate));
815+
req.with_body(&::new_crate_to_body(&new_crate, &[]));
797816
let json = bad_resp!(middle.call(&mut req));
798817
assert!(!json.errors[0].detail.contains("author") &&
799818
json.errors[0].detail.contains("description") &&

0 commit comments

Comments
 (0)