]>
Commit | Line | Data |
---|---|---|
ef4b2d45 | 1 | import sys |
2f68c436 | 2 | from waflib import Logs, Options, Errors |
4291fdcf AB |
3 | |
4 | # Check for kerberos | |
5 | have_gssapi=False | |
6 | ||
a0464e3f AS |
7 | krb5_min_required_version = "1.9" |
8 | ||
7f033268 | 9 | # Required versions |
a0464e3f AS |
10 | krb5_required_version = krb5_min_required_version |
11 | if conf.CONFIG_SET('AD_DC_BUILD_IS_ENABLED'): | |
b896da35 | 12 | krb5_required_version = "1.21" |
a0464e3f AS |
13 | |
14 | def parse_version(v): | |
15 | return tuple(map(int, (v.split(".")))) | |
16 | ||
2ddf89a2 AB |
17 | def krb5_define_syslib(conf, lib, deps): |
18 | found = 'FOUND_SYSTEMLIB_' + lib | |
19 | if found in conf.env: | |
20 | return | |
21 | conf.SET_TARGET_TYPE(lib, 'SYSLIB') | |
22 | conf.SET_SYSLIB_DEPS(lib, deps) | |
23 | conf.env[found] = True | |
d0e77700 | 24 | |
4291fdcf AB |
25 | Logs.info("Looking for kerberos features") |
26 | conf.find_program('krb5-config.heimdal', var='HEIMDAL_KRB5_CONFIG') | |
27503cea AB |
27 | |
28 | if isinstance(Options.options.with_system_mitkrb5, list): | |
29 | path_krb5_config = [x+'/bin' for x in Options.options.with_system_mitkrb5] | |
30 | else: | |
31 | path_krb5_config = None | |
32 | ||
61404faf AS |
33 | conf.CHECK_CFG(args="--cflags --libs", package="com_err", uselib_store="com_err") |
34 | conf.CHECK_FUNCS_IN('_et_list', 'com_err') | |
35 | conf.CHECK_HEADERS('com_err.h', lib='com_err') | |
36 | ||
27503cea | 37 | conf.find_program('krb5-config', path_list=path_krb5_config, var='KRB5_CONFIG') |
4291fdcf | 38 | if conf.env.KRB5_CONFIG: |
ef4b2d45 | 39 | vendor = conf.cmd_and_log(conf.env.KRB5_CONFIG+['--vendor']) |
4291fdcf | 40 | conf.env.KRB5_VENDOR = vendor.strip().lower() |
2f68c436 SM |
41 | if conf.env.KRB5_VENDOR == 'heimdal': |
42 | raise Errors.WafError('--with-system-mitkrb5 cannot be used with system heimdal') | |
43 | ||
238e4c86 AS |
44 | if conf.CHECK_CFG(path=conf.env.KRB5_CONFIG, args="--cflags --libs", |
45 | package="", uselib_store="KRB5"): | |
46 | if 'krb5' in conf.env['LIB_KRB5']: | |
47 | krb5_define_syslib(conf, "krb5", conf.env['LIB_KRB5']) | |
48 | if 'k5crypto' in conf.env['LIB_KRB5']: | |
70dea37d | 49 | krb5_define_syslib(conf, "k5crypto", conf.env['LIB_KRB5']) |
238e4c86 AS |
50 | else: |
51 | raise Errors.WafError('Unable to find required krb5 library!') | |
52 | ||
53 | if conf.CHECK_CFG(path=conf.env.KRB5_CONFIG, args="--cflags --libs", | |
54 | package="gssapi", uselib_store="GSSAPI"): | |
55 | krb5_define_syslib(conf, "gssapi", conf.env['LIB_GSSAPI']) | |
56 | if 'gssapi_krb5' in conf.env['LIB_GSSAPI']: | |
57 | krb5_define_syslib(conf, "gssapi_krb5", conf.env['LIB_GSSAPI']) | |
58 | else: | |
59 | raise Errors.WafError('Unable to find required gssapi library!') | |
60 | ||
61 | if conf.CONFIG_SET('AD_DC_BUILD_IS_ENABLED'): | |
62 | if conf.CHECK_CFG(path=conf.env.KRB5_CONFIG, args="--cflags --libs", | |
63 | package="gssrpc", uselib_store="GSSRPC"): | |
64 | krb5_define_syslib(conf, "gssrpc", conf.env['LIB_GSSRPC']) | |
65 | ||
66 | if conf.CHECK_CFG(path=conf.env.KRB5_CONFIG, args="--cflags --libs", | |
67 | package="kdb", uselib_store="KDB5"): | |
68 | krb5_define_syslib(conf, "kdb5", conf.env['LIB_KDB5']) | |
69 | conf.CHECK_HEADERS('kdb.h', lib='kdb5') | |
70 | else: | |
71 | raise Errors.WafError('Unable to find required kdb5 library!') | |
72 | ||
73 | if conf.CHECK_CFG(path=conf.env.KRB5_CONFIG, args="--cflags --libs", | |
74 | package="kadm-server", uselib_store="KADM5SRV_MIT"): | |
75 | krb5_define_syslib(conf, | |
76 | "kadm5srv_mit", | |
77 | conf.env['LIB_KADM5SRV_MIT']) | |
78 | conf.CHECK_FUNCS_IN('kadm5_init', 'kadm5srv_mit') | |
79 | else: | |
80 | raise Errors.WafError('Unable to find required kadm5srv_mit ' | |
81 | 'library!') | |
82 | ||
2f68c436 SM |
83 | conf.define('USING_SYSTEM_KRB5', 1) |
84 | del conf.env.HEIMDAL_KRB5_CONFIG | |
85 | krb5_conf_version = conf.cmd_and_log(conf.env.KRB5_CONFIG+['--version']).strip() | |
86 | ||
87 | krb5_version = krb5_conf_version.split()[-1] | |
88 | ||
89 | # drop '-prerelease' suffix | |
90 | if krb5_version.find('-') > 0: | |
91 | krb5_version = krb5_version.split("-")[0] | |
92 | ||
93 | if parse_version(krb5_version) < parse_version(krb5_required_version): | |
94 | Logs.error('ERROR: The MIT KRB5 build with Samba AD requires at least %s. %s has been found and cannot be used' % (krb5_required_version, krb5_version)) | |
95 | Logs.error('ERROR: If you want to just build Samba FS (File Server) use the option --without-ad-dc which requires version %s' % (krb5_min_required_version)) | |
96 | Logs.error('ERROR: You may try to build with embedded Heimdal Kerberos by not specifying --with-system-mitkrb5') | |
97 | sys.exit(1) | |
98 | else: | |
99 | Logs.info('MIT Kerberos %s detected, MIT krb5 build can proceed' % (krb5_version)) | |
4291fdcf | 100 | |
d62917d3 | 101 | conf.define('USING_SYSTEM_MITKRB5', '"%s"' % krb5_version) |
5d73cc40 | 102 | |
4291fdcf | 103 | conf.CHECK_HEADERS('krb5.h krb5/locate_plugin.h', lib='krb5') |
5e89a23f | 104 | conf.CHECK_HEADERS('krb5.h krb5/localauth_plugin.h', lib='krb5') |
1fd5bdaf SM |
105 | possible_gssapi_headers="gssapi.h gssapi/gssapi_generic.h gssapi/gssapi.h gssapi/gssapi_ext.h gssapi/gssapi_krb5.h gssapi/gssapi_oid.h" |
106 | conf.CHECK_HEADERS(possible_gssapi_headers, lib='gssapi') | |
4291fdcf | 107 | |
4291fdcf AB |
108 | conf.CHECK_FUNCS_IN('krb5_encrypt_data', 'k5crypto') |
109 | conf.CHECK_FUNCS_IN('des_set_key','crypto') | |
110 | conf.CHECK_FUNCS_IN('copy_Authenticator', 'asn1') | |
111 | conf.CHECK_FUNCS_IN('roken_getaddrinfo_hostspec', 'roken') | |
27503cea | 112 | |
8036aa12 | 113 | conf.CHECK_HEADERS('profile.h', lib='krb5') |
561c7466 | 114 | |
27503cea | 115 | if conf.CHECK_FUNCS_IN('gss_display_status', 'gssapi gssapi_krb5'): |
4291fdcf | 116 | have_gssapi=True |
27503cea AB |
117 | |
118 | if not have_gssapi: | |
6e9aca7d AB |
119 | if conf.env.KRB5_CONFIG and conf.env.KRB5_CONFIG != 'heimdal': |
120 | Logs.error("ERROR: WAF build with MIT Krb5 requires working GSSAPI implementation") | |
121 | sys.exit(1) | |
27503cea | 122 | |
4291fdcf AB |
123 | conf.CHECK_FUNCS_IN(''' |
124 | gss_wrap_iov | |
125 | gss_krb5_import_cred | |
126 | gss_get_name_attribute | |
127 | gss_mech_krb5 | |
128 | gss_oid_equal | |
129 | gss_inquire_sec_context_by_oid | |
130 | gsskrb5_extract_authz_data_from_sec_context | |
131 | gss_krb5_export_lucid_sec_context | |
eb9e3e8a | 132 | gss_import_cred gss_export_cred |
d630a364 | 133 | gss_acquire_cred_from |
27503cea | 134 | ''', 'gssapi gssapi_krb5') |
8036aa12 AS |
135 | conf.CHECK_VARIABLE('GSS_KRB5_CRED_NO_CI_FLAGS_X', |
136 | headers=possible_gssapi_headers, lib='gssapi gssapi_krb5') | |
4291fdcf | 137 | conf.CHECK_FUNCS(''' |
b73235fb AS |
138 | krb5_auth_con_getrecvsubkey |
139 | krb5_auth_con_getsendsubkey | |
4291fdcf AB |
140 | krb5_set_default_in_tkt_etypes krb5_set_default_tgs_enctypes |
141 | krb5_set_default_tgs_ktypes krb5_principal2salt | |
142 | krb5_c_string_to_key krb5_get_pw_salt krb5_string_to_key_salt krb5_auth_con_setkey | |
143 | krb5_auth_con_setuseruserkey krb5_get_permitted_enctypes | |
144 | krb5_get_default_in_tkt_etypes krb5_free_data_contents | |
145 | krb5_principal_get_comp_string krb5_free_unparsed_name | |
146 | krb5_free_keytab_entry_contents krb5_kt_free_entry krb5_krbhst_init | |
147 | krb5_krbhst_get_addrinfo | |
148 | krb5_crypto_init krb5_crypto_destroy | |
149 | krb5_c_verify_checksum krb5_principal_compare_any_realm | |
150 | krb5_parse_name_norealm krb5_princ_size krb5_get_init_creds_opt_set_pac_request | |
151 | krb5_get_renewed_creds krb5_free_error_contents | |
152 | initialize_krb5_error_table krb5_get_init_creds_opt_alloc | |
153 | krb5_get_init_creds_opt_free krb5_get_init_creds_opt_get_error | |
154 | krb5_enctype_to_string krb5_fwd_tgt_creds krb5_auth_con_set_req_cksumtype | |
155 | krb5_get_creds_opt_alloc krb5_get_creds_opt_set_impersonate krb5_get_creds | |
38a5a2c5 | 156 | krb5_get_credentials_for_user krb5_get_host_realm krb5_free_host_realm |
b776bc5f | 157 | krb5_get_init_creds_keyblock krb5_get_init_creds_keytab |
4d77466d | 158 | krb5_make_principal krb5_build_principal_alloc_va |
3ef95a0b | 159 | krb5_cc_get_lifetime krb5_cc_retrieve_cred |
f5e74941 | 160 | krb5_cc_copy_creds |
561c7466 GD |
161 | krb5_free_checksum_contents krb5_c_make_checksum krb5_create_checksum |
162 | krb5_config_get_bool_default krb5_get_profile | |
9fed7ed0 | 163 | krb5_data_copy |
466ebd49 | 164 | krb5_init_keyblock krb5_principal_set_realm krb5_principal_get_type |
e38acb34 | 165 | krb5_principal_set_type |
9c5470be | 166 | krb5_warnx |
b3931af2 | 167 | krb5_get_prompt_types |
8036aa12 | 168 | krb5_mk_req_extended krb5_kt_compare |
75139445 | 169 | krb5_free_enctypes |
c5778a0f | 170 | krb5_free_string |
dbb682f5 AB |
171 | krb5_get_init_creds_opt_set_fast_ccache |
172 | krb5_get_init_creds_opt_set_fast_flags | |
561c7466 | 173 | ''', |
8036aa12 AS |
174 | lib='krb5 k5crypto', |
175 | headers='krb5.h') | |
4291fdcf AB |
176 | conf.CHECK_DECLS('''krb5_get_credentials_for_user |
177 | krb5_auth_con_set_req_cksumtype''', | |
8036aa12 AS |
178 | headers='krb5.h', lib='krb5', always=True) |
179 | conf.CHECK_VARIABLE('AP_OPTS_USE_SUBKEY', headers='krb5.h', lib='krb5') | |
180 | conf.CHECK_VARIABLE('KV5M_KEYTAB', headers='krb5.h', lib='krb5') | |
181 | conf.CHECK_VARIABLE('KRB5_KU_OTHER_CKSUM', headers='krb5.h', lib='krb5') | |
182 | conf.CHECK_VARIABLE('KRB5_KEYUSAGE_APP_DATA_CKSUM', headers='krb5.h', lib='krb5') | |
a80f8e1b SM |
183 | conf.CHECK_VARIABLE('ENCTYPE_AES128_CTS_HMAC_SHA1_96', headers='krb5.h', lib='krb5', mandatory=True) |
184 | conf.CHECK_VARIABLE('ENCTYPE_AES256_CTS_HMAC_SHA1_96', headers='krb5.h', lib='krb5', mandatory=True) | |
8036aa12 AS |
185 | conf.CHECK_DECLS('KRB5_PDU_NONE', reverse=True, headers='krb5.h', lib='krb5') |
186 | conf.CHECK_STRUCTURE_MEMBER('krb5_keytab_entry', 'key', | |
187 | headers='krb5.h', | |
188 | lib='krb5', | |
4291fdcf | 189 | define='HAVE_KRB5_KEYTAB_ENTRY_KEY') |
8036aa12 AS |
190 | conf.CHECK_STRUCTURE_MEMBER('krb5_keytab_entry', 'keyblock', |
191 | headers='krb5.h', | |
192 | lib='krb5', | |
4291fdcf | 193 | define='HAVE_KRB5_KEYTAB_ENTRY_KEYBLOCK') |
8036aa12 AS |
194 | conf.CHECK_STRUCTURE_MEMBER('krb5_address', 'magic', |
195 | headers='krb5.h', | |
196 | lib='krb5', | |
4291fdcf | 197 | define='HAVE_MAGIC_IN_KRB5_ADDRESS') |
8036aa12 AS |
198 | conf.CHECK_STRUCTURE_MEMBER('krb5_address', 'addrtype', |
199 | headers='krb5.h', | |
200 | lib='krb5', | |
4291fdcf | 201 | define='HAVE_ADDRTYPE_IN_KRB5_ADDRESS') |
8036aa12 AS |
202 | conf.CHECK_STRUCTURE_MEMBER('krb5_ap_req', 'ticket', |
203 | headers='krb5.h', | |
204 | lib='krb5', | |
4291fdcf | 205 | define='HAVE_TICKET_POINTER_IN_KRB5_AP_REQ') |
8036aa12 AS |
206 | conf.CHECK_STRUCTURE_MEMBER('krb5_prompt', 'type', |
207 | headers='krb5.h', | |
208 | lib='krb5', | |
6755376c | 209 | define='HAVE_KRB5_PROMPT_TYPE') |
8036aa12 AS |
210 | conf.CHECK_CODE('krb5_trace_info', 'HAVE_KRB5_TRACE_INFO', |
211 | headers='krb5.h', lib='krb5') | |
212 | conf.CHECK_CODE('struct krb5_trace_info', 'HAVE_KRB5_TRACE_INFO_STRUCT', | |
213 | headers='krb5.h', lib='krb5') | |
214 | conf.CHECK_TYPE('krb5_encrypt_block', headers='krb5.h', lib='krb5') | |
4291fdcf AB |
215 | |
216 | conf.CHECK_CODE(''' | |
217 | krb5_context ctx; | |
218 | krb5_get_init_creds_opt *opt = NULL; | |
219 | krb5_get_init_creds_opt_free(ctx, opt); | |
220 | ''', | |
221 | 'KRB5_CREDS_OPT_FREE_REQUIRES_CONTEXT', | |
222 | headers='krb5.h', link=False, | |
8036aa12 | 223 | lib='krb5', |
4291fdcf AB |
224 | msg="Checking whether krb5_get_init_creds_opt_free takes a context argument") |
225 | conf.CHECK_CODE(''' | |
226 | const krb5_data *pkdata; | |
227 | krb5_context context; | |
228 | krb5_principal principal; | |
229 | pkdata = krb5_princ_component(context, principal, 0); | |
230 | ''', | |
231 | 'HAVE_KRB5_PRINC_COMPONENT', | |
232 | headers='krb5.h', lib='krb5', | |
233 | msg="Checking whether krb5_princ_component is available") | |
234 | ||
235 | conf.CHECK_CODE(''' | |
236 | int main(void) { | |
237 | char buf[256]; | |
238 | krb5_enctype_to_string(1, buf, 256); | |
239 | return 0; | |
240 | }''', | |
241 | 'HAVE_KRB5_ENCTYPE_TO_STRING_WITH_SIZE_T_ARG', | |
242 | headers='krb5.h', lib='krb5 k5crypto', | |
9a03cc93 | 243 | addmain=False, cflags=conf.env['WERROR_CFLAGS'], |
4291fdcf AB |
244 | msg="Checking whether krb5_enctype_to_string takes size_t argument") |
245 | ||
246 | conf.CHECK_CODE(''' | |
247 | int main(void) { | |
248 | krb5_context context = NULL; | |
249 | char *str = NULL; | |
250 | krb5_enctype_to_string(context, 1, &str); | |
251 | if (str) free (str); | |
252 | return 0; | |
253 | }''', | |
254 | 'HAVE_KRB5_ENCTYPE_TO_STRING_WITH_KRB5_CONTEXT_ARG', | |
255 | headers='krb5.h stdlib.h', lib='krb5', | |
9a03cc93 | 256 | addmain=False, cflags=conf.env['WERROR_CFLAGS'], |
4291fdcf AB |
257 | msg="Checking whether krb5_enctype_to_string takes krb5_context argument") |
258 | conf.CHECK_CODE(''' | |
259 | int main(void) { | |
260 | krb5_context ctx = NULL; | |
261 | krb5_principal princ = NULL; | |
262 | const char *str = krb5_princ_realm(ctx, princ)->data; | |
263 | return 0; | |
264 | }''', | |
265 | 'HAVE_KRB5_PRINC_REALM', | |
266 | headers='krb5.h', lib='krb5', | |
267 | addmain=False, | |
268 | msg="Checking whether the macro krb5_princ_realm is defined") | |
269 | conf.CHECK_CODE(''' | |
270 | int main(void) { | |
271 | krb5_context context; | |
272 | krb5_principal principal; | |
273 | const char *realm; realm = krb5_principal_get_realm(context, principal); | |
274 | return 0; | |
275 | }''', | |
276 | 'HAVE_KRB5_PRINCIPAL_GET_REALM', | |
277 | headers='krb5.h', lib='krb5', | |
278 | addmain=False, | |
279 | msg="Checking whether krb5_principal_get_realm is defined") | |
280 | conf.CHECK_CODE(''' | |
281 | krb5_enctype enctype; | |
282 | enctype = ENCTYPE_ARCFOUR_HMAC_MD5; | |
283 | ''', | |
284 | '_HAVE_ENCTYPE_ARCFOUR_HMAC_MD5', | |
285 | headers='krb5.h', lib='krb5', | |
54ebd103 | 286 | msg="Checking whether the ENCTYPE_ARCFOUR_HMAC_MD5 key type definition is available") |
ad945bc6 SS |
287 | conf.CHECK_CODE(''' |
288 | krb5_enctype enctype; | |
289 | enctype = ENCTYPE_ARCFOUR_HMAC_MD5_56; | |
290 | ''', | |
291 | '_HAVE_ENCTYPE_ARCFOUR_HMAC_MD5_56', | |
292 | headers='krb5.h', lib='krb5', | |
54ebd103 | 293 | msg="Checking whether the ENCTYPE_ARCFOUR_HMAC_MD5_56 key type definition is available") |
4291fdcf AB |
294 | conf.CHECK_CODE(''' |
295 | krb5_keytype keytype; | |
296 | keytype = KEYTYPE_ARCFOUR_56; | |
297 | ''', | |
298 | '_HAVE_KEYTYPE_ARCFOUR_56', | |
299 | headers='krb5.h', lib='krb5', | |
54ebd103 | 300 | msg="Checking whether the HAVE_KEYTYPE_ARCFOUR_56 key type definition is available") |
4291fdcf AB |
301 | if conf.CONFIG_SET('_HAVE_ENCTYPE_ARCFOUR_HMAC_MD5') and conf.CONFIG_SET('_HAVE_KEYTYPE_ARCFOUR_56'): |
302 | conf.DEFINE('HAVE_ENCTYPE_ARCFOUR_HMAC_MD5', '1') | |
ad945bc6 SS |
303 | if conf.CONFIG_SET('_HAVE_ENCTYPE_ARCFOUR_HMAC_MD5_56') and conf.CONFIG_SET('_HAVE_KEYTYPE_ARCFOUR_56'): |
304 | conf.DEFINE('HAVE_ENCTYPE_ARCFOUR_HMAC_MD5_56', '1') | |
4291fdcf AB |
305 | |
306 | conf.CHECK_CODE(''' | |
307 | krb5_enctype enctype; | |
308 | enctype = ENCTYPE_ARCFOUR_HMAC; | |
309 | ''', | |
310 | 'HAVE_ENCTYPE_ARCFOUR_HMAC', | |
311 | headers='krb5.h', lib='krb5', | |
54ebd103 | 312 | msg="Checking whether the ENCTYPE_ARCFOUR_HMAC key type definition is available") |
ad945bc6 SS |
313 | conf.CHECK_CODE(''' |
314 | krb5_enctype enctype; | |
315 | enctype = ENCTYPE_ARCFOUR_HMAC_EXP; | |
316 | ''', | |
317 | 'HAVE_ENCTYPE_ARCFOUR_HMAC_EXP', | |
318 | headers='krb5.h', lib='krb5', | |
54ebd103 | 319 | msg="Checking whether the ENCTYPE_ARCFOUR_HMAC_EXP key type definition is available") |
4291fdcf AB |
320 | |
321 | conf.CHECK_CODE(''' | |
322 | krb5_context context; | |
323 | krb5_keytab keytab; | |
324 | krb5_init_context(&context); | |
325 | return krb5_kt_resolve(context, "WRFILE:api", &keytab); | |
326 | ''', | |
327 | 'HAVE_WRFILE_KEYTAB', | |
328 | headers='krb5.h', lib='krb5', execute=True, | |
54ebd103 | 329 | msg="Checking whether the WRFILE -keytab is supported") |
4291fdcf AB |
330 | # Check for KRB5_DEPRECATED handling |
331 | conf.CHECK_CODE('''#define KRB5_DEPRECATED 1 | |
332 | #include <krb5.h>''', | |
333 | 'HAVE_KRB5_DEPRECATED_WITH_IDENTIFIER', addmain=False, | |
334 | link=False, | |
8036aa12 | 335 | lib='krb5', |
4291fdcf | 336 | msg="Checking for KRB5_DEPRECATED define taking an identifier") |
6e9aca7d AB |
337 | |
338 | conf.CHECK_CODE(''' | |
339 | krb5_creds creds; | |
340 | creds.flags.b.initial = 0; | |
341 | ''', | |
342 | 'HAVE_FLAGS_IN_KRB5_CREDS', | |
343 | headers='krb5.h', lib='krb5', execute=False, | |
344 | msg="Checking whether krb5_creds have flags property") | |
b5a67b9d AS |
345 | |
346 | # Check for MIT KDC | |
347 | if conf.CONFIG_SET('AD_DC_BUILD_IS_ENABLED'): | |
348 | Logs.info("Looking for MIT KDC") | |
54ebd103 | 349 | conf.DEFINE('SAMBA_USES_MITKDC', 1) |
b5a67b9d AS |
350 | |
351 | kdc_path_list = [ '/usr/sbin', '/usr/lib/mit/sbin'] | |
352 | ||
353 | if getattr(Options.options, 'with_system_mitkdc', None): | |
354 | conf.DEFINE('MIT_KDC_PATH', '"' + Options.options.with_system_mitkdc + '"') | |
355 | else: | |
356 | conf.find_program('krb5kdc', path_list=kdc_path_list, var='MIT_KDC_BINARY', mandatory=True) | |
ef4b2d45 | 357 | conf.DEFINE('MIT_KDC_PATH', '"' + " ".join(conf.env.MIT_KDC_BINARY) + '"') |