line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
###################################################################### |
2
|
|
|
|
|
|
|
# HTTP Connection from a reverse proxy client. GET/HEAD only. |
3
|
|
|
|
|
|
|
# most functionality is implemented in the base class. |
4
|
|
|
|
|
|
|
# |
5
|
|
|
|
|
|
|
# Copyright 2004, Danga Interactive, Inc. |
6
|
|
|
|
|
|
|
# Copyright 2005-2007, Six Apart, Ltd. |
7
|
|
|
|
|
|
|
# |
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
package Perlbal::ClientHTTP; |
10
|
22
|
|
|
22
|
|
412
|
use strict; |
|
22
|
|
|
|
|
45
|
|
|
22
|
|
|
|
|
885
|
|
11
|
22
|
|
|
22
|
|
124
|
use warnings; |
|
22
|
|
|
|
|
50
|
|
|
22
|
|
|
|
|
1015
|
|
12
|
22
|
|
|
22
|
|
188
|
no warnings qw(deprecated); |
|
22
|
|
|
|
|
49
|
|
|
22
|
|
|
|
|
786
|
|
13
|
|
|
|
|
|
|
|
14
|
22
|
|
|
22
|
|
119
|
use base "Perlbal::ClientHTTPBase"; |
|
22
|
|
|
|
|
42
|
|
|
22
|
|
|
|
|
6436
|
|
15
|
22
|
|
|
22
|
|
455
|
use Perlbal::Util; |
|
22
|
|
|
|
|
49
|
|
|
22
|
|
|
|
|
1211
|
|
16
|
|
|
|
|
|
|
|
17
|
22
|
|
|
|
|
165
|
use fields ('put_in_progress', # 1 when we're currently waiting for an async job to return |
18
|
|
|
|
|
|
|
'put_fh', # file handle to use for writing data |
19
|
|
|
|
|
|
|
'put_fh_filename', # filename of put_fh |
20
|
|
|
|
|
|
|
'put_final_name', # final pathname of put_fh |
21
|
|
|
|
|
|
|
'put_pos', # file offset to write next data at |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
'content_length', # length of document being transferred |
24
|
|
|
|
|
|
|
'content_length_remain', # bytes remaining to be read |
25
|
|
|
|
|
|
|
'chunked_upload_state', # bool/obj: if processing a chunked upload, Perlbal::ChunkedUploadState object, else undef |
26
|
|
|
|
|
|
|
'md5_ctx', # Digest::MD5 used to verify Content-MD5 |
27
|
22
|
|
|
22
|
|
124
|
); |
|
22
|
|
|
|
|
85
|
|
28
|
|
|
|
|
|
|
|
29
|
22
|
|
|
22
|
|
1960
|
use HTTP::Date (); |
|
22
|
|
|
|
|
49
|
|
|
22
|
|
|
|
|
393
|
|
30
|
22
|
|
|
22
|
|
114
|
use File::Path; |
|
22
|
|
|
|
|
39
|
|
|
22
|
|
|
|
|
2541
|
|
31
|
|
|
|
|
|
|
|
32
|
22
|
|
|
22
|
|
132
|
use Errno qw( EPIPE ); |
|
22
|
|
|
|
|
42
|
|
|
22
|
|
|
|
|
1133
|
|
33
|
22
|
|
|
22
|
|
113
|
use POSIX qw( O_CREAT O_TRUNC O_WRONLY O_RDONLY O_EXCL ENOENT EEXIST ); |
|
22
|
|
|
|
|
45
|
|
|
22
|
|
|
|
|
227
|
|
34
|
22
|
|
|
22
|
|
2384
|
use Digest::MD5; |
|
22
|
|
|
|
|
49
|
|
|
22
|
|
|
|
|
99491
|
|
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
# class list of directories we know exist |
37
|
|
|
|
|
|
|
our (%VerifiedDirs); |
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
sub new { |
40
|
42
|
|
|
42
|
1
|
110
|
my $class = shift; |
41
|
|
|
|
|
|
|
|
42
|
42
|
|
|
|
|
248
|
my $self = fields::new($class); |
43
|
42
|
|
|
|
|
26752
|
$self->SUPER::new(@_); |
44
|
42
|
|
|
|
|
1590
|
$self->init; |
45
|
42
|
|
|
|
|
258
|
return $self; |
46
|
|
|
|
|
|
|
} |
47
|
|
|
|
|
|
|
|
48
|
|
|
|
|
|
|
# upcasting a generic ClientHTTPBase (from a service selector) to a |
49
|
|
|
|
|
|
|
# "full-fledged" ClientHTTP. |
50
|
|
|
|
|
|
|
sub new_from_base { |
51
|
7
|
|
|
7
|
0
|
19
|
my $class = shift; |
52
|
7
|
|
|
|
|
174
|
my Perlbal::ClientHTTPBase $cb = shift; # base object |
53
|
7
|
|
|
|
|
38
|
Perlbal::Util::rebless($cb, $class); |
54
|
7
|
|
|
|
|
36
|
$cb->init; |
55
|
|
|
|
|
|
|
|
56
|
7
|
|
|
|
|
43
|
$cb->watch_read(1); # enable our reads, so we can get PUT/POST data |
57
|
7
|
|
|
|
|
228
|
$cb->handle_request; # this will disable reads, if GET/HEAD/etc |
58
|
7
|
|
|
|
|
298
|
return $cb; |
59
|
|
|
|
|
|
|
} |
60
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
sub init { |
62
|
49
|
|
|
49
|
0
|
89
|
my Perlbal::ClientHTTP $self = shift; |
63
|
49
|
|
|
|
|
127
|
$self->{put_in_progress} = 0; |
64
|
49
|
|
|
|
|
108
|
$self->{put_fh} = undef; |
65
|
49
|
|
|
|
|
113
|
$self->{put_pos} = 0; |
66
|
49
|
|
|
|
|
101
|
$self->{chunked_upload_state} = undef; |
67
|
49
|
|
|
|
|
101
|
$self->{md5_ctx} = undef; |
68
|
49
|
|
|
|
|
103
|
$self->{put_final_name} = undef; |
69
|
|
|
|
|
|
|
} |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
sub close { |
72
|
41
|
|
|
41
|
1
|
162
|
my Perlbal::ClientHTTP $self = shift; |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
# don't close twice |
75
|
41
|
50
|
|
|
|
146
|
return if $self->{closed}; |
76
|
|
|
|
|
|
|
|
77
|
41
|
|
|
|
|
85
|
$self->{put_fh} = undef; |
78
|
41
|
|
|
|
|
233
|
$self->SUPER::close(@_); |
79
|
|
|
|
|
|
|
} |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
sub setup_keepalive { |
82
|
63
|
|
|
63
|
0
|
131
|
my Perlbal::ClientHTTP $self = $_[0]; |
83
|
63
|
|
100
|
|
|
302
|
my $not_done_reading = defined $self->{content_length_remain} && $self->{content_length_remain} > 0; |
84
|
|
|
|
|
|
|
|
85
|
63
|
100
|
|
|
|
771
|
return $self->SUPER::setup_keepalive($_[1], $not_done_reading ? 0 : undef); |
86
|
|
|
|
|
|
|
} |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
sub event_read { |
89
|
99
|
|
|
99
|
1
|
812963
|
my Perlbal::ClientHTTP $self = shift; |
90
|
99
|
|
|
|
|
302
|
$self->{alive_time} = $Perlbal::tick_time; |
91
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
# see if we have headers? |
93
|
99
|
100
|
|
|
|
403
|
if ($self->{req_headers}) { |
94
|
40
|
50
|
|
|
|
163
|
if ($self->{req_headers}->request_method eq 'PUT') { |
95
|
40
|
|
|
|
|
117
|
$self->event_read_put; |
96
|
|
|
|
|
|
|
} else { |
97
|
|
|
|
|
|
|
# since we have headers and we're not doing any special |
98
|
|
|
|
|
|
|
# handling above, let's just disable read notification, because |
99
|
|
|
|
|
|
|
# we won't do anything with the data |
100
|
0
|
|
|
|
|
0
|
$self->watch_read(0); |
101
|
|
|
|
|
|
|
} |
102
|
40
|
|
|
|
|
852
|
return; |
103
|
|
|
|
|
|
|
} |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
# try and get the headers, if they're all here |
106
|
59
|
100
|
|
|
|
585
|
my $hd = $self->read_request_headers |
107
|
|
|
|
|
|
|
or return; |
108
|
|
|
|
|
|
|
|
109
|
56
|
|
|
|
|
253
|
$self->handle_request; |
110
|
|
|
|
|
|
|
} |
111
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
# one-time routing of new request to the right handlers |
113
|
|
|
|
|
|
|
sub handle_request { |
114
|
63
|
|
|
63
|
0
|
135
|
my Perlbal::ClientHTTP $self = shift; |
115
|
63
|
|
|
|
|
150
|
my $hd = $self->{req_headers}; |
116
|
|
|
|
|
|
|
|
117
|
63
|
|
|
|
|
401
|
$self->check_req_headers; |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
# fully formed request received |
120
|
63
|
|
|
|
|
139
|
$self->{requests}++; |
121
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
# notify that we're about to serve |
123
|
63
|
50
|
|
|
|
739
|
return if $self->{service}->run_hook('start_web_request', $self); |
124
|
63
|
100
|
|
|
|
239
|
return if $self->{service}->run_hook('start_http_request', $self); |
125
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
# GET/HEAD requests (local, from disk) |
127
|
62
|
100
|
100
|
|
|
1162
|
if ($hd->request_method eq 'GET' || $hd->request_method eq 'HEAD') { |
128
|
|
|
|
|
|
|
# and once we have it, start serving |
129
|
45
|
|
|
|
|
390
|
$self->watch_read(0); |
130
|
45
|
|
|
|
|
1439
|
return $self->_serve_request($hd); |
131
|
|
|
|
|
|
|
} |
132
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
# PUT requests |
134
|
17
|
100
|
|
|
|
54
|
return $self->handle_put if $hd->request_method eq 'PUT'; |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
# DELETE requests |
137
|
3
|
50
|
|
|
|
14
|
return $self->handle_delete if $hd->request_method eq 'DELETE'; |
138
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
# else, bad request |
140
|
0
|
|
|
|
|
0
|
return $self->send_response(400); |
141
|
|
|
|
|
|
|
} |
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
sub handle_put { |
144
|
14
|
|
|
14
|
0
|
31
|
my Perlbal::ClientHTTP $self = shift; |
145
|
14
|
|
|
|
|
35
|
my $hd = $self->{req_headers}; |
146
|
|
|
|
|
|
|
|
147
|
14
|
100
|
|
|
|
73
|
return $self->send_response(403) unless $self->{service}->{enable_put}; |
148
|
|
|
|
|
|
|
|
149
|
13
|
100
|
100
|
|
|
89
|
$self->{md5_ctx} = $self->{service}->{enable_md5} && $hd->header('Content-MD5') ? Digest::MD5->new : undef; |
150
|
|
|
|
|
|
|
|
151
|
13
|
100
|
|
|
|
53
|
return if $self->handle_put_chunked; |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
# they want to put something, so let's setup and wait for more reads |
154
|
12
|
|
|
|
|
38
|
my $clen = |
155
|
|
|
|
|
|
|
$self->{content_length} = |
156
|
|
|
|
|
|
|
$self->{content_length_remain} = |
157
|
|
|
|
|
|
|
$hd->header('Content-length') + 0; |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
# return a 400 (bad request) if we got no content length or if it's |
160
|
|
|
|
|
|
|
# bigger than any specified max put size |
161
|
12
|
50
|
33
|
|
|
129
|
return $self->send_response(400, "Content-length of $clen is invalid.") |
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
162
|
|
|
|
|
|
|
if ! defined($clen) || |
163
|
|
|
|
|
|
|
$clen < 0 || |
164
|
|
|
|
|
|
|
($self->{service}->{max_put_size} && |
165
|
|
|
|
|
|
|
$clen > $self->{service}->{max_put_size}); |
166
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
# if we are supposed to read data and have some data already from a header over-read, note it |
168
|
12
|
50
|
66
|
|
|
107
|
if ($clen && defined $self->{read_ahead} && $self->{read_ahead} > 0) { |
|
|
|
66
|
|
|
|
|
169
|
0
|
|
|
|
|
0
|
$self->{content_length_remain} -= $self->{read_ahead}; |
170
|
|
|
|
|
|
|
} |
171
|
|
|
|
|
|
|
|
172
|
12
|
50
|
|
|
|
55
|
return if $self->{service}->run_hook('handle_put', $self); |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
# error in filename? (any .. is an error) |
175
|
12
|
|
|
|
|
50
|
my $uri = $self->{req_headers}->request_uri; |
176
|
12
|
50
|
|
|
|
546
|
return $self->send_response(400, 'Invalid filename') |
177
|
|
|
|
|
|
|
if $uri =~ /\.\./; |
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
# now we want to get the URI |
180
|
12
|
50
|
|
|
|
127
|
return $self->send_response(400, 'Invalid filename') |
181
|
|
|
|
|
|
|
unless $uri =~ m!^ |
182
|
|
|
|
|
|
|
((?:/[\w\-\.]+)*) # $1: zero+ path components of /FOO where foo is |
183
|
|
|
|
|
|
|
# one+ conservative characters |
184
|
|
|
|
|
|
|
/ # path separator |
185
|
|
|
|
|
|
|
([\w\-\.]+) # $2: and the filename, one+ conservative characters |
186
|
|
|
|
|
|
|
$!x; |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
# sanitize uri into path and file into a disk path and filename |
189
|
12
|
|
100
|
|
|
78
|
my ($path, $filename) = ($1 || '', $2); |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
# the final action we'll be taking, eventually, is to start an async |
192
|
|
|
|
|
|
|
# file open of the requested disk path. but we might need to verify |
193
|
|
|
|
|
|
|
# the min_put_directory first. |
194
|
|
|
|
|
|
|
my $start_open = sub { |
195
|
11
|
|
|
11
|
|
53
|
my $disk_path = $self->{service}->{docroot} . '/' . $path; |
196
|
11
|
|
|
|
|
49
|
$self->start_put_open($disk_path, $filename); |
197
|
12
|
|
|
|
|
83
|
}; |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
# verify minput if necessary |
200
|
12
|
100
|
|
|
|
71
|
if ($self->{service}->{min_put_directory}) { |
201
|
3
|
50
|
|
|
|
13
|
my @elems = grep { defined $_ && length $_ } split '/', $path; |
|
9
|
|
|
|
|
45
|
|
202
|
3
|
50
|
|
|
|
16
|
return $self->send_response(400, 'Does not meet minimum directory requirement') |
203
|
|
|
|
|
|
|
unless scalar(@elems) >= $self->{service}->{min_put_directory}; |
204
|
3
|
|
|
|
|
16
|
my $req_path = '/' . join('/', splice(@elems, 0, $self->{service}->{min_put_directory})); |
205
|
3
|
|
|
|
|
10
|
my $extra_path = '/' . join('/', @elems); |
206
|
3
|
|
|
|
|
16
|
$self->validate_min_put_directory($req_path, $extra_path, $filename, $start_open); |
207
|
|
|
|
|
|
|
} else { |
208
|
9
|
|
|
|
|
19
|
$start_open->(); |
209
|
|
|
|
|
|
|
} |
210
|
|
|
|
|
|
|
|
211
|
12
|
|
|
|
|
235
|
return; |
212
|
|
|
|
|
|
|
} |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
sub handle_put_chunked { |
215
|
13
|
|
|
13
|
0
|
30
|
my Perlbal::ClientHTTP $self = shift; |
216
|
13
|
|
|
|
|
41
|
my $req_hd = $self->{req_headers}; |
217
|
13
|
|
|
|
|
43
|
my $te = $req_hd->header("Transfer-Encoding"); |
218
|
13
|
100
|
66
|
|
|
77
|
return unless $te && $te eq "chunked"; |
219
|
|
|
|
|
|
|
|
220
|
1
|
|
|
|
|
3
|
my $eh = $req_hd->header("Expect"); |
221
|
1
|
50
|
33
|
|
|
12
|
if ($eh && $eh =~ /\b100-continue\b/) { |
222
|
1
|
|
|
|
|
10
|
$self->write(\ "HTTP/1.1 100 Continue\r\n\r\n"); |
223
|
|
|
|
|
|
|
} |
224
|
|
|
|
|
|
|
|
225
|
1
|
|
|
|
|
3
|
my $max_size = $self->{service}{max_chunked_request_size}; |
226
|
|
|
|
|
|
|
|
227
|
|
|
|
|
|
|
# error in filename? (any .. is an error) |
228
|
1
|
|
|
|
|
6
|
my $uri = $self->{req_headers}->request_uri; |
229
|
1
|
50
|
|
|
|
5
|
return $self->send_response(400, 'Invalid filename') |
230
|
|
|
|
|
|
|
if $uri =~ /\.\./; |
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
# now we want to get the URI |
233
|
1
|
50
|
|
|
|
16
|
return $self->send_response(400, 'Invalid filename') |
234
|
|
|
|
|
|
|
unless $uri =~ m!^ |
235
|
|
|
|
|
|
|
((?:/[\w\-\.]+)*) # $1: zero+ path components of /FOO where foo is |
236
|
|
|
|
|
|
|
# one+ conservative characters |
237
|
|
|
|
|
|
|
/ # path separator |
238
|
|
|
|
|
|
|
([\w\-\.]+) # $2: and the filename, one+ conservative characters |
239
|
|
|
|
|
|
|
$!x; |
240
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
# sanitize uri into path and file into a disk path and filename |
242
|
1
|
|
50
|
|
|
9
|
my ($path, $filename) = ($1 || '', $2); |
243
|
|
|
|
|
|
|
|
244
|
1
|
|
|
|
|
4
|
my $disk_path = $self->{service}->{docroot} . '/' . $path; |
245
|
|
|
|
|
|
|
|
246
|
1
|
|
|
|
|
3
|
$self->{chunked_upload_state} = Perlbal::ChunkedUploadState->new(%{{ |
247
|
|
|
|
|
|
|
on_new_chunk => sub { |
248
|
32
|
|
|
32
|
|
40
|
my $cref = shift; |
249
|
32
|
|
|
|
|
43
|
my $len = length($$cref); |
250
|
32
|
|
|
|
|
45
|
push @{$self->{read_buf}}, $cref; |
|
32
|
|
|
|
|
75
|
|
251
|
|
|
|
|
|
|
|
252
|
32
|
|
|
|
|
71
|
$self->{read_ahead} += $len; |
253
|
32
|
|
|
|
|
54
|
$self->{content_length} += $len; |
254
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
# if too large, disconnect them... |
256
|
32
|
50
|
33
|
|
|
180
|
if ($max_size && $self->{content_length} > $max_size) { |
257
|
|
|
|
|
|
|
# TODO: delete file at this point? we're disconnecting them |
258
|
|
|
|
|
|
|
# to prevent them from writing more, but do we care to keep |
259
|
|
|
|
|
|
|
# what they already wrote? |
260
|
0
|
|
|
|
|
0
|
$self->close; |
261
|
0
|
|
|
|
|
0
|
return; |
262
|
|
|
|
|
|
|
} |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
# Reading too far ahead of our AIO subsystem will cause us to buffer it in memory. |
265
|
32
|
50
|
|
|
|
71
|
$self->watch_read(0) if $self->{read_ahead} >= 1024 * 1024; # arbitrary |
266
|
|
|
|
|
|
|
# ->put_writeout clears {read_ahead}, so we run it after we need that |
267
|
32
|
100
|
|
|
|
123
|
$self->put_writeout if $self->{read_ahead} >= 8192; # arbitrary |
268
|
|
|
|
|
|
|
}, |
269
|
|
|
|
|
|
|
on_disconnect => sub { |
270
|
0
|
|
|
0
|
|
0
|
warn "Disconnect during chunked PUT.\n"; |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
# TODO: do we unlink the file here, since it wasn't a proper close |
273
|
|
|
|
|
|
|
# ending in a zero-length chunk? perhaps a config option? for |
274
|
|
|
|
|
|
|
# now we'll just leave it on disk with what we've got so far: |
275
|
0
|
|
|
|
|
0
|
$self->close('remote_closure_during_chunked_put'); |
276
|
|
|
|
|
|
|
}, |
277
|
|
|
|
|
|
|
on_zero_chunk => sub { |
278
|
1
|
|
|
1
|
|
3
|
$self->{chunked_upload_state} = undef; |
279
|
1
|
|
|
|
|
5
|
$self->watch_read(0); |
280
|
|
|
|
|
|
|
|
281
|
|
|
|
|
|
|
# kick off any necessary aio writes: |
282
|
1
|
|
|
|
|
31
|
$self->put_writeout; |
283
|
|
|
|
|
|
|
# this will do nothing, if a put is already in progress: |
284
|
1
|
|
|
|
|
5
|
$self->put_close; |
285
|
|
|
|
|
|
|
}, |
286
|
1
|
|
|
|
|
30
|
}}); |
287
|
|
|
|
|
|
|
|
288
|
1
|
|
|
|
|
6
|
$self->start_put_open($disk_path, $filename); |
289
|
|
|
|
|
|
|
|
290
|
1
|
|
|
|
|
20
|
return 1; |
291
|
|
|
|
|
|
|
} |
292
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
# called when we're requested to do a delete |
294
|
|
|
|
|
|
|
sub handle_delete { |
295
|
3
|
|
|
3
|
0
|
7
|
my Perlbal::ClientHTTP $self = shift; |
296
|
|
|
|
|
|
|
|
297
|
3
|
100
|
|
|
|
20
|
return $self->send_response(403) unless $self->{service}->{enable_delete}; |
298
|
|
|
|
|
|
|
|
299
|
2
|
|
|
|
|
12
|
$self->watch_read(0); |
300
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
# error in filename? (any .. is an error) |
302
|
2
|
|
|
|
|
65
|
my $uri = $self->{req_headers}->request_uri; |
303
|
2
|
50
|
|
|
|
10
|
return $self->send_response(400, 'Invalid filename') |
304
|
|
|
|
|
|
|
if $uri =~ /\.\./; |
305
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
# now we want to get the URI |
307
|
2
|
50
|
|
|
|
16
|
if ($uri =~ m!^(?:/[\w\-\.]+)+$!) { |
308
|
|
|
|
|
|
|
# now attempt the unlink |
309
|
|
|
|
|
|
|
Perlbal::AIO::aio_unlink($self->{service}->{docroot} . '/' . $uri, sub { |
310
|
2
|
|
|
2
|
|
4
|
my $err = shift; |
311
|
2
|
100
|
|
|
|
21
|
if ($err == 0) { |
|
|
50
|
|
|
|
|
|
312
|
|
|
|
|
|
|
# delete was successful |
313
|
1
|
|
|
|
|
8
|
return $self->send_response(204); |
314
|
|
|
|
|
|
|
} elsif ($! == ENOENT) { |
315
|
|
|
|
|
|
|
# no such file |
316
|
1
|
|
|
|
|
7
|
return $self->send_response(404); |
317
|
|
|
|
|
|
|
} else { |
318
|
|
|
|
|
|
|
# failure... |
319
|
0
|
|
|
|
|
0
|
return $self->send_response(400, "$!"); |
320
|
|
|
|
|
|
|
} |
321
|
2
|
|
|
|
|
29
|
}); |
322
|
|
|
|
|
|
|
} else { |
323
|
|
|
|
|
|
|
# bad URI, don't accept the delete |
324
|
0
|
|
|
|
|
0
|
return $self->send_response(400, 'Invalid filename'); |
325
|
|
|
|
|
|
|
} |
326
|
|
|
|
|
|
|
} |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
sub event_read_put { |
329
|
40
|
|
|
40
|
0
|
54
|
my Perlbal::ClientHTTP $self = shift; |
330
|
|
|
|
|
|
|
|
331
|
40
|
100
|
|
|
|
124
|
if (my $cus = $self->{chunked_upload_state}) { |
332
|
30
|
|
|
|
|
96
|
$cus->on_readable($self); |
333
|
30
|
|
|
|
|
196
|
return; |
334
|
|
|
|
|
|
|
} |
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
# read in data and shove it on the read buffer |
337
|
10
|
|
|
|
|
45
|
my $dataref = $self->read($self->{content_length_remain}); |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
# unless they disconnected prematurely |
340
|
10
|
50
|
|
|
|
265
|
unless (defined $dataref) { |
341
|
0
|
|
|
|
|
0
|
$self->close('remote_closure'); |
342
|
0
|
|
|
|
|
0
|
return; |
343
|
|
|
|
|
|
|
} |
344
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
# got some data |
346
|
10
|
|
|
|
|
19
|
push @{$self->{read_buf}}, $dataref; |
|
10
|
|
|
|
|
25
|
|
347
|
10
|
|
|
|
|
22
|
my $clen = length($$dataref); |
348
|
10
|
|
|
|
|
26
|
$self->{read_size} += $clen; |
349
|
10
|
|
|
|
|
19
|
$self->{read_ahead} += $clen; |
350
|
10
|
|
|
|
|
17
|
$self->{content_length_remain} -= $clen; |
351
|
|
|
|
|
|
|
|
352
|
10
|
50
|
|
|
|
29
|
if ($self->{content_length_remain}) { |
353
|
|
|
|
|
|
|
# Reading too far ahead of our AIO subsystem will cause us to buffer it in memory. |
354
|
0
|
0
|
|
|
|
0
|
$self->watch_read(0) if $self->{read_ahead} >= 1024 * 1024; # arbitrary |
355
|
|
|
|
|
|
|
# ->put_writeout clears {read_ahead}, so we run it after we need that |
356
|
0
|
0
|
|
|
|
0
|
$self->put_writeout if $self->{read_ahead} >= 8192; # arbitrary |
357
|
|
|
|
|
|
|
} else { |
358
|
|
|
|
|
|
|
# now, if we've filled the content of this put, we're done |
359
|
10
|
|
|
|
|
37
|
$self->watch_read(0); |
360
|
10
|
|
|
|
|
341
|
$self->put_writeout; |
361
|
|
|
|
|
|
|
} |
362
|
|
|
|
|
|
|
} |
363
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
# verify that a minimum put directory exists. if/when it's verified, |
365
|
|
|
|
|
|
|
# perhaps cached, the provided callback will be run. |
366
|
|
|
|
|
|
|
sub validate_min_put_directory { |
367
|
3
|
|
|
3
|
0
|
7
|
my Perlbal::ClientHTTP $self = shift; |
368
|
3
|
|
|
|
|
10
|
my ($req_path, $extra_path, $filename, $callback) = @_; |
369
|
|
|
|
|
|
|
|
370
|
3
|
|
|
|
|
13
|
my $disk_dir = $self->{service}->{docroot} . '/' . $req_path; |
371
|
3
|
100
|
|
|
|
15
|
return $callback->() if $VerifiedDirs{$disk_dir}; |
372
|
|
|
|
|
|
|
|
373
|
2
|
|
|
|
|
5
|
$self->{put_in_progress} = 1; |
374
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
Perlbal::AIO::aio_open($disk_dir, O_RDONLY, 0755, sub { |
376
|
2
|
|
|
2
|
|
5
|
my $fh = shift; |
377
|
2
|
|
|
|
|
7
|
$self->{put_in_progress} = 0; |
378
|
|
|
|
|
|
|
|
379
|
|
|
|
|
|
|
# if error return failure |
380
|
2
|
100
|
|
|
|
12
|
return $self->send_response(404, "Base directory does not exist") unless $fh; |
381
|
1
|
|
|
|
|
15
|
CORE::close($fh); |
382
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
# mindir existed, mark it as so and start the open for the rest of the path |
384
|
1
|
|
|
|
|
4
|
$VerifiedDirs{$disk_dir} = 1; |
385
|
1
|
|
|
|
|
4
|
$callback->(); |
386
|
2
|
|
|
|
|
23
|
}); |
387
|
|
|
|
|
|
|
} |
388
|
|
|
|
|
|
|
|
389
|
|
|
|
|
|
|
# attempt to open a file being PUT for writing to disk |
390
|
|
|
|
|
|
|
sub start_put_open { |
391
|
13
|
|
|
13
|
0
|
28
|
my Perlbal::ClientHTTP $self = shift; |
392
|
13
|
|
|
|
|
34
|
my ($path, $file) = @_; |
393
|
13
|
|
|
|
|
16
|
my ($fs_path, $open_flags); |
394
|
|
|
|
|
|
|
|
395
|
13
|
|
|
|
|
29
|
$self->{put_in_progress} = 1; |
396
|
13
|
100
|
|
|
|
50
|
if ($self->{md5_ctx}) { |
397
|
2
|
|
|
|
|
15
|
$fs_path = "$path/$file.$$." . int(rand(0xffffffff)) . '.tmp'; |
398
|
2
|
|
|
|
|
6
|
$self->{put_final_name} = "$path/$file"; |
399
|
2
|
|
|
|
|
4
|
$open_flags = O_CREAT | O_EXCL | O_WRONLY; |
400
|
|
|
|
|
|
|
} else { |
401
|
11
|
|
|
|
|
38
|
$fs_path = "$path/$file"; |
402
|
11
|
|
|
|
|
20
|
$open_flags = O_CREAT | O_TRUNC | O_WRONLY; |
403
|
|
|
|
|
|
|
} |
404
|
|
|
|
|
|
|
|
405
|
|
|
|
|
|
|
Perlbal::AIO::aio_open($fs_path, $open_flags, 0644, sub { |
406
|
|
|
|
|
|
|
# get the fd |
407
|
13
|
|
|
13
|
|
33
|
my $fh = shift; |
408
|
|
|
|
|
|
|
|
409
|
|
|
|
|
|
|
# verify file was opened |
410
|
13
|
|
|
|
|
30
|
$self->{put_in_progress} = 0; |
411
|
|
|
|
|
|
|
|
412
|
13
|
100
|
|
|
|
40
|
if (! $fh) { |
413
|
1
|
50
|
0
|
|
|
16
|
if ($! == ENOENT) { |
|
|
0
|
|
|
|
|
|
414
|
|
|
|
|
|
|
# directory doesn't exist, so let's manually create it |
415
|
1
|
|
|
|
|
3
|
eval { File::Path::mkpath($path, 0, 0755); }; |
|
1
|
|
|
|
|
928
|
|
416
|
1
|
50
|
|
|
|
6
|
return $self->system_error("Unable to create directory", "path = $path, file = $file") if $@; |
417
|
|
|
|
|
|
|
|
418
|
|
|
|
|
|
|
# should be created, call self recursively to try |
419
|
1
|
|
|
|
|
9
|
return $self->start_put_open($path, $file); |
420
|
|
|
|
|
|
|
} elsif ($! == EEXIST && $self->{put_final_name}) { |
421
|
|
|
|
|
|
|
# temp name collision, bail hard because this should be near impossible already |
422
|
0
|
|
|
|
|
0
|
Perlbal::log('crit', "Failure to open exclusively $fs_path as temp file in PUT"); |
423
|
0
|
|
|
|
|
0
|
return $self->_simple_response(500); |
424
|
|
|
|
|
|
|
} else { |
425
|
0
|
|
|
|
|
0
|
return $self->system_error("Internal error", "error = $!, path = $path, file = $file"); |
426
|
|
|
|
|
|
|
} |
427
|
|
|
|
|
|
|
} |
428
|
|
|
|
|
|
|
|
429
|
12
|
|
|
|
|
25
|
$self->{put_fh} = $fh; |
430
|
12
|
|
|
|
|
31
|
$self->{put_pos} = 0; |
431
|
12
|
|
|
|
|
34
|
$self->{put_fh_filename} = $fs_path; |
432
|
|
|
|
|
|
|
|
433
|
|
|
|
|
|
|
# We just opened the file, haven't read_ahead any bytes, are expecting 0 bytes for read and we're |
434
|
|
|
|
|
|
|
# not in chunked mode, so close the file immediately, we're done. |
435
|
12
|
100
|
66
|
|
|
95
|
unless ($self->{read_ahead} || $self->{content_length_remain} || $self->{chunked_upload_state}) { |
|
|
|
100
|
|
|
|
|
436
|
|
|
|
|
|
|
# FIXME this should be done through AIO |
437
|
1
|
|
|
|
|
6
|
$self->put_close; |
438
|
1
|
|
|
|
|
6
|
return; |
439
|
|
|
|
|
|
|
} |
440
|
|
|
|
|
|
|
|
441
|
11
|
|
|
|
|
54
|
$self->put_writeout; |
442
|
13
|
|
|
|
|
148
|
}); |
443
|
|
|
|
|
|
|
} |
444
|
|
|
|
|
|
|
|
445
|
|
|
|
|
|
|
# called when we've got some put data to write out |
446
|
|
|
|
|
|
|
sub put_writeout { |
447
|
36
|
|
|
36
|
0
|
56
|
my Perlbal::ClientHTTP $self = shift; |
448
|
36
|
50
|
|
|
|
109
|
Carp::confess("wrong class for $self") unless ref $self eq "Perlbal::ClientHTTP"; |
449
|
|
|
|
|
|
|
|
450
|
36
|
50
|
|
|
|
151
|
return if $self->{service}->run_hook('put_writeout', $self); |
451
|
36
|
50
|
|
|
|
93
|
return if $self->{put_in_progress}; |
452
|
36
|
50
|
|
|
|
91
|
return unless $self->{put_fh}; |
453
|
36
|
100
|
|
|
|
251
|
return unless $self->{read_ahead}; |
454
|
|
|
|
|
|
|
|
455
|
24
|
|
|
|
|
38
|
my $data = join("", map { $$_ } @{$self->{read_buf}}); |
|
42
|
|
|
|
|
833
|
|
|
24
|
|
|
|
|
56
|
|
456
|
24
|
|
|
|
|
56
|
my $count = length $data; |
457
|
24
|
|
|
|
|
38
|
my $md5_ctx = $self->{md5_ctx}; |
458
|
24
|
100
|
|
|
|
125
|
$md5_ctx->add($data) if $md5_ctx; |
459
|
|
|
|
|
|
|
|
460
|
|
|
|
|
|
|
# reset our input buffer |
461
|
24
|
|
|
|
|
61
|
$self->{read_buf} = []; |
462
|
24
|
|
|
|
|
66
|
$self->{read_ahead} = 0; |
463
|
|
|
|
|
|
|
|
464
|
|
|
|
|
|
|
# After copying out and clearing the buffer, turn reads back on again to fill up another buffer. |
465
|
24
|
100
|
66
|
|
|
180
|
$self->watch_read(1) if $self->{content_length_remain} || $self->{chunked_upload_state}; |
466
|
|
|
|
|
|
|
|
467
|
|
|
|
|
|
|
# okay, file is open, write some data |
468
|
24
|
|
|
|
|
186
|
$self->{put_in_progress} = 1; |
469
|
|
|
|
|
|
|
|
470
|
24
|
|
|
|
|
113
|
Perlbal::AIO::set_file_for_channel($self->{put_fh_filename}); |
471
|
|
|
|
|
|
|
Perlbal::AIO::aio_write($self->{put_fh}, $self->{put_pos}, $count, $data, sub { |
472
|
24
|
50
|
|
24
|
|
87
|
return if $self->{closed}; |
473
|
|
|
|
|
|
|
|
474
|
|
|
|
|
|
|
# see how many bytes written |
475
|
24
|
|
|
|
|
51
|
my $bytes = shift() + 0; |
476
|
|
|
|
|
|
|
|
477
|
24
|
|
|
|
|
46
|
$self->{put_pos} += $bytes; |
478
|
24
|
|
|
|
|
43
|
$self->{put_in_progress} = 0; |
479
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
# now recursively call ourselves? |
481
|
24
|
50
|
|
|
|
74
|
if ($self->{read_ahead}) { |
482
|
0
|
|
|
|
|
0
|
$self->put_writeout; |
483
|
0
|
|
|
|
|
0
|
return; |
484
|
|
|
|
|
|
|
} |
485
|
|
|
|
|
|
|
|
486
|
24
|
100
|
66
|
|
|
157
|
return if $self->{content_length_remain} || $self->{chunked_upload_state}; |
487
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
# we're done putting this file, so close it. |
489
|
|
|
|
|
|
|
# FIXME this should be done through AIO |
490
|
10
|
|
|
|
|
41
|
$self->put_close; |
491
|
24
|
|
|
|
|
213
|
}); |
492
|
|
|
|
|
|
|
} |
493
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
sub put_check_md5 { |
495
|
2
|
|
|
2
|
0
|
6
|
my Perlbal::ClientHTTP $self = shift; |
496
|
|
|
|
|
|
|
|
497
|
2
|
|
|
|
|
17
|
my $actual = $self->{md5_ctx}->b64digest; |
498
|
2
|
|
|
|
|
10
|
my $expect = $self->{req_headers}->header("Content-MD5"); |
499
|
2
|
|
|
|
|
15
|
$expect =~ s/=+\s*\z//; |
500
|
2
|
100
|
|
|
|
7
|
if ($actual eq $expect) { |
501
|
|
|
|
|
|
|
Perlbal::AIO::aio_rename($self->{put_fh_filename}, $self->{put_final_name}, sub { |
502
|
1
|
|
|
1
|
|
2
|
my $err = shift; |
503
|
1
|
|
|
|
|
3
|
$self->{put_fh_filename} = undef; |
504
|
1
|
|
|
|
|
3
|
$self->{put_final_name} = undef; |
505
|
1
|
50
|
|
|
|
4
|
if ($err == 0) { |
506
|
1
|
|
|
|
|
7
|
return $self->send_response(201); |
507
|
|
|
|
|
|
|
} else { |
508
|
0
|
|
|
|
|
0
|
return $self->system_error("Error renaming file", "error in rename: $!"); |
509
|
|
|
|
|
|
|
} |
510
|
1
|
|
|
|
|
12
|
}); |
511
|
|
|
|
|
|
|
} else { |
512
|
|
|
|
|
|
|
Perlbal::AIO::aio_unlink($self->{put_fh_filename}, sub { |
513
|
1
|
|
|
1
|
|
3
|
my $err = shift; |
514
|
1
|
|
|
|
|
4
|
$self->{put_fh_filename} = undef; |
515
|
1
|
|
|
|
|
3
|
$self->{put_final_name} = undef; |
516
|
1
|
50
|
|
|
|
5
|
if ($err == 0) { |
517
|
1
|
|
|
|
|
11
|
return $self->send_response(400, |
518
|
|
|
|
|
|
|
"Content-MD5 mismatch, expected: $expect actual: $actual"); |
519
|
|
|
|
|
|
|
} else { |
520
|
0
|
|
|
|
|
0
|
return $self->system_error("Error unlinking file", "error in unlink: $!"); |
521
|
|
|
|
|
|
|
} |
522
|
1
|
|
|
|
|
11
|
}); |
523
|
|
|
|
|
|
|
} |
524
|
|
|
|
|
|
|
} |
525
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
sub put_close { |
527
|
12
|
|
|
12
|
0
|
26
|
my Perlbal::ClientHTTP $self = shift; |
528
|
12
|
50
|
|
|
|
36
|
return if $self->{put_in_progress}; |
529
|
12
|
50
|
|
|
|
36
|
return unless $self->{put_fh}; |
530
|
|
|
|
|
|
|
|
531
|
12
|
50
|
|
|
|
189
|
if (CORE::close($self->{put_fh})) { |
532
|
12
|
|
|
|
|
27
|
$self->{put_fh} = undef; |
533
|
|
|
|
|
|
|
|
534
|
12
|
100
|
|
|
|
169
|
return $self->put_check_md5 if $self->{md5_ctx}; |
535
|
10
|
|
|
|
|
84
|
return $self->send_response(200); |
536
|
|
|
|
|
|
|
} else { |
537
|
0
|
|
|
|
|
|
return $self->system_error("Error saving file", "error in close: $!"); |
538
|
|
|
|
|
|
|
} |
539
|
|
|
|
|
|
|
} |
540
|
|
|
|
|
|
|
|
541
|
|
|
|
|
|
|
1; |
542
|
|
|
|
|
|
|
|
543
|
|
|
|
|
|
|
# Local Variables: |
544
|
|
|
|
|
|
|
# mode: perl |
545
|
|
|
|
|
|
|
# c-basic-indent: 4 |
546
|
|
|
|
|
|
|
# indent-tabs-mode: nil |
547
|
|
|
|
|
|
|
# End: |