Rev 3584 | Go to most recent revision | Show entire file | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed
Rev 3584 | Rev 3616 | ||
---|---|---|---|
Line 61... | Line 61... | ||
61 | 61 | ||
62 | /* uncomment this to use scheduler based calling |
62 | /* uncomment this to use scheduler based calling |
63 | #define FETCHER_CURLL_SCHEDULED 1 |
63 | #define FETCHER_CURLL_SCHEDULED 1 |
Line 64... | Line -... | ||
64 | */ |
- | |
65 | - | ||
66 | typedef int X509_STORE_CTX; |
- | |
67 | typedef int X509; |
- | |
68 | #define CURL_ERROR_SIZE 0 |
- | |
69 | typedef int CURLcode; |
- | |
70 | typedef int curl_infotype; |
- | |
71 | /** SSL certificate info */ |
- | |
72 | struct cert_info { |
- | |
73 | X509 *cert; /**< Pointer to certificate */ |
- | |
Line 74... | Line -... | ||
74 | long err; /**< OpenSSL error code */ |
- | |
75 | }; |
64 | */ |
76 | 65 | ||
- | 66 | ||
77 | /** Information for a single fetch. */ |
67 | struct fetch_curl_context { |
- | 68 | struct fetch_curl_context *r_next, *r_prev; |
|
78 | struct curl_fetch_info { |
69 | |
79 | struct fetch *fetch_handle; /**< The fetch handle we're parented by. */ |
- | |
80 | CURL * curl_handle; /**< cURL handle if being fetched, or 0. */ |
70 | struct fetch *fetchh; /**< Handle for this fetch */ |
81 | bool had_headers; /**< Headers have been processed. */ |
- | |
82 | bool abort; /**< Abort requested. */ |
- | |
83 | bool stopped; /**< Download stopped on purpose. */ |
- | |
84 | bool only_2xx; /**< Only HTTP 2xx responses acceptable. */ |
- | |
85 | bool downgrade_tls; /**< Downgrade to TLS <= 1.0 */ |
- | |
86 | nsurl *url; /**< URL of this fetch. */ |
- | |
87 | lwc_string *host; /**< The hostname of this fetch. */ |
- | |
88 | struct curl_slist *headers; /**< List of request headers. */ |
- | |
89 | char *location; /**< Response Location header, or 0. */ |
- | |
90 | unsigned long content_length; /**< Response Content-Length, or 0. */ |
- | |
91 | char *cookie_string; /**< Cookie string for this fetch */ |
- | |
92 | char *realm; /**< HTTP Auth Realm */ |
- | |
93 | char *post_urlenc; /**< Url encoded POST string, or 0. */ |
- | |
94 | long http_code; /**< HTTP result code from cURL. */ |
- | |
95 | struct curl_httppost *post_multipart; /**< Multipart post data, or 0. */ |
- | |
96 | #define MAX_CERTS 10 |
- | |
Line 97... | Line -... | ||
97 | struct cert_info cert_data[MAX_CERTS]; /**< HTTPS certificate data */ |
- | |
98 | unsigned int last_progress_update; /**< Time of last progress update */ |
71 | |
99 | }; |
72 | bool aborted; /**< Flag indicating fetch has been aborted */ |
Line 100... | Line 73... | ||
100 | 73 | bool locked; /**< Flag indicating entry is already entered */ |
|
101 | struct cache_handle { |
- | |
102 | CURL *handle; /**< The cached cURL handle */ |
74 | |
Line 103... | Line -... | ||
103 | lwc_string *host; /**< The host for which this handle is cached */ |
- | |
104 | - | ||
105 | struct cache_handle *r_prev; /**< Previous cached handle in ring. */ |
- | |
106 | struct cache_handle *r_next; /**< Next cached handle in ring. */ |
- | |
107 | }; |
75 | nsurl *url; /**< The full url the fetch refers to */ |
108 | - | ||
109 | CURLM *fetch_curl_multi; /**< Global cURL multi handle. */ |
76 | char *path; /**< The actual path to be used with open() */ |
110 | /** Curl handle with default options set; not used for transfers. */ |
- | |
111 | static CURL *fetch_blank_curl; |
- | |
112 | static struct cache_handle *curl_handle_ring = 0; /**< Ring of cached handles */ |
77 | |
113 | static int curl_fetchers_registered = 0; |
78 | time_t file_etag; /**< Request etag for file (previous st.m_time) */ |
114 | static bool curl_with_openssl; |
79 | }; |
115 | 80 | ||
116 | static char fetch_error_buffer[CURL_ERROR_SIZE]; /**< Error buffer for cURL. */ |
81 | static struct fetch_curl_context *ring = NULL; |
117 | static char fetch_proxy_userpwd[100]; /**< Proxy authentication details. */ |
82 | |
118 | 83 | ||
119 | static bool fetch_curl_initialise(lwc_string *scheme); |
84 | static bool fetch_curl_initialise(lwc_string *scheme); //here |
120 | static void fetch_curl_finalise(lwc_string *scheme); |
85 | static void fetch_curl_finalise(lwc_string *scheme); //here |
121 | static bool fetch_curl_can_fetch(const nsurl *url); |
- | |
122 | static void * fetch_curl_setup(struct fetch *parent_fetch, nsurl *url, |
- | |
123 | bool only_2xx, bool downgrade_tls, const char *post_urlenc, |
- | |
124 | const struct fetch_multipart_data *post_multipart, |
- | |
125 | const char **headers); |
- | |
126 | static bool fetch_curl_start(void *vfetch); |
- | |
127 | static bool fetch_curl_initiate_fetch(struct curl_fetch_info *fetch, |
- | |
- | 86 | static bool fetch_curl_can_fetch(const nsurl *url); //here |
|
128 | CURL *handle); |
87 | static void * fetch_curl_setup(struct fetch *parent_fetch, nsurl *url, |
129 | static CURL *fetch_curl_get_handle(lwc_string *host); |
- | |
130 | static void fetch_curl_cache_handle(CURL *handle, lwc_string *host); |
88 | bool only_2xx, bool downgrade_tls, const char *post_urlenc, |
131 | static CURLcode fetch_curl_set_options(struct curl_fetch_info *f); |
89 | const struct fetch_multipart_data *post_multipart, |
132 | static CURLcode fetch_curl_sslctxfun(CURL *curl_handle, void *_sslctx, |
- | |
133 | void *p); |
- | |
134 | static void fetch_curl_abort(void *vf); |
- | |
135 | static void fetch_curl_stop(struct curl_fetch_info *f); |
- | |
136 | static void fetch_curl_free(void *f); |
- | |
137 | static void fetch_curl_poll(lwc_string *scheme_ignored); |
- | |
138 | static void fetch_curl_done(CURL *curl_handle, CURLcode result); |
- | |
139 | static int fetch_curl_progress(void *clientp, double dltotal, double dlnow, |
- | |
140 | double ultotal, double ulnow); |
- | |
141 | static int fetch_curl_ignore_debug(CURL *handle, |
- | |
142 | curl_infotype type, |
- | |
143 | char *data, |
- | |
144 | size_t size, |
- | |
145 | void *userptr); |
- | |
146 | static size_t fetch_curl_data(char *data, size_t size, size_t nmemb, |
- | |
147 | void *_f); |
- | |
148 | static size_t fetch_curl_header(char *data, size_t size, size_t nmemb, |
- | |
149 | void *_f); |
- | |
150 | static bool fetch_curl_process_headers(struct curl_fetch_info *f); |
- | |
Line 151... | Line 90... | ||
151 | static struct curl_httppost *fetch_curl_post_convert( |
90 | const char **headers); //here |
152 | const struct fetch_multipart_data *control); |
91 | static bool fetch_curl_start(void *vfetch); //here |
153 | static int fetch_curl_verify_callback(int preverify_ok, |
92 | |
Line 171... | Line 110... | ||
171 | LOG(("curl register\n")); |
110 | LOG(("curl register\n")); |
Line 172... | Line 111... | ||
172 | 111 | ||
Line 173... | Line 112... | ||
173 | lwc_intern_string("http", SLEN("http"), &scheme); |
112 | lwc_intern_string("http", SLEN("http"), &scheme); |
174 | 113 | ||
175 | if (!fetch_add_fetcher(scheme, |
114 | if (!fetch_add_fetcher(scheme, |
176 | fetch_curl_initialise, |
115 | fetch_curl_initialise, //here |
177 | fetch_curl_can_fetch, |
116 | fetch_curl_can_fetch, //here |
178 | fetch_curl_setup, |
117 | fetch_curl_setup, |
179 | fetch_curl_start, |
118 | fetch_curl_start, |
180 | fetch_curl_abort, |
119 | fetch_curl_abort, //here |
181 | fetch_curl_free, |
120 | fetch_curl_free, //here |
182 | #ifdef FETCHER_CURLL_SCHEDULED |
121 | #ifdef FETCHER_CURLL_SCHEDULED |
183 | NULL, |
122 | NULL, |
184 | #else |
123 | #else |
185 | fetch_curl_poll, |
124 | fetch_curl_poll, //here |
186 | #endif |
125 | #endif |
187 | fetch_curl_finalise)) { |
126 | fetch_curl_finalise)) { //here |
Line 188... | Line 127... | ||
188 | LOG(("Unable to register cURL fetcher for HTTP")); |
127 | LOG(("Unable to register cURL fetcher for HTTP")); |
Line 229... | Line 168... | ||
229 | void fetch_curl_finalise(lwc_string *scheme) |
168 | void fetch_curl_finalise(lwc_string *scheme) |
230 | { |
169 | { |
231 | LOG(("curl finali\n")); |
170 | LOG(("curl finali\n")); |
232 | } |
171 | } |
Line 233... | Line 172... | ||
233 | 172 | ||
234 | bool fetch_curl_can_fetch(const nsurl *url) |
173 | static bool fetch_curl_can_fetch(const nsurl *url) |
235 | { |
174 | { |
236 | LOG(("curl can fetch\n")); |
175 | LOG(("curl can fetch\n")); |
237 | return false; |
176 | return true; //let's lie a bit |
Line 238... | Line 177... | ||
238 | } |
177 | } |
239 | 178 | ||
240 | /** |
179 | /** |
Line 257... | Line 196... | ||
257 | * |
196 | * |
258 | * Some private data can be passed as the last parameter to fetch_start, and |
197 | * Some private data can be passed as the last parameter to fetch_start, and |
259 | * callbacks will contain this. |
198 | * callbacks will contain this. |
260 | */ |
199 | */ |
Line 261... | Line 200... | ||
261 | 200 | ||
- | 201 | void * fetch_curl_setup (struct fetch *fetchh, |
|
- | 202 | nsurl *url, |
|
- | 203 | bool only_2xx, |
|
262 | void * fetch_curl_setup(struct fetch *parent_fetch, nsurl *url, |
204 | bool downgrade_tls, |
263 | bool only_2xx, bool downgrade_tls, const char *post_urlenc, |
205 | const char *post_urlenc, |
264 | const struct fetch_multipart_data *post_multipart, |
206 | const struct fetch_multipart_data *post_multipart, |
265 | const char **headers) |
207 | const char **headers) |
- | 208 | { |
|
266 | { |
209 | |
Line -... | Line 210... | ||
- | 210 | LOG(("curl setup\n")); |
|
267 | LOG(("curl setup\n")); |
211 | |
268 | - | ||
Line -... | Line 212... | ||
- | 212 | struct fetch_curl_context *ctx; |
|
- | 213 | int i; |
|
- | 214 | ||
Line 269... | Line -... | ||
269 | return; |
- | |
270 | } |
215 | ctx = calloc(1, sizeof(*ctx)); |
271 | - | ||
272 | 216 | if (ctx == NULL) |
|
273 | /** |
- | |
274 | * Dispatch a single job |
217 | return NULL; |
275 | */ |
218 | |
276 | bool fetch_curl_start(void *vfetch) |
219 | ctx->path = url_to_path(nsurl_access(url)); |
Line -... | Line 220... | ||
- | 220 | if (ctx->path == NULL) { |
|
Line 277... | Line -... | ||
277 | { |
- | |
278 | LOG(("curl start\n")); |
- | |
279 | return 0; |
- | |
280 | } |
- | |
281 | - | ||
282 | - | ||
283 | /** |
- | |
284 | * Initiate a fetch from the queue. |
- | |
285 | * |
- | |
286 | * Called with a fetch structure and a CURL handle to be used to fetch the |
- | |
287 | * content. |
- | |
288 | * |
- | |
289 | * This will return whether or not the fetch was successfully initiated. |
- | |
290 | */ |
- | |
Line -... | Line 221... | ||
- | 221 | free(ctx); |
|
Line 291... | Line -... | ||
291 | - | ||
292 | bool fetch_curl_initiate_fetch(struct curl_fetch_info *fetch, CURL *handle) |
222 | return NULL; |
293 | { |
- | |
Line 294... | Line -... | ||
294 | LOG(("curl initi fetch\n")); |
- | |
295 | return 0; |
- | |
296 | } |
- | |
297 | 223 | } |
|
298 | 224 | ||
Line 299... | Line 225... | ||
299 | /** |
225 | ctx->url = nsurl_ref(url); |
300 | * Find a CURL handle to use to dispatch a job |
226 | |
301 | */ |
227 | |
302 | - | ||
303 | CURL *fetch_curl_get_handle(lwc_string *host) |
228 | ctx->fetchh = fetchh; |
304 | { |
229 | |
305 | LOG(("curl get handle\n")); |
230 | RING_INSERT(ring, ctx); |
- | 231 | ||
306 | return 0; |
232 | return ctx; |
Line 307... | Line -... | ||
307 | } |
- | |
308 | - | ||
309 | - | ||
Line 310... | Line -... | ||
310 | /** |
- | |
Line 311... | Line -... | ||
311 | * Cache a CURL handle for the provided host (if wanted) |
- | |
312 | */ |
- | |
313 | - | ||
314 | void fetch_curl_cache_handle(CURL *handle, lwc_string *host) |
- | |
315 | { |
- | |
316 | LOG(("curl cache handle\n")); |
- | |
Line 317... | Line 233... | ||
317 | } |
233 | } |
318 | 234 | ||
319 | 235 | ||
Line 320... | Line -... | ||
320 | /** |
- | |
321 | * Set options specific for a fetch. |
236 | /** |
322 | */ |
237 | * Dispatch a single job |
323 | 238 | */ |
|
Line -... | Line 239... | ||
- | 239 | bool fetch_curl_start(void *vfetch) |
|
- | 240 | { |
|
- | 241 | LOG(("curl start\n")); |
|
- | 242 | return true; |
|
324 | typedef int CURLcode; |
243 | } |
325 | 244 | ||
Line 326... | Line 245... | ||
326 | CURLcode |
245 | |
327 | fetch_curl_set_options(struct curl_fetch_info *f) |
246 | |
328 | { |
247 | |
Line 329... | Line 248... | ||
329 | LOG(("curl set options\n")); |
248 | |
330 | return -1; |
249 | |
- | 250 | /** |
|
331 | } |
251 | * Abort a fetch. |
- | 252 | */ |
|
- | 253 | ||
- | 254 | void fetch_curl_abort(void *ctx) |
|
332 | 255 | { |
|
Line -... | Line 256... | ||
- | 256 | struct fetch_curl_context *c = ctx; |
|
- | 257 | ||
- | 258 | /* To avoid the poll loop having to deal with the fetch context |
|
- | 259 | * disappearing from under it, we simply flag the abort here. |
|
- | 260 | * The poll loop itself will perform the appropriate cleanup. |
|
- | 261 | */ |
|
Line 333... | Line -... | ||
333 | - | ||
334 | /** |
262 | c->aborted = true; |
335 | * cURL SSL setup callback |
263 | } |
336 | */ |
- | |
337 | - | ||
Line 338... | Line 264... | ||
338 | CURLcode |
264 | |
- | 265 | ||
339 | fetch_curl_sslctxfun(CURL *curl_handle, void *_sslctx, void *parm) |
266 | /** |
340 | { |
267 | * Free a fetch structure and associated resources. |
- | 268 | */ |
|
341 | LOG(("curlcodessl\n")); |
269 | |
Line -... | Line 270... | ||
- | 270 | void fetch_curl_free(void *ctx) |
|
Line 342... | Line -... | ||
342 | - | ||
343 | return -1; |
271 | { |
344 | } |
- | |
Line 345... | Line -... | ||
345 | - | ||
346 | - | ||
347 | /** |
272 | struct fetch_curl_context *c = ctx; |
348 | * Abort a fetch. |
- | |
Line -... | Line 273... | ||
- | 273 | nsurl_unref(c->url); |
|
- | 274 | free(c->path); |
|
- | 275 | RING_REMOVE(ring, c); |
|
- | 276 | free(ctx); |
|
Line 349... | Line -... | ||
349 | */ |
- | |
350 | 277 | } |
|
351 | void fetch_curl_abort(void *vf) |
278 | |
352 | { |
- | |
353 | LOG(("curl abort\n")); |
- | |
Line 354... | Line 279... | ||
354 | } |
279 | static inline bool fetch_curl_send_callback(const fetch_msg *msg, |
355 | 280 | struct fetch_curl_context *ctx) |
|
- | 281 | { |
|
- | 282 | ctx->locked = true; |
|
356 | 283 | fetch_send_callback(msg, ctx->fetchh); |
|
357 | /** |
284 | ctx->locked = false; |
Line -... | Line 285... | ||
- | 285 | ||
- | 286 | return ctx->aborted; |
|
Line 358... | Line 287... | ||
358 | * Clean up the provided fetch object and free it. |
287 | } |
359 | * |
288 | |
360 | * Will prod the queue afterwards to allow pending requests to be initiated. |
- | |
361 | */ |
289 | static bool fetch_curl_send_header(struct fetch_curl_context *ctx, |
362 | - | ||
Line 363... | Line 290... | ||
363 | void fetch_curl_stop(struct curl_fetch_info *f) |
290 | const char *fmt, ...) |
364 | { |
- | |
365 | LOG(("curl stop\n")); |
291 | { |
366 | } |
- | |
Line -... | Line 292... | ||
- | 292 | fetch_msg msg; |
|
- | 293 | char header[64]; |
|
- | 294 | va_list ap; |
|
- | 295 | ||
Line 367... | Line 296... | ||
367 | 296 | va_start(ap, fmt); |
|
- | 297 | ||
- | 298 | vsnprintf(header, sizeof header, fmt, ap); |
|
368 | 299 | ||
369 | /** |
- | |
- | 300 | va_end(ap); |
|
Line 370... | Line 301... | ||
370 | * Free a fetch structure and associated resources. |
301 | |
371 | */ |
302 | msg.type = FETCH_HEADER; |
372 | 303 | msg.data.header_or_data.buf = (const uint8_t *) header; |
|
373 | void fetch_curl_free(void *vf) |
304 | msg.data.header_or_data.len = strlen(header); |
374 | { |
305 | fetch_curl_send_callback(&msg, ctx); |
375 | LOG(("curl free\n")); |
306 | |
Line -... | Line 307... | ||
- | 307 | return ctx->aborted; |
|
Line -... | Line 308... | ||
- | 308 | } |
|
- | 309 | ||
- | 310 | static void fetch_curl_process_error(struct fetch_curl_context *ctx, int code) |
|
- | 311 | { |
|
376 | } |
312 | fetch_msg msg; |
377 | 313 | char buffer[1024]; |
|
- | 314 | const char *title; |
|
378 | 315 | char key[8]; |
|
- | 316 | ||
- | 317 | /* content is going to return error code */ |
|
379 | /** |
318 | fetch_set_http_code(ctx->fetchh, code); |
380 | * Do some work on current fetches. |
319 | |
Line 381... | Line -... | ||
381 | * |
- | |
382 | * Must be called regularly to make progress on fetches. |
320 | /* content type */ |
383 | */ |
- | |
384 | - | ||
385 | void fetch_curl_poll(lwc_string *scheme_ignored) |
321 | if (fetch_curl_send_header(ctx, "Content-Type: text/html")) |
386 | { |
- | |
387 | LOG(("curl poll\n")); |
322 | goto fetch_file_process_error_aborted; |
388 | } |
- | |
389 | - | ||
Line 390... | Line -... | ||
390 | - | ||
391 | /** |
323 | |
392 | * Handle a completed fetch (CURLMSG_DONE from curl_multi_info_read()). |
- | |
Line -... | Line 324... | ||
- | 324 | snprintf(key, sizeof key, "HTTP%03d", code); |
|
393 | * |
325 | title = messages_get(key); |
394 | * \param curl_handle curl easy handle of fetch |
326 | |
395 | */ |
- | |
396 | 327 | snprintf(buffer, sizeof buffer, " |
|
397 | void fetch_curl_done(CURL *curl_handle, CURLcode result) |
- | |
398 | { |
- | |
Line 399... | Line -... | ||
399 | LOG(("curl done\n")); |
- | |
400 | } |
- | |
401 | - | ||
402 | - | ||
403 | /** |
- | |
Line -... | Line 328... | ||
- | 328 | " |
|
- | 329 | " |
|
404 | * Callback function for fetch progress. |
330 | title, title, code, nsurl_access(ctx->url)); |
- | 331 | ||
- | 332 | msg.type = FETCH_DATA; |
|
- | 333 | msg.data.header_or_data.buf = (const uint8_t *) buffer; |
|
405 | */ |
334 | msg.data.header_or_data.len = strlen(buffer); |
Line 406... | Line -... | ||
406 | - | ||
407 | int fetch_curl_progress(void *clientp, double dltotal, double dlnow, |
- | |
408 | double ultotal, double ulnow) |
- | |
409 | { |
335 | if (fetch_curl_send_callback(&msg, ctx)) |
Line -... | Line 336... | ||
- | 336 | goto fetch_file_process_error_aborted; |
|
410 | LOG(("curl progress\n")); |
337 | |
411 | return 0; |
338 | msg.type = FETCH_FINISHED; |
412 | } |
339 | fetch_curl_send_callback(&msg, ctx); |
413 | 340 | ||
414 | 341 | fetch_file_process_error_aborted: |
|
Line 415... | Line 342... | ||
415 | 342 | return; |
|
416 | /** |
343 | } |
417 | * Ignore everything given to it. |
344 | |
418 | * |
- | |
419 | * Used to ignore cURL debug. |
- | |
Line -... | Line 345... | ||
- | 345 | ||
Line 420... | Line -... | ||
420 | */ |
- | |
421 | - | ||
422 | int fetch_curl_ignore_debug(CURL *handle, |
- | |
423 | curl_infotype type, |
- | |
424 | char *data, |
- | |
425 | size_t size, |
- | |
426 | void *userptr) |
- | |
427 | { |
346 | static void fetch_curl_process(struct fetch_curl_context *ctx) { |
Line -... | Line 347... | ||
- | 347 | ||
- | 348 | fetch_msg msg; |
|
- | 349 | const char * buf = " |
|
- | 350 | // fetch_curl_process_error(ctx, 501); |
|
- | 351 | // return; |
|
- | 352 | ||
- | 353 | /* fetch is going to be successful */ |
|
- | 354 | fetch_set_http_code(ctx->fetchh, 200); |
|
- | 355 | ||
428 | LOG(("curl igdebug\n")); |
356 | /* Any callback can result in the fetch being aborted. |
- | 357 | * Therefore, we _must_ check for this after _every_ call to |
|
429 | return 0; |
358 | * fetch_file_send_callback(). |
430 | } |
359 | */ |
Line -... | Line 360... | ||
- | 360 | ||
- | 361 | /* content type */ |
|
- | 362 | if (fetch_curl_send_header(ctx, "Content-Type: text/html")) |
|
- | 363 | goto fetch_file_process_aborted; |
|
- | 364 | ||
Line 431... | Line -... | ||
431 | - | ||
432 | 365 | ||
433 | /** |
366 | /* main data loop */ |
434 | * Callback function for cURL. |
367 | |
- | 368 | msg.type = FETCH_DATA; |
|
Line 435... | Line 369... | ||
435 | */ |
369 | msg.data.header_or_data.buf = (const uint8_t *) buf; |
- | 370 | msg.data.header_or_data.len = strlen(buf); |
|
436 | 371 | fetch_curl_send_callback(&msg, ctx); |
|
- | 372 | ||
- | 373 | ||
- | 374 | ||
- | 375 | if (ctx->aborted == false) { |
|
Line 437... | Line -... | ||
437 | size_t fetch_curl_data(char *data, size_t size, size_t nmemb, |
- | |
438 | void *_f) |
- | |
439 | { |
376 | msg.type = FETCH_FINISHED; |
Line 440... | Line -... | ||
440 | LOG(("curl callback\n")); |
- | |
441 | return 0; |
- | |
442 | } |
- | |
443 | - | ||
Line 444... | Line -... | ||
444 | - | ||
445 | /** |
- | |
Line 446... | Line -... | ||
446 | * Callback function for headers. |
- | |
447 | * |
- | |
448 | * See RFC 2616 4.2. |
- |