]>
Commit | Line | Data |
---|---|---|
82cc1f9a MS |
1 | /* |
2 | * "$Id$" | |
3 | * | |
4 | * TLS support code for the CUPS scheduler on OS X. | |
5 | * | |
6 | * Copyright 2007-2012 by Apple Inc. | |
7 | * Copyright 1997-2007 by Easy Software Products, all rights reserved. | |
8 | * | |
9 | * These coded instructions, statements, and computer programs are the | |
10 | * property of Apple Inc. and are protected by Federal copyright | |
11 | * law. Distribution and use rights are outlined in the file "LICENSE.txt" | |
12 | * which should have been included with this file. If this file is | |
13 | * file is missing or damaged, see the license at "http://www.cups.org/". | |
14 | * | |
15 | * Contents: | |
16 | * | |
17 | * cupsdEndTLS() - Shutdown a secure session with the client. | |
18 | * cupsdStartTLS() - Start a secure session with the client. | |
19 | * copy_cdsa_certificate() - Copy a SSL/TLS certificate from the System | |
20 | * keychain. | |
21 | * make_certificate() - Make a self-signed SSL/TLS certificate. | |
22 | */ | |
23 | ||
24 | ||
25 | /* | |
26 | * Local functions... | |
27 | */ | |
28 | ||
29 | static CFArrayRef copy_cdsa_certificate(cupsd_client_t *con); | |
30 | static int make_certificate(cupsd_client_t *con); | |
31 | ||
32 | ||
33 | /* | |
34 | * 'cupsdEndTLS()' - Shutdown a secure session with the client. | |
35 | */ | |
36 | ||
37 | int /* O - 1 on success, 0 on error */ | |
38 | cupsdEndTLS(cupsd_client_t *con) /* I - Client connection */ | |
39 | { | |
40 | while (SSLClose(con->http.tls) == errSSLWouldBlock) | |
41 | usleep(1000); | |
42 | ||
43 | SSLDisposeContext(con->http.tls); | |
44 | con->http.tls = NULL; | |
45 | ||
46 | if (con->http.tls_credentials) | |
47 | CFRelease(con->http.tls_credentials); | |
48 | ||
49 | return (1); | |
50 | } | |
51 | ||
52 | ||
53 | /* | |
54 | * 'cupsdStartTLS()' - Start a secure session with the client. | |
55 | */ | |
56 | ||
57 | int /* O - 1 on success, 0 on error */ | |
58 | cupsdStartTLS(cupsd_client_t *con) /* I - Client connection */ | |
59 | { | |
60 | OSStatus error = 0; /* Error code */ | |
61 | CFArrayRef peerCerts; /* Peer certificates */ | |
62 | ||
63 | ||
64 | cupsdLogMessage(CUPSD_LOG_DEBUG, "[Client %d] Encrypting connection.", | |
65 | con->http.fd); | |
66 | ||
67 | con->http.tls_credentials = copy_cdsa_certificate(con); | |
68 | ||
69 | if (!con->http.tls_credentials) | |
70 | { | |
71 | /* | |
72 | * No keychain (yet), make a self-signed certificate... | |
73 | */ | |
74 | ||
75 | if (make_certificate(con)) | |
76 | con->http.tls_credentials = copy_cdsa_certificate(con); | |
77 | } | |
78 | ||
79 | if (!con->http.tls_credentials) | |
80 | { | |
81 | cupsdLogMessage(CUPSD_LOG_ERROR, | |
82 | "Could not find signing key in keychain \"%s\"", | |
83 | ServerCertificate); | |
84 | error = errSSLBadConfiguration; | |
85 | } | |
86 | ||
87 | if (!error) | |
88 | error = SSLNewContext(true, &con->http.tls); | |
89 | ||
90 | if (!error) | |
91 | error = SSLSetIOFuncs(con->http.tls, _httpReadCDSA, _httpWriteCDSA); | |
92 | ||
93 | if (!error) | |
94 | error = SSLSetConnection(con->http.tls, HTTP(con)); | |
95 | ||
96 | if (!error) | |
97 | error = SSLSetAllowsExpiredCerts(con->http.tls, true); | |
98 | ||
99 | if (!error) | |
100 | error = SSLSetAllowsAnyRoot(con->http.tls, true); | |
101 | ||
102 | if (!error) | |
103 | error = SSLSetCertificate(con->http.tls, con->http.tls_credentials); | |
104 | ||
105 | if (!error) | |
106 | { | |
107 | /* | |
108 | * Perform SSL/TLS handshake | |
109 | */ | |
110 | ||
111 | while ((error = SSLHandshake(con->http.tls)) == errSSLWouldBlock) | |
112 | usleep(1000); | |
113 | } | |
114 | ||
115 | if (error) | |
116 | { | |
117 | cupsdLogMessage(CUPSD_LOG_ERROR, | |
118 | "Unable to encrypt connection from %s - %s (%d)", | |
119 | con->http.hostname, cssmErrorString(error), (int)error); | |
120 | ||
121 | con->http.error = error; | |
122 | con->http.status = HTTP_ERROR; | |
123 | ||
124 | if (con->http.tls) | |
125 | { | |
126 | SSLDisposeContext(con->http.tls); | |
127 | con->http.tls = NULL; | |
128 | } | |
129 | ||
130 | if (con->http.tls_credentials) | |
131 | { | |
132 | CFRelease(con->http.tls_credentials); | |
133 | con->http.tls_credentials = NULL; | |
134 | } | |
135 | ||
136 | return (0); | |
137 | } | |
138 | ||
139 | cupsdLogMessage(CUPSD_LOG_DEBUG, "Connection from %s now encrypted.", | |
140 | con->http.hostname); | |
141 | ||
142 | if (!SSLCopyPeerCertificates(con->http.tls, &peerCerts) && peerCerts) | |
143 | { | |
144 | cupsdLogMessage(CUPSD_LOG_DEBUG, "Received %d peer certificates!", | |
145 | (int)CFArrayGetCount(peerCerts)); | |
146 | CFRelease(peerCerts); | |
147 | } | |
148 | else | |
149 | cupsdLogMessage(CUPSD_LOG_DEBUG, "Received NO peer certificates!"); | |
150 | ||
151 | return (1); | |
152 | } | |
153 | ||
154 | ||
155 | /* | |
156 | * 'copy_cdsa_certificate()' - Copy a SSL/TLS certificate from the System | |
157 | * keychain. | |
158 | */ | |
159 | ||
160 | static CFArrayRef /* O - Array of certificates */ | |
161 | copy_cdsa_certificate( | |
162 | cupsd_client_t *con) /* I - Client connection */ | |
163 | { | |
164 | OSStatus err; /* Error info */ | |
165 | SecKeychainRef keychain = NULL;/* Keychain reference */ | |
166 | SecIdentitySearchRef search = NULL; /* Search reference */ | |
167 | SecIdentityRef identity = NULL;/* Identity */ | |
168 | CFArrayRef certificates = NULL; | |
169 | /* Certificate array */ | |
170 | # if HAVE_SECPOLICYCREATESSL | |
171 | SecPolicyRef policy = NULL; /* Policy ref */ | |
172 | CFStringRef servername = NULL; | |
173 | /* Server name */ | |
174 | CFMutableDictionaryRef query = NULL; /* Query qualifiers */ | |
175 | CFArrayRef list = NULL; /* Keychain list */ | |
a29fd7dd | 176 | # if defined(HAVE_DNSSD) || defined(HAVE_AVAHI) |
82cc1f9a | 177 | char localname[1024];/* Local hostname */ |
a29fd7dd | 178 | # endif /* HAVE_DNSSD || HAVE_AVAHI */ |
82cc1f9a MS |
179 | # elif defined(HAVE_SECIDENTITYSEARCHCREATEWITHPOLICY) |
180 | SecPolicyRef policy = NULL; /* Policy ref */ | |
181 | SecPolicySearchRef policy_search = NULL; | |
182 | /* Policy search ref */ | |
183 | CSSM_DATA options; /* Policy options */ | |
184 | CSSM_APPLE_TP_SSL_OPTIONS | |
185 | ssl_options; /* SSL Option for hostname */ | |
186 | char localname[1024];/* Local hostname */ | |
187 | # endif /* HAVE_SECPOLICYCREATESSL */ | |
188 | ||
189 | ||
190 | cupsdLogMessage(CUPSD_LOG_DEBUG, | |
191 | "copy_cdsa_certificate: Looking for certs for \"%s\"...", | |
192 | con->servername); | |
193 | ||
194 | if ((err = SecKeychainOpen(ServerCertificate, &keychain))) | |
195 | { | |
196 | cupsdLogMessage(CUPSD_LOG_ERROR, "Cannot open keychain \"%s\" - %s (%d)", | |
197 | ServerCertificate, cssmErrorString(err), (int)err); | |
198 | goto cleanup; | |
199 | } | |
200 | ||
201 | # if HAVE_SECPOLICYCREATESSL | |
202 | servername = CFStringCreateWithCString(kCFAllocatorDefault, con->servername, | |
203 | kCFStringEncodingUTF8); | |
204 | ||
205 | policy = SecPolicyCreateSSL(1, servername); | |
206 | ||
207 | if (servername) | |
208 | CFRelease(servername); | |
209 | ||
210 | if (!policy) | |
211 | { | |
212 | cupsdLogMessage(CUPSD_LOG_ERROR, "Cannot create ssl policy reference"); | |
213 | goto cleanup; | |
214 | } | |
215 | ||
216 | if (!(query = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, | |
217 | &kCFTypeDictionaryKeyCallBacks, | |
218 | &kCFTypeDictionaryValueCallBacks))) | |
219 | { | |
220 | cupsdLogMessage(CUPSD_LOG_ERROR, "Cannot create query dictionary"); | |
221 | goto cleanup; | |
222 | } | |
223 | ||
224 | list = CFArrayCreate(kCFAllocatorDefault, (const void **)&keychain, 1, | |
225 | &kCFTypeArrayCallBacks); | |
226 | ||
227 | CFDictionaryAddValue(query, kSecClass, kSecClassIdentity); | |
228 | CFDictionaryAddValue(query, kSecMatchPolicy, policy); | |
229 | CFDictionaryAddValue(query, kSecReturnRef, kCFBooleanTrue); | |
230 | CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitOne); | |
231 | CFDictionaryAddValue(query, kSecMatchSearchList, list); | |
232 | ||
233 | CFRelease(list); | |
234 | ||
235 | err = SecItemCopyMatching(query, (CFTypeRef *)&identity); | |
236 | ||
a29fd7dd | 237 | # if defined(HAVE_DNSSD) || defined(HAVE_AVAHI) |
82cc1f9a MS |
238 | if (err && DNSSDHostName) |
239 | { | |
240 | /* | |
241 | * Search for the connection server name failed; try the DNS-SD .local | |
242 | * hostname instead... | |
243 | */ | |
244 | ||
245 | snprintf(localname, sizeof(localname), "%s.local", DNSSDHostName); | |
246 | ||
247 | cupsdLogMessage(CUPSD_LOG_DEBUG, | |
248 | "copy_cdsa_certificate: Looking for certs for \"%s\"...", | |
249 | localname); | |
250 | ||
251 | servername = CFStringCreateWithCString(kCFAllocatorDefault, localname, | |
252 | kCFStringEncodingUTF8); | |
253 | ||
254 | CFRelease(policy); | |
255 | ||
256 | policy = SecPolicyCreateSSL(1, servername); | |
257 | ||
258 | if (servername) | |
259 | CFRelease(servername); | |
260 | ||
261 | if (!policy) | |
262 | { | |
263 | cupsdLogMessage(CUPSD_LOG_ERROR, "Cannot create ssl policy reference"); | |
264 | goto cleanup; | |
265 | } | |
266 | ||
267 | CFDictionarySetValue(query, kSecMatchPolicy, policy); | |
268 | ||
269 | err = SecItemCopyMatching(query, (CFTypeRef *)&identity); | |
270 | } | |
a29fd7dd | 271 | # endif /* HAVE_DNSSD || HAVE_AVAHI */ |
82cc1f9a MS |
272 | |
273 | if (err) | |
274 | { | |
275 | cupsdLogMessage(CUPSD_LOG_DEBUG, | |
276 | "Cannot find signing key in keychain \"%s\": %s (%d)", | |
277 | ServerCertificate, cssmErrorString(err), (int)err); | |
278 | goto cleanup; | |
279 | } | |
280 | ||
281 | # elif defined(HAVE_SECIDENTITYSEARCHCREATEWITHPOLICY) | |
282 | /* | |
283 | * Use a policy to search for valid certificates whose common name matches the | |
284 | * servername... | |
285 | */ | |
286 | ||
287 | if (SecPolicySearchCreate(CSSM_CERT_X_509v3, &CSSMOID_APPLE_TP_SSL, | |
288 | NULL, &policy_search)) | |
289 | { | |
290 | cupsdLogMessage(CUPSD_LOG_ERROR, "Cannot create a policy search reference"); | |
291 | goto cleanup; | |
292 | } | |
293 | ||
294 | if (SecPolicySearchCopyNext(policy_search, &policy)) | |
295 | { | |
296 | cupsdLogMessage(CUPSD_LOG_ERROR, | |
297 | "Cannot find a policy to use for searching"); | |
298 | goto cleanup; | |
299 | } | |
300 | ||
301 | memset(&ssl_options, 0, sizeof(ssl_options)); | |
302 | ssl_options.Version = CSSM_APPLE_TP_SSL_OPTS_VERSION; | |
303 | ssl_options.ServerName = con->servername; | |
304 | ssl_options.ServerNameLen = strlen(con->servername); | |
305 | ||
306 | options.Data = (uint8 *)&ssl_options; | |
307 | options.Length = sizeof(ssl_options); | |
308 | ||
309 | if (SecPolicySetValue(policy, &options)) | |
310 | { | |
311 | cupsdLogMessage(CUPSD_LOG_ERROR, | |
312 | "Cannot set policy value to use for searching"); | |
313 | goto cleanup; | |
314 | } | |
315 | ||
316 | if ((err = SecIdentitySearchCreateWithPolicy(policy, NULL, CSSM_KEYUSE_SIGN, | |
317 | keychain, FALSE, &search))) | |
318 | { | |
319 | cupsdLogMessage(CUPSD_LOG_ERROR, | |
320 | "Cannot create identity search reference: %s (%d)", | |
321 | cssmErrorString(err), (int)err); | |
322 | goto cleanup; | |
323 | } | |
324 | ||
325 | err = SecIdentitySearchCopyNext(search, &identity); | |
326 | ||
a29fd7dd | 327 | # if defined(HAVE_DNSSD) || defined(HAVE_AVAHI) |
82cc1f9a MS |
328 | if (err && DNSSDHostName) |
329 | { | |
330 | /* | |
331 | * Search for the connection server name failed; try the DNS-SD .local | |
332 | * hostname instead... | |
333 | */ | |
334 | ||
335 | snprintf(localname, sizeof(localname), "%s.local", DNSSDHostName); | |
336 | ||
337 | ssl_options.ServerName = localname; | |
338 | ssl_options.ServerNameLen = strlen(localname); | |
339 | ||
340 | cupsdLogMessage(CUPSD_LOG_DEBUG, | |
341 | "copy_cdsa_certificate: Looking for certs for \"%s\"...", | |
342 | localname); | |
343 | ||
344 | if (SecPolicySetValue(policy, &options)) | |
345 | { | |
346 | cupsdLogMessage(CUPSD_LOG_ERROR, | |
347 | "Cannot set policy value to use for searching"); | |
348 | goto cleanup; | |
349 | } | |
350 | ||
351 | CFRelease(search); | |
352 | search = NULL; | |
353 | if ((err = SecIdentitySearchCreateWithPolicy(policy, NULL, CSSM_KEYUSE_SIGN, | |
354 | keychain, FALSE, &search))) | |
355 | { | |
356 | cupsdLogMessage(CUPSD_LOG_ERROR, | |
357 | "Cannot create identity search reference: %s (%d)", | |
358 | cssmErrorString(err), (int)err); | |
359 | goto cleanup; | |
360 | } | |
361 | ||
362 | err = SecIdentitySearchCopyNext(search, &identity); | |
363 | ||
364 | } | |
a29fd7dd | 365 | # endif /* HAVE_DNSSD || HAVE_AVAHI */ |
82cc1f9a MS |
366 | |
367 | if (err) | |
368 | { | |
369 | cupsdLogMessage(CUPSD_LOG_DEBUG, | |
370 | "Cannot find signing key in keychain \"%s\": %s (%d)", | |
371 | ServerCertificate, cssmErrorString(err), (int)err); | |
372 | goto cleanup; | |
373 | } | |
374 | ||
375 | # else | |
376 | /* | |
377 | * Assume there is exactly one SecIdentity in the keychain... | |
378 | */ | |
379 | ||
380 | if ((err = SecIdentitySearchCreate(keychain, CSSM_KEYUSE_SIGN, &search))) | |
381 | { | |
382 | cupsdLogMessage(CUPSD_LOG_DEBUG, | |
383 | "Cannot create identity search reference (%d)", (int)err); | |
384 | goto cleanup; | |
385 | } | |
386 | ||
387 | if ((err = SecIdentitySearchCopyNext(search, &identity))) | |
388 | { | |
389 | cupsdLogMessage(CUPSD_LOG_DEBUG, | |
390 | "Cannot find signing key in keychain \"%s\": %s (%d)", | |
391 | ServerCertificate, cssmErrorString(err), (int)err); | |
392 | goto cleanup; | |
393 | } | |
394 | # endif /* HAVE_SECPOLICYCREATESSL */ | |
395 | ||
396 | if (CFGetTypeID(identity) != SecIdentityGetTypeID()) | |
397 | { | |
398 | cupsdLogMessage(CUPSD_LOG_ERROR, "SecIdentity CFTypeID failure!"); | |
399 | goto cleanup; | |
400 | } | |
401 | ||
402 | if ((certificates = CFArrayCreate(NULL, (const void **)&identity, | |
403 | 1, &kCFTypeArrayCallBacks)) == NULL) | |
404 | { | |
405 | cupsdLogMessage(CUPSD_LOG_ERROR, "Cannot create certificate array"); | |
406 | goto cleanup; | |
407 | } | |
408 | ||
409 | cleanup : | |
410 | ||
411 | if (keychain) | |
412 | CFRelease(keychain); | |
413 | if (search) | |
414 | CFRelease(search); | |
415 | if (identity) | |
416 | CFRelease(identity); | |
417 | ||
418 | # if HAVE_SECPOLICYCREATESSL | |
419 | if (policy) | |
420 | CFRelease(policy); | |
421 | if (query) | |
422 | CFRelease(query); | |
423 | # elif defined(HAVE_SECIDENTITYSEARCHCREATEWITHPOLICY) | |
424 | if (policy) | |
425 | CFRelease(policy); | |
426 | if (policy_search) | |
427 | CFRelease(policy_search); | |
428 | # endif /* HAVE_SECPOLICYCREATESSL */ | |
429 | ||
430 | return (certificates); | |
431 | } | |
432 | ||
433 | ||
434 | /* | |
435 | * 'make_certificate()' - Make a self-signed SSL/TLS certificate. | |
436 | */ | |
437 | ||
438 | static int /* O - 1 on success, 0 on failure */ | |
439 | make_certificate(cupsd_client_t *con) /* I - Client connection */ | |
440 | { | |
441 | int pid, /* Process ID of command */ | |
442 | status; /* Status of command */ | |
443 | char command[1024], /* Command */ | |
444 | *argv[4], /* Command-line arguments */ | |
445 | *envp[MAX_ENV + 1], /* Environment variables */ | |
446 | keychain[1024], /* Keychain argument */ | |
447 | infofile[1024], /* Type-in information for cert */ | |
a29fd7dd | 448 | # if defined(HAVE_DNSSD) || defined(HAVE_AVAHI) |
82cc1f9a | 449 | localname[1024], /* Local hostname */ |
a29fd7dd | 450 | # endif /* HAVE_DNSSD || HAVE_AVAHI */ |
82cc1f9a MS |
451 | *servername; /* Name of server in cert */ |
452 | cups_file_t *fp; /* Seed/info file */ | |
453 | int infofd; /* Info file descriptor */ | |
454 | ||
455 | ||
a29fd7dd | 456 | # if defined(HAVE_DNSSD) || defined(HAVE_AVAHI) |
82cc1f9a MS |
457 | if (con->servername && isdigit(con->servername[0] & 255) && DNSSDHostName) |
458 | { | |
459 | snprintf(localname, sizeof(localname), "%s.local", DNSSDHostName); | |
460 | servername = localname; | |
461 | } | |
462 | else | |
a29fd7dd | 463 | # endif /* HAVE_DNSSD || HAVE_AVAHI */ |
82cc1f9a MS |
464 | servername = con->servername; |
465 | ||
466 | /* | |
467 | * Run the "certtool" command to generate a self-signed certificate... | |
468 | */ | |
469 | ||
470 | if (!cupsFileFind("certtool", getenv("PATH"), 1, command, sizeof(command))) | |
471 | { | |
472 | cupsdLogMessage(CUPSD_LOG_ERROR, | |
473 | "No SSL certificate and certtool command not found!"); | |
474 | return (0); | |
475 | } | |
476 | ||
477 | /* | |
478 | * Create a file with the certificate information fields... | |
479 | * | |
480 | * Note: This assumes that the default questions are asked by the certtool | |
481 | * command... | |
482 | */ | |
483 | ||
484 | if ((fp = cupsTempFile2(infofile, sizeof(infofile))) == NULL) | |
485 | { | |
486 | cupsdLogMessage(CUPSD_LOG_ERROR, | |
487 | "Unable to create certificate information file %s - %s", | |
488 | infofile, strerror(errno)); | |
489 | return (0); | |
490 | } | |
491 | ||
492 | cupsFilePrintf(fp, | |
493 | "%s\n" /* Enter key and certificate label */ | |
494 | "r\n" /* Generate RSA key pair */ | |
495 | "2048\n" /* Key size in bits */ | |
496 | "y\n" /* OK (y = yes) */ | |
497 | "b\n" /* Usage (b=signing/encryption) */ | |
498 | "s\n" /* Sign with SHA1 */ | |
499 | "y\n" /* OK (y = yes) */ | |
500 | "%s\n" /* Common name */ | |
501 | "\n" /* Country (default) */ | |
502 | "\n" /* Organization (default) */ | |
503 | "\n" /* Organizational unit (default) */ | |
504 | "\n" /* State/Province (default) */ | |
505 | "%s\n" /* Email address */ | |
506 | "y\n", /* OK (y = yes) */ | |
507 | servername, servername, ServerAdmin); | |
508 | cupsFileClose(fp); | |
509 | ||
510 | cupsdLogMessage(CUPSD_LOG_INFO, | |
511 | "Generating SSL server key and certificate..."); | |
512 | ||
513 | snprintf(keychain, sizeof(keychain), "k=%s", ServerCertificate); | |
514 | ||
515 | argv[0] = "certtool"; | |
516 | argv[1] = "c"; | |
517 | argv[2] = keychain; | |
518 | argv[3] = NULL; | |
519 | ||
520 | cupsdLoadEnv(envp, MAX_ENV); | |
521 | ||
522 | infofd = open(infofile, O_RDONLY); | |
523 | ||
524 | if (!cupsdStartProcess(command, argv, envp, infofd, -1, -1, -1, -1, 1, NULL, | |
525 | NULL, &pid)) | |
526 | { | |
527 | close(infofd); | |
528 | unlink(infofile); | |
529 | return (0); | |
530 | } | |
531 | ||
532 | close(infofd); | |
533 | unlink(infofile); | |
534 | ||
535 | while (waitpid(pid, &status, 0) < 0) | |
536 | if (errno != EINTR) | |
537 | { | |
538 | status = 1; | |
539 | break; | |
540 | } | |
541 | ||
542 | cupsdFinishProcess(pid, command, sizeof(command), NULL); | |
543 | ||
544 | if (status) | |
545 | { | |
546 | if (WIFEXITED(status)) | |
547 | cupsdLogMessage(CUPSD_LOG_ERROR, | |
548 | "Unable to create SSL server key and certificate - " | |
549 | "the certtool command stopped with status %d!", | |
550 | WEXITSTATUS(status)); | |
551 | else | |
552 | cupsdLogMessage(CUPSD_LOG_ERROR, | |
553 | "Unable to create SSL server key and certificate - " | |
554 | "the certtool command crashed on signal %d!", | |
555 | WTERMSIG(status)); | |
556 | } | |
557 | else | |
558 | { | |
559 | cupsdLogMessage(CUPSD_LOG_INFO, | |
560 | "Created SSL server certificate file \"%s\"...", | |
561 | ServerCertificate); | |
562 | } | |
563 | ||
564 | return (!status); | |
565 | } | |
566 | ||
567 | ||
568 | /* | |
569 | * End of "$Id$". | |
570 | */ |