1
1
//! Functionality related to publishing a new crate or version of a crate.
2
2
3
3
use crate :: auth:: AuthCheck ;
4
+ use axum:: body:: Bytes ;
4
5
use flate2:: read:: GzDecoder ;
5
6
use hex:: ToHex ;
6
- use http :: Request ;
7
+ use hyper :: body :: Buf ;
7
8
use sha2:: { Digest , Sha256 } ;
8
9
use std:: collections:: BTreeMap ;
9
10
use std:: io:: Read ;
10
11
use std:: path:: Path ;
11
12
12
13
use crate :: controllers:: cargo_prelude:: * ;
14
+ use crate :: controllers:: util:: RequestPartsExt ;
13
15
use crate :: models:: {
14
16
insert_version_owner_action, Category , Crate , DependencyKind , Keyword , NewCrate , NewVersion ,
15
17
Rights , VersionAction ,
@@ -20,7 +22,7 @@ use crate::middleware::log_request::CustomMetadataRequestExt;
20
22
use crate :: models:: token:: EndpointScope ;
21
23
use crate :: schema:: * ;
22
24
use crate :: util:: errors:: { cargo_err, AppResult } ;
23
- use crate :: util:: { read_fill , read_le_u32 , CargoVcsInfo , LimitErrorReader , Maximums } ;
25
+ use crate :: util:: { CargoVcsInfo , LimitErrorReader , Maximums } ;
24
26
use crate :: views:: {
25
27
EncodableCrate , EncodableCrateDependency , EncodableCrateUpload , GoodCrate , PublishWarnings ,
26
28
} ;
@@ -43,27 +45,38 @@ pub const WILDCARD_ERROR_MESSAGE: &str = "wildcard (`*`) dependency constraints
43
45
/// Currently blocks the HTTP thread, perhaps some function calls can spawn new
44
46
/// threads and return completion or error through other methods a `cargo publish
45
47
/// --status` command, via crates.io's front end, or email.
46
- pub async fn publish ( mut req : ConduitRequest ) -> AppResult < Json < GoodCrate > > {
47
- conduit_compat ( move || {
48
- let app = req. app ( ) . clone ( ) ;
48
+ pub async fn publish ( req : ConduitRequest ) -> AppResult < Json < GoodCrate > > {
49
+ let ( req, body) = req. 0 . into_parts ( ) ;
50
+ let bytes = body. into_inner ( ) ;
51
+ let ( json_bytes, tarball_bytes) = split_body ( bytes, & req) ?;
52
+
53
+ let new_crate: EncodableCrateUpload = serde_json:: from_slice ( & json_bytes)
54
+ . map_err ( |e| cargo_err ( & format_args ! ( "invalid upload request: {e}" ) ) ) ?;
55
+
56
+ req. add_custom_metadata ( "crate_name" , new_crate. name . to_string ( ) ) ;
57
+ req. add_custom_metadata ( "crate_version" , new_crate. vers . to_string ( ) ) ;
58
+
59
+ // Make sure required fields are provided
60
+ fn empty ( s : Option < & String > ) -> bool {
61
+ s. map_or ( true , String :: is_empty)
62
+ }
49
63
50
- // The format of the req.body() of a publish request is as follows:
51
- //
52
- // metadata length
53
- // metadata in JSON about the crate being published
54
- // .crate tarball length
55
- // .crate tarball file
56
- //
57
- // - The metadata is read and interpreted in the parse_new_headers function.
58
- // - The .crate tarball length is read in this function in order to save the size of the file
59
- // in the version record in the database.
60
- // - Then the .crate tarball length is passed to the upload_crate function where the actual
61
- // file is read and uploaded.
64
+ // It can have up to three elements per below conditions.
65
+ let mut missing = Vec :: with_capacity ( 3 ) ;
62
66
63
- let new_crate = parse_new_headers ( & mut req) ?;
67
+ if empty ( new_crate. description . as_ref ( ) ) {
68
+ missing. push ( "description" ) ;
69
+ }
70
+ if empty ( new_crate. license . as_ref ( ) ) && empty ( new_crate. license_file . as_ref ( ) ) {
71
+ missing. push ( "license" ) ;
72
+ }
73
+ if !missing. is_empty ( ) {
74
+ let message = missing_metadata_error_message ( & missing) ;
75
+ return Err ( cargo_err ( & message) ) ;
76
+ }
64
77
65
- req . add_custom_metadata ( "crate_name" , new_crate . name . to_string ( ) ) ;
66
- req. add_custom_metadata ( "crate_version" , new_crate . vers . to_string ( ) ) ;
78
+ conduit_compat ( move || {
79
+ let app = req. app ( ) . clone ( ) ;
67
80
68
81
let conn = app. primary_database . get ( ) ?;
69
82
@@ -156,13 +169,7 @@ pub async fn publish(mut req: ConduitRequest) -> AppResult<Json<GoodCrate>> {
156
169
}
157
170
}
158
171
159
- // Length of the .crate tarball, which appears after the metadata in the request body.
160
- // TODO: Not sure why we're using the total content length (metadata + .crate file length)
161
- // to compare against the max upload size... investigate that and perhaps change to use
162
- // this file length.
163
- let file_length = read_le_u32 ( req. body_mut ( ) ) ?;
164
-
165
- let content_length = req. body ( ) . get_ref ( ) . len ( ) as u64 ;
172
+ let content_length = tarball_bytes. len ( ) as u64 ;
166
173
167
174
let maximums = Maximums :: new (
168
175
krate. max_upload_size ,
@@ -181,9 +188,7 @@ pub async fn publish(mut req: ConduitRequest) -> AppResult<Json<GoodCrate>> {
181
188
let license = new_crate. license . clone ( ) ;
182
189
183
190
// Read tarball from request
184
- let mut tarball = Vec :: new ( ) ;
185
- req. body_mut ( ) . read_to_end ( & mut tarball) ?;
186
- let hex_cksum: String = Sha256 :: digest ( & tarball) . encode_hex ( ) ;
191
+ let hex_cksum: String = Sha256 :: digest ( & tarball_bytes) . encode_hex ( ) ;
187
192
188
193
// Persist the new version of this crate
189
194
let version = NewVersion :: new (
@@ -194,7 +199,7 @@ pub async fn publish(mut req: ConduitRequest) -> AppResult<Json<GoodCrate>> {
194
199
license_file,
195
200
// Downcast is okay because the file length must be less than the max upload size
196
201
// to get here, and max upload sizes are way less than i32 max
197
- file_length as i32 ,
202
+ content_length as i32 ,
198
203
user. id ,
199
204
hex_cksum. clone ( ) ,
200
205
links. clone ( ) ,
@@ -222,7 +227,8 @@ pub async fn publish(mut req: ConduitRequest) -> AppResult<Json<GoodCrate>> {
222
227
let top_versions = krate. top_versions ( & conn) ?;
223
228
224
229
let pkg_name = format ! ( "{}-{}" , krate. name, vers) ;
225
- let cargo_vcs_info = verify_tarball ( & pkg_name, & tarball, maximums. max_unpack_size ) ?;
230
+ let cargo_vcs_info =
231
+ verify_tarball ( & pkg_name, & tarball_bytes, maximums. max_unpack_size ) ?;
226
232
let pkg_path_in_vcs = cargo_vcs_info. map ( |info| info. path_in_vcs ) ;
227
233
228
234
if let Some ( readme) = new_crate. readme {
@@ -241,7 +247,7 @@ pub async fn publish(mut req: ConduitRequest) -> AppResult<Json<GoodCrate>> {
241
247
// Upload crate tarball
242
248
app. config
243
249
. uploader ( )
244
- . upload_crate ( app. http_client ( ) , tarball , & krate, vers) ?;
250
+ . upload_crate ( app. http_client ( ) , tarball_bytes , & krate, vers) ?;
245
251
246
252
let ( features, features2) : ( BTreeMap < _ , _ > , BTreeMap < _ , _ > ) =
247
253
features. into_iter ( ) . partition ( |( _k, vals) | {
@@ -300,44 +306,36 @@ fn count_versions_published_today(krate_id: i32, conn: &PgConnection) -> QueryRe
300
306
. get_result ( conn)
301
307
}
302
308
303
- /// Used by the `krate::new` function.
304
- ///
305
- /// This function parses the JSON headers to interpret the data and validates
306
- /// the data during and after the parsing. Returns crate metadata.
307
- fn parse_new_headers < B : Read > ( req : & mut Request < B > ) -> AppResult < EncodableCrateUpload > {
308
- // Read the json upload request
309
- let metadata_length = u64:: from ( read_le_u32 ( req. body_mut ( ) ) ?) ;
310
- req. add_custom_metadata ( "metadata_length" , metadata_length) ;
311
-
312
- let max = req. app ( ) . config . max_upload_size ;
313
- if metadata_length > max {
314
- return Err ( cargo_err ( & format_args ! ( "max upload size is: {max}" ) ) ) ;
315
- }
316
- let mut json = vec ! [ 0 ; metadata_length as usize ] ;
317
- read_fill ( req. body_mut ( ) , & mut json) ?;
318
- let new: EncodableCrateUpload = serde_json:: from_slice ( & json)
319
- . map_err ( |e| cargo_err ( & format_args ! ( "invalid upload request: {e}" ) ) ) ?;
320
-
321
- // Make sure required fields are provided
322
- fn empty ( s : Option < & String > ) -> bool {
323
- s. map_or ( true , String :: is_empty)
309
+ #[ instrument( skip_all) ]
310
+ fn split_body < R : RequestPartsExt > ( mut bytes : Bytes , req : & R ) -> AppResult < ( Bytes , Bytes ) > {
311
+ // The format of the req.body() of a publish request is as follows:
312
+ //
313
+ // metadata length
314
+ // metadata in JSON about the crate being published
315
+ // .crate tarball length
316
+ // .crate tarball file
317
+
318
+ let json_len = bytes. get_u32_le ( ) as usize ;
319
+ req. add_custom_metadata ( "metadata_length" , json_len) ;
320
+
321
+ if json_len > bytes. len ( ) {
322
+ return Err ( cargo_err ( & format ! (
323
+ "invalid metadata length for remaining payload: {json_len}"
324
+ ) ) ) ;
324
325
}
325
326
326
- // It can have up to three elements per below conditions.
327
- let mut missing = Vec :: with_capacity ( 3 ) ;
327
+ let json_bytes = bytes. split_to ( json_len) ;
328
328
329
- if empty ( new. description . as_ref ( ) ) {
330
- missing. push ( "description" ) ;
331
- }
332
- if empty ( new. license . as_ref ( ) ) && empty ( new. license_file . as_ref ( ) ) {
333
- missing. push ( "license" ) ;
334
- }
335
- if !missing. is_empty ( ) {
336
- let message = missing_metadata_error_message ( & missing) ;
337
- return Err ( cargo_err ( & message) ) ;
329
+ let tarball_len = bytes. get_u32_le ( ) as usize ;
330
+ if tarball_len > bytes. len ( ) {
331
+ return Err ( cargo_err ( & format ! (
332
+ "invalid metadata length for remaining payload: {tarball_len}"
333
+ ) ) ) ;
338
334
}
339
335
340
- Ok ( new)
336
+ let tarball_bytes = bytes. split_to ( tarball_len) ;
337
+
338
+ Ok ( ( json_bytes, tarball_bytes) )
341
339
}
342
340
343
341
pub fn missing_metadata_error_message ( missing : & [ & str ] ) -> String {
0 commit comments