1 | /*
|
---|
2 | * Copyright (c) 2008 Apple Inc. All Rights Reserved.
|
---|
3 | *
|
---|
4 | * Export of this software from the United States of America may require
|
---|
5 | * a specific license from the United States Government. It is the
|
---|
6 | * responsibility of any person or organization contemplating export to
|
---|
7 | * obtain such a license before exporting.
|
---|
8 | *
|
---|
9 | * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
|
---|
10 | * distribute this software and its documentation for any purpose and
|
---|
11 | * without fee is hereby granted, provided that the above copyright
|
---|
12 | * notice appear in all copies and that both that copyright notice and
|
---|
13 | * this permission notice appear in supporting documentation, and that
|
---|
14 | * the name of Apple Inc. not be used in advertising or publicity pertaining
|
---|
15 | * to distribution of the software without specific, written prior
|
---|
16 | * permission. Apple Inc. makes no representations about the suitability of
|
---|
17 | * this software for any purpose. It is provided "as is" without express
|
---|
18 | * or implied warranty.
|
---|
19 | *
|
---|
20 | * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
|
---|
21 | * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
---|
22 | * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
---|
23 | *
|
---|
24 | */
|
---|
25 |
|
---|
26 | #include "kdc_locl.h"
|
---|
27 |
|
---|
28 | #if defined(__APPLE__) && defined(HAVE_GCD)
|
---|
29 |
|
---|
30 | #include <CoreFoundation/CoreFoundation.h>
|
---|
31 | #include <SystemConfiguration/SCDynamicStore.h>
|
---|
32 | #include <SystemConfiguration/SCDynamicStoreCopySpecific.h>
|
---|
33 | #include <SystemConfiguration/SCDynamicStoreKey.h>
|
---|
34 |
|
---|
35 | #include <dispatch/dispatch.h>
|
---|
36 |
|
---|
37 | #include <asl.h>
|
---|
38 | #include <resolv.h>
|
---|
39 |
|
---|
40 | #include <dns_sd.h>
|
---|
41 | #include <err.h>
|
---|
42 |
|
---|
43 | static krb5_kdc_configuration *announce_config;
|
---|
44 | static krb5_context announce_context;
|
---|
45 |
|
---|
46 | struct entry {
|
---|
47 | DNSRecordRef recordRef;
|
---|
48 | char *domain;
|
---|
49 | char *realm;
|
---|
50 | #define F_EXISTS 1
|
---|
51 | #define F_PUSH 2
|
---|
52 | int flags;
|
---|
53 | struct entry *next;
|
---|
54 | };
|
---|
55 |
|
---|
56 | /* #define REGISTER_SRV_RR */
|
---|
57 |
|
---|
58 | static struct entry *g_entries = NULL;
|
---|
59 | static CFStringRef g_hostname = NULL;
|
---|
60 | static DNSServiceRef g_dnsRef = NULL;
|
---|
61 | static SCDynamicStoreRef g_store = NULL;
|
---|
62 | static dispatch_queue_t g_queue = NULL;
|
---|
63 |
|
---|
64 | #define LOG(...) asl_log(NULL, NULL, ASL_LEVEL_INFO, __VA_ARGS__)
|
---|
65 |
|
---|
66 | static void create_dns_sd(void);
|
---|
67 | static void destroy_dns_sd(void);
|
---|
68 | static void update_all(SCDynamicStoreRef, CFArrayRef, void *);
|
---|
69 |
|
---|
70 |
|
---|
71 | /* parameters */
|
---|
72 | static CFStringRef NetworkChangedKey_BackToMyMac = CFSTR("Setup:/Network/BackToMyMac");
|
---|
73 |
|
---|
74 |
|
---|
75 | static char *
|
---|
76 | CFString2utf8(CFStringRef string)
|
---|
77 | {
|
---|
78 | size_t size;
|
---|
79 | char *str;
|
---|
80 |
|
---|
81 | size = 1 + CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8);
|
---|
82 | str = malloc(size);
|
---|
83 | if (str == NULL)
|
---|
84 | return NULL;
|
---|
85 |
|
---|
86 | if (CFStringGetCString(string, str, size, kCFStringEncodingUTF8) == false) {
|
---|
87 | free(str);
|
---|
88 | return NULL;
|
---|
89 | }
|
---|
90 | return str;
|
---|
91 | }
|
---|
92 |
|
---|
93 | /*
|
---|
94 | *
|
---|
95 | */
|
---|
96 |
|
---|
97 | static void
|
---|
98 | retry_timer(void)
|
---|
99 | {
|
---|
100 | dispatch_source_t s;
|
---|
101 | dispatch_time_t t;
|
---|
102 |
|
---|
103 | s = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
|
---|
104 | 0, 0, g_queue);
|
---|
105 | t = dispatch_time(DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC);
|
---|
106 | dispatch_source_set_timer(s, t, 0, NSEC_PER_SEC);
|
---|
107 | dispatch_source_set_event_handler(s, ^{
|
---|
108 | create_dns_sd();
|
---|
109 | dispatch_release(s);
|
---|
110 | });
|
---|
111 | dispatch_resume(s);
|
---|
112 | }
|
---|
113 |
|
---|
114 | /*
|
---|
115 | *
|
---|
116 | */
|
---|
117 |
|
---|
118 | static void
|
---|
119 | create_dns_sd(void)
|
---|
120 | {
|
---|
121 | DNSServiceErrorType error;
|
---|
122 | dispatch_source_t s;
|
---|
123 |
|
---|
124 | error = DNSServiceCreateConnection(&g_dnsRef);
|
---|
125 | if (error) {
|
---|
126 | retry_timer();
|
---|
127 | return;
|
---|
128 | }
|
---|
129 |
|
---|
130 | dispatch_suspend(g_queue);
|
---|
131 |
|
---|
132 | s = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
|
---|
133 | DNSServiceRefSockFD(g_dnsRef),
|
---|
134 | 0, g_queue);
|
---|
135 |
|
---|
136 | dispatch_source_set_event_handler(s, ^{
|
---|
137 | DNSServiceErrorType ret = DNSServiceProcessResult(g_dnsRef);
|
---|
138 | /* on error tear down and set timer to recreate */
|
---|
139 | if (ret != kDNSServiceErr_NoError && ret != kDNSServiceErr_Transient) {
|
---|
140 | dispatch_source_cancel(s);
|
---|
141 | }
|
---|
142 | });
|
---|
143 |
|
---|
144 | dispatch_source_set_cancel_handler(s, ^{
|
---|
145 | destroy_dns_sd();
|
---|
146 | retry_timer();
|
---|
147 | dispatch_release(s);
|
---|
148 | });
|
---|
149 |
|
---|
150 | dispatch_resume(s);
|
---|
151 |
|
---|
152 | /* Do the first update ourself */
|
---|
153 | update_all(g_store, NULL, NULL);
|
---|
154 | dispatch_resume(g_queue);
|
---|
155 | }
|
---|
156 |
|
---|
157 | static void
|
---|
158 | domain_add(const char *domain, const char *realm, int flag)
|
---|
159 | {
|
---|
160 | struct entry *e;
|
---|
161 |
|
---|
162 | for (e = g_entries; e != NULL; e = e->next) {
|
---|
163 | if (strcmp(domain, e->domain) == 0 && strcmp(realm, e->realm) == 0) {
|
---|
164 | e->flags |= flag;
|
---|
165 | return;
|
---|
166 | }
|
---|
167 | }
|
---|
168 |
|
---|
169 | LOG("Adding realm %s to domain %s", realm, domain);
|
---|
170 |
|
---|
171 | e = calloc(1, sizeof(*e));
|
---|
172 | if (e == NULL)
|
---|
173 | return;
|
---|
174 | e->domain = strdup(domain);
|
---|
175 | e->realm = strdup(realm);
|
---|
176 | if (e->domain == NULL || e->realm == NULL) {
|
---|
177 | free(e->domain);
|
---|
178 | free(e->realm);
|
---|
179 | free(e);
|
---|
180 | return;
|
---|
181 | }
|
---|
182 | e->flags = flag | F_PUSH; /* if we allocate, we push */
|
---|
183 | e->next = g_entries;
|
---|
184 | g_entries = e;
|
---|
185 | }
|
---|
186 |
|
---|
187 | struct addctx {
|
---|
188 | int flags;
|
---|
189 | const char *realm;
|
---|
190 | };
|
---|
191 |
|
---|
192 | static void
|
---|
193 | domains_add(const void *key, const void *value, void *context)
|
---|
194 | {
|
---|
195 | char *str = CFString2utf8((CFStringRef)value);
|
---|
196 | struct addctx *ctx = context;
|
---|
197 |
|
---|
198 | if (str == NULL)
|
---|
199 | return;
|
---|
200 | if (str[0] != '\0')
|
---|
201 | domain_add(str, ctx->realm, F_EXISTS | ctx->flags);
|
---|
202 | free(str);
|
---|
203 | }
|
---|
204 |
|
---|
205 |
|
---|
206 | static void
|
---|
207 | dnsCallback(DNSServiceRef sdRef __attribute__((unused)),
|
---|
208 | DNSRecordRef RecordRef __attribute__((unused)),
|
---|
209 | DNSServiceFlags flags __attribute__((unused)),
|
---|
210 | DNSServiceErrorType errorCode __attribute__((unused)),
|
---|
211 | void *context __attribute__((unused)))
|
---|
212 | {
|
---|
213 | }
|
---|
214 |
|
---|
215 | #ifdef REGISTER_SRV_RR
|
---|
216 |
|
---|
217 | /*
|
---|
218 | * Register DNS SRV rr for the realm.
|
---|
219 | */
|
---|
220 |
|
---|
221 | static const char *register_names[2] = {
|
---|
222 | "_kerberos._tcp",
|
---|
223 | "_kerberos._udp"
|
---|
224 | };
|
---|
225 |
|
---|
226 | static struct {
|
---|
227 | DNSRecordRef *val;
|
---|
228 | size_t len;
|
---|
229 | } srvRefs = { NULL, 0 };
|
---|
230 |
|
---|
231 | static void
|
---|
232 | register_srv(const char *realm, const char *hostname, int port)
|
---|
233 | {
|
---|
234 | unsigned char target[1024];
|
---|
235 | int i;
|
---|
236 | int size;
|
---|
237 |
|
---|
238 | /* skip registering LKDC realms */
|
---|
239 | if (strncmp(realm, "LKDC:", 5) == 0)
|
---|
240 | return;
|
---|
241 |
|
---|
242 | /* encode SRV-RR */
|
---|
243 | target[0] = 0; /* priority */
|
---|
244 | target[1] = 0; /* priority */
|
---|
245 | target[2] = 0; /* weight */
|
---|
246 | target[3] = 0; /* weigth */
|
---|
247 | target[4] = (port >> 8) & 0xff; /* port */
|
---|
248 | target[5] = (port >> 0) & 0xff; /* port */
|
---|
249 |
|
---|
250 | size = dn_comp(hostname, target + 6, sizeof(target) - 6, NULL, NULL);
|
---|
251 | if (size < 0)
|
---|
252 | return;
|
---|
253 |
|
---|
254 | size += 6;
|
---|
255 |
|
---|
256 | LOG("register SRV rr for realm %s hostname %s:%d", realm, hostname, port);
|
---|
257 |
|
---|
258 | for (i = 0; i < sizeof(register_names)/sizeof(register_names[0]); i++) {
|
---|
259 | char name[kDNSServiceMaxDomainName];
|
---|
260 | DNSServiceErrorType error;
|
---|
261 | void *ptr;
|
---|
262 |
|
---|
263 | ptr = realloc(srvRefs.val, sizeof(srvRefs.val[0]) * (srvRefs.len + 1));
|
---|
264 | if (ptr == NULL)
|
---|
265 | errx(1, "malloc: out of memory");
|
---|
266 | srvRefs.val = ptr;
|
---|
267 |
|
---|
268 | DNSServiceConstructFullName(name, NULL, register_names[i], realm);
|
---|
269 |
|
---|
270 | error = DNSServiceRegisterRecord(g_dnsRef,
|
---|
271 | &srvRefs.val[srvRefs.len],
|
---|
272 | kDNSServiceFlagsUnique | kDNSServiceFlagsShareConnection,
|
---|
273 | 0,
|
---|
274 | name,
|
---|
275 | kDNSServiceType_SRV,
|
---|
276 | kDNSServiceClass_IN,
|
---|
277 | size,
|
---|
278 | target,
|
---|
279 | 0,
|
---|
280 | dnsCallback,
|
---|
281 | NULL);
|
---|
282 | if (error) {
|
---|
283 | LOG("Failed to register SRV rr for realm %s: %d", realm, error);
|
---|
284 | } else
|
---|
285 | srvRefs.len++;
|
---|
286 | }
|
---|
287 | }
|
---|
288 |
|
---|
289 | static void
|
---|
290 | unregister_srv_realms(void)
|
---|
291 | {
|
---|
292 | if (g_dnsRef) {
|
---|
293 | for (i = 0; i < srvRefs.len; i++)
|
---|
294 | DNSServiceRemoveRecord(g_dnsRef, srvRefs.val[i], 0);
|
---|
295 | }
|
---|
296 | free(srvRefs.val);
|
---|
297 | srvRefs.len = 0;
|
---|
298 | srvRefs.val = NULL;
|
---|
299 | }
|
---|
300 |
|
---|
301 | static void
|
---|
302 | register_srv_realms(CFStringRef host)
|
---|
303 | {
|
---|
304 | krb5_error_code ret;
|
---|
305 | char *hostname;
|
---|
306 | size_t i;
|
---|
307 |
|
---|
308 | /* first unregister old names */
|
---|
309 |
|
---|
310 | hostname = CFString2utf8(host);
|
---|
311 | if (hostname == NULL)
|
---|
312 | return;
|
---|
313 |
|
---|
314 | for(i = 0; i < announce_config->num_db; i++) {
|
---|
315 | char **realms, **r;
|
---|
316 |
|
---|
317 | if (announce_config->db[i]->hdb_get_realms == NULL)
|
---|
318 | continue;
|
---|
319 |
|
---|
320 | ret = (announce_config->db[i]->hdb_get_realms)(announce_context, &realms);
|
---|
321 | if (ret == 0) {
|
---|
322 | for (r = realms; r && *r; r++)
|
---|
323 | register_srv(*r, hostname, 88);
|
---|
324 | krb5_free_host_realm(announce_context, realms);
|
---|
325 | }
|
---|
326 | }
|
---|
327 |
|
---|
328 | free(hostname);
|
---|
329 | }
|
---|
330 | #endif /* REGISTER_SRV_RR */
|
---|
331 |
|
---|
332 | static void
|
---|
333 | update_dns(void)
|
---|
334 | {
|
---|
335 | DNSServiceErrorType error;
|
---|
336 | struct entry **e = &g_entries;
|
---|
337 | char *hostname;
|
---|
338 |
|
---|
339 | hostname = CFString2utf8(g_hostname);
|
---|
340 | if (hostname == NULL)
|
---|
341 | return;
|
---|
342 |
|
---|
343 | while (*e != NULL) {
|
---|
344 | /* remove if this wasn't updated */
|
---|
345 | if (((*e)->flags & F_EXISTS) == 0) {
|
---|
346 | struct entry *drop = *e;
|
---|
347 | *e = (*e)->next;
|
---|
348 |
|
---|
349 | LOG("Deleting realm %s from domain %s",
|
---|
350 | drop->realm, drop->domain);
|
---|
351 |
|
---|
352 | if (drop->recordRef && g_dnsRef)
|
---|
353 | DNSServiceRemoveRecord(g_dnsRef, drop->recordRef, 0);
|
---|
354 | free(drop->domain);
|
---|
355 | free(drop->realm);
|
---|
356 | free(drop);
|
---|
357 | continue;
|
---|
358 | }
|
---|
359 | if ((*e)->flags & F_PUSH) {
|
---|
360 | struct entry *update = *e;
|
---|
361 | char *dnsdata, *name;
|
---|
362 | size_t len;
|
---|
363 |
|
---|
364 | len = strlen(update->realm);
|
---|
365 | asprintf(&dnsdata, "%c%s", (int)len, update->realm);
|
---|
366 | if (dnsdata == NULL)
|
---|
367 | errx(1, "malloc");
|
---|
368 |
|
---|
369 | asprintf(&name, "_kerberos.%s.%s", hostname, update->domain);
|
---|
370 | if (name == NULL)
|
---|
371 | errx(1, "malloc");
|
---|
372 |
|
---|
373 | if (update->recordRef)
|
---|
374 | DNSServiceRemoveRecord(g_dnsRef, update->recordRef, 0);
|
---|
375 |
|
---|
376 | error = DNSServiceRegisterRecord(g_dnsRef,
|
---|
377 | &update->recordRef,
|
---|
378 | kDNSServiceFlagsShared | kDNSServiceFlagsAllowRemoteQuery,
|
---|
379 | 0,
|
---|
380 | name,
|
---|
381 | kDNSServiceType_TXT,
|
---|
382 | kDNSServiceClass_IN,
|
---|
383 | len+1,
|
---|
384 | dnsdata,
|
---|
385 | 0,
|
---|
386 | dnsCallback,
|
---|
387 | NULL);
|
---|
388 | free(name);
|
---|
389 | free(dnsdata);
|
---|
390 | if (error)
|
---|
391 | errx(1, "failure to update entry for %s/%s",
|
---|
392 | update->domain, update->realm);
|
---|
393 | }
|
---|
394 | e = &(*e)->next;
|
---|
395 | }
|
---|
396 | free(hostname);
|
---|
397 | }
|
---|
398 |
|
---|
399 | static void
|
---|
400 | update_entries(SCDynamicStoreRef store, const char *realm, int flags)
|
---|
401 | {
|
---|
402 | CFDictionaryRef btmm;
|
---|
403 |
|
---|
404 | /* we always announce in the local domain */
|
---|
405 | domain_add("local", realm, F_EXISTS | flags);
|
---|
406 |
|
---|
407 | /* announce btmm */
|
---|
408 | btmm = SCDynamicStoreCopyValue(store, NetworkChangedKey_BackToMyMac);
|
---|
409 | if (btmm) {
|
---|
410 | struct addctx addctx;
|
---|
411 |
|
---|
412 | addctx.flags = flags;
|
---|
413 | addctx.realm = realm;
|
---|
414 |
|
---|
415 | CFDictionaryApplyFunction(btmm, domains_add, &addctx);
|
---|
416 | CFRelease(btmm);
|
---|
417 | }
|
---|
418 | }
|
---|
419 |
|
---|
420 | static void
|
---|
421 | update_all(SCDynamicStoreRef store, CFArrayRef changedKeys, void *info)
|
---|
422 | {
|
---|
423 | struct entry *e;
|
---|
424 | CFStringRef host;
|
---|
425 | int i, flags = 0;
|
---|
426 |
|
---|
427 | LOG("something changed, running update");
|
---|
428 |
|
---|
429 | host = SCDynamicStoreCopyLocalHostName(store);
|
---|
430 | if (host == NULL)
|
---|
431 | return;
|
---|
432 |
|
---|
433 | if (g_hostname == NULL || CFStringCompare(host, g_hostname, 0) != kCFCompareEqualTo) {
|
---|
434 | if (g_hostname)
|
---|
435 | CFRelease(g_hostname);
|
---|
436 | g_hostname = CFRetain(host);
|
---|
437 | flags = F_PUSH; /* if hostname has changed, force push */
|
---|
438 |
|
---|
439 | #ifdef REGISTER_SRV_RR
|
---|
440 | register_srv_realms(g_hostname);
|
---|
441 | #endif
|
---|
442 | }
|
---|
443 |
|
---|
444 | for (e = g_entries; e != NULL; e = e->next)
|
---|
445 | e->flags &= ~(F_EXISTS|F_PUSH);
|
---|
446 |
|
---|
447 | for(i = 0; i < announce_config->num_db; i++) {
|
---|
448 | krb5_error_code ret;
|
---|
449 | char **realms, **r;
|
---|
450 |
|
---|
451 | if (announce_config->db[i]->hdb_get_realms == NULL)
|
---|
452 | continue;
|
---|
453 |
|
---|
454 | ret = (announce_config->db[i]->hdb_get_realms)(announce_context, announce_config->db[i], &realms);
|
---|
455 | if (ret == 0) {
|
---|
456 | for (r = realms; r && *r; r++)
|
---|
457 | update_entries(store, *r, flags);
|
---|
458 | krb5_free_host_realm(announce_context, realms);
|
---|
459 | }
|
---|
460 | }
|
---|
461 |
|
---|
462 | update_dns();
|
---|
463 |
|
---|
464 | CFRelease(host);
|
---|
465 | }
|
---|
466 |
|
---|
467 | static void
|
---|
468 | delete_all(void)
|
---|
469 | {
|
---|
470 | struct entry *e;
|
---|
471 |
|
---|
472 | for (e = g_entries; e != NULL; e = e->next)
|
---|
473 | e->flags &= ~(F_EXISTS|F_PUSH);
|
---|
474 |
|
---|
475 | update_dns();
|
---|
476 | if (g_entries != NULL)
|
---|
477 | errx(1, "Failed to remove all bonjour entries");
|
---|
478 | }
|
---|
479 |
|
---|
480 | static void
|
---|
481 | destroy_dns_sd(void)
|
---|
482 | {
|
---|
483 | if (g_dnsRef == NULL)
|
---|
484 | return;
|
---|
485 |
|
---|
486 | delete_all();
|
---|
487 | #ifdef REGISTER_SRV_RR
|
---|
488 | unregister_srv_realms();
|
---|
489 | #endif
|
---|
490 |
|
---|
491 | DNSServiceRefDeallocate(g_dnsRef);
|
---|
492 | g_dnsRef = NULL;
|
---|
493 | }
|
---|
494 |
|
---|
495 |
|
---|
496 | static SCDynamicStoreRef
|
---|
497 | register_notification(void)
|
---|
498 | {
|
---|
499 | SCDynamicStoreRef store;
|
---|
500 | CFStringRef computerNameKey;
|
---|
501 | CFMutableArrayRef keys;
|
---|
502 |
|
---|
503 | computerNameKey = SCDynamicStoreKeyCreateHostNames(kCFAllocatorDefault);
|
---|
504 |
|
---|
505 | store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("Network watcher"),
|
---|
506 | update_all, NULL);
|
---|
507 | if (store == NULL)
|
---|
508 | errx(1, "SCDynamicStoreCreate");
|
---|
509 |
|
---|
510 | keys = CFArrayCreateMutable(kCFAllocatorDefault, 2, &kCFTypeArrayCallBacks);
|
---|
511 | if (keys == NULL)
|
---|
512 | errx(1, "CFArrayCreateMutable");
|
---|
513 |
|
---|
514 | CFArrayAppendValue(keys, computerNameKey);
|
---|
515 | CFArrayAppendValue(keys, NetworkChangedKey_BackToMyMac);
|
---|
516 |
|
---|
517 | if (SCDynamicStoreSetNotificationKeys(store, keys, NULL) == false)
|
---|
518 | errx(1, "SCDynamicStoreSetNotificationKeys");
|
---|
519 |
|
---|
520 | CFRelease(computerNameKey);
|
---|
521 | CFRelease(keys);
|
---|
522 |
|
---|
523 | if (!SCDynamicStoreSetDispatchQueue(store, g_queue))
|
---|
524 | errx(1, "SCDynamicStoreSetDispatchQueue");
|
---|
525 |
|
---|
526 | return store;
|
---|
527 | }
|
---|
528 | #endif
|
---|
529 |
|
---|
530 | void
|
---|
531 | bonjour_announce(krb5_context context, krb5_kdc_configuration *config)
|
---|
532 | {
|
---|
533 | #if defined(__APPLE__) && defined(HAVE_GCD)
|
---|
534 | g_queue = dispatch_queue_create("com.apple.kdc_announce", NULL);
|
---|
535 | if (!g_queue)
|
---|
536 | errx(1, "dispatch_queue_create");
|
---|
537 |
|
---|
538 | g_store = register_notification();
|
---|
539 | announce_config = config;
|
---|
540 | announce_context = context;
|
---|
541 |
|
---|
542 | create_dns_sd();
|
---|
543 | #endif
|
---|
544 | }
|
---|