]>
Commit | Line | Data |
---|---|---|
28f540f4 RM |
1 | /* @(#)svc_udp.c 2.2 88/07/29 4.0 RPCSRC */ |
2 | /* | |
3 | * Sun RPC is a product of Sun Microsystems, Inc. and is provided for | |
4 | * unrestricted use provided that this legend is included on all tape | |
5 | * media and as a part of the software program in whole or part. Users | |
6 | * may copy or modify Sun RPC without charge, but are not authorized | |
7 | * to license or distribute it to anyone else except as part of a product or | |
8 | * program developed by the user. | |
cbd3dceb | 9 | * |
28f540f4 RM |
10 | * SUN RPC IS PROVIDED AS IS WITH NO WARRANTIES OF ANY KIND INCLUDING THE |
11 | * WARRANTIES OF DESIGN, MERCHANTIBILITY AND FITNESS FOR A PARTICULAR | |
12 | * PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE OR TRADE PRACTICE. | |
cbd3dceb | 13 | * |
28f540f4 RM |
14 | * Sun RPC is provided with no support and without any obligation on the |
15 | * part of Sun Microsystems, Inc. to assist in its use, correction, | |
16 | * modification or enhancement. | |
cbd3dceb | 17 | * |
28f540f4 RM |
18 | * SUN MICROSYSTEMS, INC. SHALL HAVE NO LIABILITY WITH RESPECT TO THE |
19 | * INFRINGEMENT OF COPYRIGHTS, TRADE SECRETS OR ANY PATENTS BY SUN RPC | |
20 | * OR ANY PART THEREOF. | |
cbd3dceb | 21 | * |
28f540f4 RM |
22 | * In no event will Sun Microsystems, Inc. be liable for any lost revenue |
23 | * or profits or other special, indirect and consequential damages, even if | |
24 | * Sun has been advised of the possibility of such damages. | |
cbd3dceb | 25 | * |
28f540f4 RM |
26 | * Sun Microsystems, Inc. |
27 | * 2550 Garcia Avenue | |
28 | * Mountain View, California 94043 | |
29 | */ | |
30 | #if !defined(lint) && defined(SCCSIDS) | |
31 | static char sccsid[] = "@(#)svc_udp.c 1.24 87/08/11 Copyr 1984 Sun Micro"; | |
32 | #endif | |
33 | ||
34 | /* | |
35 | * svc_udp.c, | |
36 | * Server side for UDP/IP based RPC. (Does some caching in the hopes of | |
37 | * achieving execute-at-most-once semantics.) | |
38 | * | |
39 | * Copyright (C) 1984, Sun Microsystems, Inc. | |
40 | */ | |
41 | ||
42 | #include <stdio.h> | |
e7fd8a39 UD |
43 | #include <unistd.h> |
44 | #include <string.h> | |
28f540f4 RM |
45 | #include <rpc/rpc.h> |
46 | #include <sys/socket.h> | |
47 | #include <errno.h> | |
48 | ||
50304ef0 UD |
49 | #ifdef USE_IN_LIBIO |
50 | # include <libio/iolibio.h> | |
51 | # define fputs(s, f) _IO_fputs (s, f) | |
52 | #endif | |
28f540f4 RM |
53 | |
54 | #define rpc_buffer(xprt) ((xprt)->xp_p1) | |
1f64ac13 | 55 | #ifndef MAX |
e7fd8a39 | 56 | #define MAX(a, b) ((a > b) ? a : b) |
1f64ac13 | 57 | #endif |
28f540f4 | 58 | |
e7fd8a39 UD |
59 | static bool_t svcudp_recv (SVCXPRT *, struct rpc_msg *); |
60 | static bool_t svcudp_reply (SVCXPRT *, struct rpc_msg *); | |
61 | static enum xprt_stat svcudp_stat (SVCXPRT *); | |
62 | static bool_t svcudp_getargs (SVCXPRT *, xdrproc_t, caddr_t); | |
63 | static bool_t svcudp_freeargs (SVCXPRT *, xdrproc_t, caddr_t); | |
64 | static void svcudp_destroy (SVCXPRT *); | |
65 | ||
66 | static const struct xp_ops svcudp_op = | |
67 | { | |
68 | svcudp_recv, | |
69 | svcudp_stat, | |
70 | svcudp_getargs, | |
71 | svcudp_reply, | |
72 | svcudp_freeargs, | |
73 | svcudp_destroy | |
28f540f4 RM |
74 | }; |
75 | ||
e7fd8a39 UD |
76 | static int cache_get (SVCXPRT *, struct rpc_msg *, char **replyp, |
77 | u_long *replylenp); | |
78 | static void cache_set (SVCXPRT *xprt, u_long replylen); | |
28f540f4 RM |
79 | |
80 | /* | |
81 | * kept in xprt->xp_p2 | |
82 | */ | |
e7fd8a39 UD |
83 | struct svcudp_data |
84 | { | |
85 | u_int su_iosz; /* byte size of send.recv buffer */ | |
86 | u_long su_xid; /* transaction id */ | |
87 | XDR su_xdrs; /* XDR handle */ | |
88 | char su_verfbody[MAX_AUTH_BYTES]; /* verifier body */ | |
89 | char *su_cache; /* cached data, NULL if no cache */ | |
90 | }; | |
28f540f4 RM |
91 | #define su_data(xprt) ((struct svcudp_data *)(xprt->xp_p2)) |
92 | ||
93 | /* | |
94 | * Usage: | |
e7fd8a39 | 95 | * xprt = svcudp_create(sock); |
28f540f4 RM |
96 | * |
97 | * If sock<0 then a socket is created, else sock is used. | |
98 | * If the socket, sock is not bound to a port then svcudp_create | |
99 | * binds it to an arbitrary port. In any (successful) case, | |
100 | * xprt->xp_sock is the registered socket number and xprt->xp_port is the | |
101 | * associated port number. | |
102 | * Once *xprt is initialized, it is registered as a transporter; | |
103 | * see (svc.h, xprt_register). | |
104 | * The routines returns NULL if a problem occurred. | |
105 | */ | |
106 | SVCXPRT * | |
e7fd8a39 UD |
107 | svcudp_bufcreate (sock, sendsz, recvsz) |
108 | int sock; | |
109 | u_int sendsz, recvsz; | |
28f540f4 | 110 | { |
e7fd8a39 UD |
111 | bool_t madesock = FALSE; |
112 | SVCXPRT *xprt; | |
113 | struct svcudp_data *su; | |
114 | struct sockaddr_in addr; | |
f671aeab | 115 | size_t len = sizeof (struct sockaddr_in); |
e7fd8a39 UD |
116 | |
117 | if (sock == RPC_ANYSOCK) | |
118 | { | |
50304ef0 | 119 | if ((sock = __socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) |
e7fd8a39 UD |
120 | { |
121 | perror (_("svcudp_create: socket creation problem")); | |
122 | return (SVCXPRT *) NULL; | |
28f540f4 | 123 | } |
e7fd8a39 UD |
124 | madesock = TRUE; |
125 | } | |
50304ef0 | 126 | __bzero ((char *) &addr, sizeof (addr)); |
e7fd8a39 UD |
127 | addr.sin_family = AF_INET; |
128 | if (bindresvport (sock, &addr)) | |
129 | { | |
130 | addr.sin_port = 0; | |
131 | (void) bind (sock, (struct sockaddr *) &addr, len); | |
132 | } | |
133 | if (getsockname (sock, (struct sockaddr *) &addr, &len) != 0) | |
134 | { | |
135 | perror (_("svcudp_create - cannot getsockname")); | |
136 | if (madesock) | |
50304ef0 | 137 | (void) __close (sock); |
e7fd8a39 UD |
138 | return (SVCXPRT *) NULL; |
139 | } | |
140 | xprt = (SVCXPRT *) mem_alloc (sizeof (SVCXPRT)); | |
141 | if (xprt == NULL) | |
142 | { | |
143 | (void) fputs (_("svcudp_create: out of memory\n"), stderr); | |
144 | return NULL; | |
145 | } | |
146 | su = (struct svcudp_data *) mem_alloc (sizeof (*su)); | |
147 | if (su == NULL) | |
148 | { | |
149 | (void) fputs (_("svcudp_create: out of memory\n"), stderr); | |
150 | return NULL; | |
151 | } | |
152 | su->su_iosz = ((MAX (sendsz, recvsz) + 3) / 4) * 4; | |
153 | if ((rpc_buffer (xprt) = mem_alloc (su->su_iosz)) == NULL) | |
154 | { | |
155 | (void) fputs (_("svcudp_create: out of memory\n"), stderr); | |
156 | return NULL; | |
157 | } | |
158 | xdrmem_create (&(su->su_xdrs), rpc_buffer (xprt), su->su_iosz, XDR_DECODE); | |
159 | su->su_cache = NULL; | |
160 | xprt->xp_p2 = (caddr_t) su; | |
161 | xprt->xp_verf.oa_base = su->su_verfbody; | |
162 | xprt->xp_ops = &svcudp_op; | |
163 | xprt->xp_port = ntohs (addr.sin_port); | |
164 | xprt->xp_sock = sock; | |
165 | xprt_register (xprt); | |
166 | return xprt; | |
28f540f4 RM |
167 | } |
168 | ||
169 | SVCXPRT * | |
e7fd8a39 UD |
170 | svcudp_create (sock) |
171 | int sock; | |
28f540f4 RM |
172 | { |
173 | ||
e7fd8a39 | 174 | return svcudp_bufcreate (sock, UDPMSGSIZE, UDPMSGSIZE); |
28f540f4 RM |
175 | } |
176 | ||
177 | static enum xprt_stat | |
e7fd8a39 UD |
178 | svcudp_stat (xprt) |
179 | SVCXPRT *xprt; | |
28f540f4 RM |
180 | { |
181 | ||
e7fd8a39 | 182 | return XPRT_IDLE; |
28f540f4 RM |
183 | } |
184 | ||
185 | static bool_t | |
e7fd8a39 UD |
186 | svcudp_recv (xprt, msg) |
187 | SVCXPRT *xprt; | |
188 | struct rpc_msg *msg; | |
28f540f4 | 189 | { |
e7fd8a39 UD |
190 | struct svcudp_data *su = su_data (xprt); |
191 | XDR *xdrs = &(su->su_xdrs); | |
192 | int rlen; | |
193 | char *reply; | |
194 | u_long replylen; | |
f671aeab | 195 | size_t len; |
e7fd8a39 UD |
196 | |
197 | again: | |
f671aeab UD |
198 | /* FIXME -- should xp_addrlen be a size_t? */ |
199 | len = sizeof(struct sockaddr_in); | |
e7fd8a39 | 200 | rlen = recvfrom (xprt->xp_sock, rpc_buffer (xprt), (int) su->su_iosz, 0, |
f671aeab UD |
201 | (struct sockaddr *) &(xprt->xp_raddr), &len); |
202 | xprt->xp_addrlen = len; | |
e7fd8a39 UD |
203 | if (rlen == -1 && errno == EINTR) |
204 | goto again; | |
205 | if (rlen < 16) /* < 4 32-bit ints? */ | |
206 | return FALSE; | |
207 | xdrs->x_op = XDR_DECODE; | |
208 | XDR_SETPOS (xdrs, 0); | |
209 | if (!xdr_callmsg (xdrs, msg)) | |
210 | return FALSE; | |
211 | su->su_xid = msg->rm_xid; | |
212 | if (su->su_cache != NULL) | |
213 | { | |
214 | if (cache_get (xprt, msg, &reply, &replylen)) | |
215 | { | |
216 | (void) sendto (xprt->xp_sock, reply, (int) replylen, 0, | |
f671aeab | 217 | (struct sockaddr *) &xprt->xp_raddr, len); |
e7fd8a39 | 218 | return TRUE; |
28f540f4 | 219 | } |
e7fd8a39 UD |
220 | } |
221 | return TRUE; | |
28f540f4 RM |
222 | } |
223 | ||
224 | static bool_t | |
e7fd8a39 UD |
225 | svcudp_reply (xprt, msg) |
226 | SVCXPRT *xprt; | |
227 | struct rpc_msg *msg; | |
28f540f4 | 228 | { |
e7fd8a39 UD |
229 | struct svcudp_data *su = su_data (xprt); |
230 | XDR *xdrs = &(su->su_xdrs); | |
231 | int slen; | |
232 | bool_t stat = FALSE; | |
233 | ||
234 | xdrs->x_op = XDR_ENCODE; | |
235 | XDR_SETPOS (xdrs, 0); | |
236 | msg->rm_xid = su->su_xid; | |
237 | if (xdr_replymsg (xdrs, msg)) | |
238 | { | |
239 | slen = (int) XDR_GETPOS (xdrs); | |
240 | if (sendto (xprt->xp_sock, rpc_buffer (xprt), slen, 0, | |
241 | (struct sockaddr *) &(xprt->xp_raddr), xprt->xp_addrlen) | |
242 | == slen) | |
243 | { | |
244 | stat = TRUE; | |
245 | if (su->su_cache && slen >= 0) | |
246 | { | |
247 | cache_set (xprt, (u_long) slen); | |
248 | } | |
28f540f4 | 249 | } |
e7fd8a39 UD |
250 | } |
251 | return stat; | |
28f540f4 RM |
252 | } |
253 | ||
254 | static bool_t | |
e7fd8a39 UD |
255 | svcudp_getargs (xprt, xdr_args, args_ptr) |
256 | SVCXPRT *xprt; | |
257 | xdrproc_t xdr_args; | |
258 | caddr_t args_ptr; | |
28f540f4 RM |
259 | { |
260 | ||
e7fd8a39 | 261 | return (*xdr_args) (&(su_data (xprt)->su_xdrs), args_ptr); |
28f540f4 RM |
262 | } |
263 | ||
264 | static bool_t | |
e7fd8a39 UD |
265 | svcudp_freeargs (xprt, xdr_args, args_ptr) |
266 | SVCXPRT *xprt; | |
267 | xdrproc_t xdr_args; | |
268 | caddr_t args_ptr; | |
28f540f4 | 269 | { |
e7fd8a39 | 270 | XDR *xdrs = &(su_data (xprt)->su_xdrs); |
28f540f4 | 271 | |
e7fd8a39 UD |
272 | xdrs->x_op = XDR_FREE; |
273 | return (*xdr_args) (xdrs, args_ptr); | |
28f540f4 RM |
274 | } |
275 | ||
276 | static void | |
e7fd8a39 UD |
277 | svcudp_destroy (xprt) |
278 | SVCXPRT *xprt; | |
28f540f4 | 279 | { |
e7fd8a39 UD |
280 | struct svcudp_data *su = su_data (xprt); |
281 | ||
282 | xprt_unregister (xprt); | |
50304ef0 | 283 | (void) __close (xprt->xp_sock); |
e7fd8a39 UD |
284 | XDR_DESTROY (&(su->su_xdrs)); |
285 | mem_free (rpc_buffer (xprt), su->su_iosz); | |
286 | mem_free ((caddr_t) su, sizeof (struct svcudp_data)); | |
287 | mem_free ((caddr_t) xprt, sizeof (SVCXPRT)); | |
28f540f4 RM |
288 | } |
289 | ||
290 | ||
291 | /***********this could be a separate file*********************/ | |
292 | ||
293 | /* | |
294 | * Fifo cache for udp server | |
295 | * Copies pointers to reply buffers into fifo cache | |
296 | * Buffers are sent again if retransmissions are detected. | |
297 | */ | |
298 | ||
e7fd8a39 | 299 | #define SPARSENESS 4 /* 75% sparse */ |
28f540f4 RM |
300 | |
301 | #define CACHE_PERROR(msg) \ | |
302 | (void) fprintf(stderr,"%s\n", msg) | |
303 | ||
304 | #define ALLOC(type, size) \ | |
305 | (type *) mem_alloc((unsigned) (sizeof(type) * (size))) | |
306 | ||
307 | #define BZERO(addr, type, size) \ | |
50304ef0 | 308 | __bzero((char *) addr, sizeof(type) * (int) (size)) |
28f540f4 RM |
309 | |
310 | /* | |
311 | * An entry in the cache | |
312 | */ | |
313 | typedef struct cache_node *cache_ptr; | |
e7fd8a39 UD |
314 | struct cache_node |
315 | { | |
316 | /* | |
317 | * Index into cache is xid, proc, vers, prog and address | |
318 | */ | |
319 | u_long cache_xid; | |
320 | u_long cache_proc; | |
321 | u_long cache_vers; | |
322 | u_long cache_prog; | |
323 | struct sockaddr_in cache_addr; | |
324 | /* | |
325 | * The cached reply and length | |
326 | */ | |
327 | char *cache_reply; | |
328 | u_long cache_replylen; | |
329 | /* | |
330 | * Next node on the list, if there is a collision | |
331 | */ | |
332 | cache_ptr cache_next; | |
333 | }; | |
28f540f4 RM |
334 | |
335 | ||
336 | ||
337 | /* | |
338 | * The entire cache | |
339 | */ | |
e7fd8a39 UD |
340 | struct udp_cache |
341 | { | |
342 | u_long uc_size; /* size of cache */ | |
343 | cache_ptr *uc_entries; /* hash table of entries in cache */ | |
344 | cache_ptr *uc_fifo; /* fifo list of entries in cache */ | |
345 | u_long uc_nextvictim; /* points to next victim in fifo list */ | |
346 | u_long uc_prog; /* saved program number */ | |
347 | u_long uc_vers; /* saved version number */ | |
348 | u_long uc_proc; /* saved procedure number */ | |
349 | struct sockaddr_in uc_addr; /* saved caller's address */ | |
350 | }; | |
28f540f4 RM |
351 | |
352 | ||
353 | /* | |
354 | * the hashing function | |
355 | */ | |
356 | #define CACHE_LOC(transp, xid) \ | |
cbd3dceb | 357 | (xid % (SPARSENESS*((struct udp_cache *) su_data(transp)->su_cache)->uc_size)) |
28f540f4 RM |
358 | |
359 | ||
360 | /* | |
cbd3dceb | 361 | * Enable use of the cache. |
28f540f4 RM |
362 | * Note: there is no disable. |
363 | */ | |
e7fd8a39 UD |
364 | int |
365 | svcudp_enablecache (SVCXPRT *transp, u_long size) | |
28f540f4 | 366 | { |
e7fd8a39 UD |
367 | struct svcudp_data *su = su_data (transp); |
368 | struct udp_cache *uc; | |
369 | ||
370 | if (su->su_cache != NULL) | |
371 | { | |
372 | CACHE_PERROR (_("enablecache: cache already enabled")); | |
373 | return 0; | |
374 | } | |
375 | uc = ALLOC (struct udp_cache, 1); | |
376 | if (uc == NULL) | |
377 | { | |
378 | CACHE_PERROR (_("enablecache: could not allocate cache")); | |
379 | return 0; | |
380 | } | |
381 | uc->uc_size = size; | |
382 | uc->uc_nextvictim = 0; | |
383 | uc->uc_entries = ALLOC (cache_ptr, size * SPARSENESS); | |
384 | if (uc->uc_entries == NULL) | |
385 | { | |
386 | CACHE_PERROR (_("enablecache: could not allocate cache data")); | |
387 | return 0; | |
388 | } | |
389 | BZERO (uc->uc_entries, cache_ptr, size * SPARSENESS); | |
390 | uc->uc_fifo = ALLOC (cache_ptr, size); | |
391 | if (uc->uc_fifo == NULL) | |
392 | { | |
393 | CACHE_PERROR (_("enablecache: could not allocate cache fifo")); | |
394 | return 0; | |
395 | } | |
396 | BZERO (uc->uc_fifo, cache_ptr, size); | |
397 | su->su_cache = (char *) uc; | |
398 | return 1; | |
28f540f4 RM |
399 | } |
400 | ||
401 | ||
402 | /* | |
403 | * Set an entry in the cache | |
404 | */ | |
e7fd8a39 UD |
405 | static void |
406 | cache_set (SVCXPRT *xprt, u_long replylen) | |
28f540f4 | 407 | { |
e7fd8a39 UD |
408 | cache_ptr victim; |
409 | cache_ptr *vicp; | |
410 | struct svcudp_data *su = su_data (xprt); | |
411 | struct udp_cache *uc = (struct udp_cache *) su->su_cache; | |
412 | u_int loc; | |
413 | char *newbuf; | |
414 | ||
415 | /* | |
416 | * Find space for the new entry, either by | |
417 | * reusing an old entry, or by mallocing a new one | |
418 | */ | |
419 | victim = uc->uc_fifo[uc->uc_nextvictim]; | |
420 | if (victim != NULL) | |
421 | { | |
422 | loc = CACHE_LOC (xprt, victim->cache_xid); | |
423 | for (vicp = &uc->uc_entries[loc]; | |
424 | *vicp != NULL && *vicp != victim; | |
425 | vicp = &(*vicp)->cache_next) | |
426 | ; | |
427 | if (*vicp == NULL) | |
428 | { | |
429 | CACHE_PERROR (_("cache_set: victim not found")); | |
430 | return; | |
28f540f4 | 431 | } |
e7fd8a39 UD |
432 | *vicp = victim->cache_next; /* remote from cache */ |
433 | newbuf = victim->cache_reply; | |
434 | } | |
435 | else | |
436 | { | |
437 | victim = ALLOC (struct cache_node, 1); | |
438 | if (victim == NULL) | |
439 | { | |
440 | CACHE_PERROR (_("cache_set: victim alloc failed")); | |
441 | return; | |
442 | } | |
443 | newbuf = mem_alloc (su->su_iosz); | |
444 | if (newbuf == NULL) | |
445 | { | |
446 | CACHE_PERROR (_("cache_set: could not allocate new rpc_buffer")); | |
447 | return; | |
448 | } | |
449 | } | |
450 | ||
451 | /* | |
452 | * Store it away | |
453 | */ | |
454 | victim->cache_replylen = replylen; | |
455 | victim->cache_reply = rpc_buffer (xprt); | |
456 | rpc_buffer (xprt) = newbuf; | |
457 | xdrmem_create (&(su->su_xdrs), rpc_buffer (xprt), su->su_iosz, XDR_ENCODE); | |
458 | victim->cache_xid = su->su_xid; | |
459 | victim->cache_proc = uc->uc_proc; | |
460 | victim->cache_vers = uc->uc_vers; | |
461 | victim->cache_prog = uc->uc_prog; | |
462 | victim->cache_addr = uc->uc_addr; | |
463 | loc = CACHE_LOC (xprt, victim->cache_xid); | |
464 | victim->cache_next = uc->uc_entries[loc]; | |
465 | uc->uc_entries[loc] = victim; | |
466 | uc->uc_fifo[uc->uc_nextvictim++] = victim; | |
467 | uc->uc_nextvictim %= uc->uc_size; | |
28f540f4 RM |
468 | } |
469 | ||
470 | /* | |
471 | * Try to get an entry from the cache | |
472 | * return 1 if found, 0 if not found | |
473 | */ | |
e7fd8a39 UD |
474 | static int |
475 | cache_get (xprt, msg, replyp, replylenp) | |
476 | SVCXPRT *xprt; | |
477 | struct rpc_msg *msg; | |
478 | char **replyp; | |
479 | u_long *replylenp; | |
28f540f4 | 480 | { |
e7fd8a39 UD |
481 | u_int loc; |
482 | cache_ptr ent; | |
483 | struct svcudp_data *su = su_data (xprt); | |
484 | struct udp_cache *uc = (struct udp_cache *) su->su_cache; | |
485 | ||
50304ef0 | 486 | #define EQADDR(a1, a2) (memcmp((char*)&a1, (char*)&a2, sizeof(a1)) == 0) |
e7fd8a39 UD |
487 | |
488 | loc = CACHE_LOC (xprt, su->su_xid); | |
489 | for (ent = uc->uc_entries[loc]; ent != NULL; ent = ent->cache_next) | |
490 | { | |
491 | if (ent->cache_xid == su->su_xid && | |
492 | ent->cache_proc == uc->uc_proc && | |
493 | ent->cache_vers == uc->uc_vers && | |
494 | ent->cache_prog == uc->uc_prog && | |
495 | EQADDR (ent->cache_addr, uc->uc_addr)) | |
496 | { | |
497 | *replyp = ent->cache_reply; | |
498 | *replylenp = ent->cache_replylen; | |
499 | return 1; | |
28f540f4 | 500 | } |
e7fd8a39 UD |
501 | } |
502 | /* | |
503 | * Failed to find entry | |
504 | * Remember a few things so we can do a set later | |
505 | */ | |
506 | uc->uc_proc = msg->rm_call.cb_proc; | |
507 | uc->uc_vers = msg->rm_call.cb_vers; | |
508 | uc->uc_prog = msg->rm_call.cb_prog; | |
509 | uc->uc_addr = xprt->xp_raddr; | |
510 | return 0; | |
28f540f4 | 511 | } |