silly people that don't want to install/run autoconf :-)
[asterisk/asterisk.git] / http.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 1999 - 2006, Digium, Inc.
5  *
6  * Mark Spencer <markster@digium.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*!
20  * \file 
21  * \brief http server for AMI access
22  *
23  * \author Mark Spencer <markster@digium.com>
24  * This program implements a tiny http server supporting the "get" method
25  * only and was inspired by micro-httpd by Jef Poskanzer 
26  * 
27  * \ref AstHTTP - AMI over the http protocol
28  */
29
30 #include <sys/types.h>
31 #include <stdio.h>
32 #include <unistd.h>
33 #include <stdlib.h>
34 #include <time.h>
35 #include <string.h>
36 #include <netinet/in.h>
37 #include <sys/time.h>
38 #include <sys/socket.h>
39 #include <sys/stat.h>
40 #include <sys/signal.h>
41 #include <arpa/inet.h>
42 #include <errno.h>
43 #include <fcntl.h>
44 #include <pthread.h>
45
46 #include "asterisk.h"
47
48 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
49
50 #include "asterisk/cli.h"
51 #include "asterisk/http.h"
52 #include "asterisk/utils.h"
53 #include "asterisk/strings.h"
54 #include "asterisk/options.h"
55 #include "asterisk/config.h"
56
57 #define MAX_PREFIX 80
58 #define DEFAULT_PREFIX "/asterisk"
59
60 struct ast_http_server_instance {
61         FILE *f;
62         int fd;
63         struct sockaddr_in requestor;
64         ast_http_callback callback;
65 };
66
67 static struct ast_http_uri *uris;
68
69 static int httpfd = -1;
70 static pthread_t master = AST_PTHREADT_NULL;
71 static char prefix[MAX_PREFIX];
72 static int prefix_len = 0;
73 static struct sockaddr_in oldsin;
74 static int enablestatic=0;
75
76 /*! \brief Limit the kinds of files we're willing to serve up */
77 static struct {
78         char *ext;
79         char *mtype;
80 } mimetypes[] = {
81         { "png", "image/png" },
82         { "jpg", "image/jpeg" },
83         { "js", "application/x-javascript" },
84         { "wav", "audio/x-wav" },
85         { "mp3", "audio/mpeg" },
86 };
87
88 static char *ftype2mtype(const char *ftype, char *wkspace, int wkspacelen)
89 {
90         int x;
91         if (ftype) {
92                 for (x=0;x<sizeof(mimetypes) / sizeof(mimetypes[0]); x++) {
93                         if (!strcasecmp(ftype, mimetypes[x].ext))
94                                 return mimetypes[x].mtype;
95                 }
96         }
97         snprintf(wkspace, wkspacelen, "text/%s", ftype ? ftype : "plain");
98         return wkspace;
99 }
100
101 static char *static_callback(struct sockaddr_in *req, const char *uri, struct ast_variable *vars, int *status, char **title, int *contentlength)
102 {
103         char result[4096];
104         char *c=result;
105         char *path;
106         char *ftype, *mtype;
107         char wkspace[80];
108         struct stat st;
109         int len;
110         int fd;
111         void *blob;
112
113         /* Yuck.  I'm not really sold on this, but if you don't deliver static content it makes your configuration 
114            substantially more challenging, but this seems like a rather irritating feature creep on Asterisk. */
115         if (!enablestatic || ast_strlen_zero(uri))
116                 goto out403;
117         /* Disallow any funny filenames at all */
118         if ((uri[0] < 33) || strchr("./|~@#$%^&*() \t", uri[0]))
119                 goto out403;
120         if (strstr(uri, "/.."))
121                 goto out403;
122                 
123         if ((ftype = strrchr(uri, '.')))
124                 ftype++;
125         mtype=ftype2mtype(ftype, wkspace, sizeof(wkspace));
126         
127         /* Cap maximum length */
128         len = strlen(uri) + strlen(ast_config_AST_DATA_DIR) + strlen("/static-http/") + 5;
129         if (len > 1024)
130                 goto out403;
131                 
132         path = alloca(len);
133         sprintf(path, "%s/static-http/%s", ast_config_AST_DATA_DIR, uri);
134         if (stat(path, &st))
135                 goto out404;
136         if (S_ISDIR(st.st_mode))
137                 goto out404;
138         fd = open(path, O_RDONLY);
139         if (fd < 0)
140                 goto out403;
141         
142         len = st.st_size + strlen(mtype) + 40;
143         
144         blob = malloc(len);
145         if (blob) {
146                 c = blob;
147                 sprintf(c, "Content-type: %s\r\n\r\n", mtype);
148                 c += strlen(c);
149                 *contentlength = read(fd, c, st.st_size);
150                 if (*contentlength < 0) {
151                         close(fd);
152                         free(blob);
153                         goto out403;
154                 }
155         }
156         return blob;
157
158 out404:
159         *status = 404;
160         *title = strdup("Not Found");
161         return ast_http_error(404, "Not Found", NULL, "Nothing to see here.  Move along.");
162
163 out403:
164         *status = 403;
165         *title = strdup("Access Denied");
166         return ast_http_error(403, "Access Denied", NULL, "Sorry, I cannot let you do that, Dave.");
167 }
168
169
170 static char *httpstatus_callback(struct sockaddr_in *req, const char *uri, struct ast_variable *vars, int *status, char **title, int *contentlength)
171 {
172         char result[4096];
173         size_t reslen = sizeof(result);
174         char *c=result;
175         struct ast_variable *v;
176         char iabuf[INET_ADDRSTRLEN];
177
178         ast_build_string(&c, &reslen,
179                 "\r\n"
180                 "<title>Asterisk HTTP Status</title>\r\n"
181                 "<body bgcolor=\"#ffffff\">\r\n"
182                 "<table bgcolor=\"#f1f1f1\" align=\"center\"><tr><td bgcolor=\"#e0e0ff\" colspan=\"2\" width=\"500\">\r\n"
183                 "<h2>&nbsp;&nbsp;Asterisk&trade; HTTP Status</h2></td></tr>\r\n");
184
185         ast_build_string(&c, &reslen, "<tr><td><i>Prefix</i></td><td><b>%s</b></td></tr>\r\n", prefix);
186         ast_build_string(&c, &reslen, "<tr><td><i>Bind Address</i></td><td><b>%s</b></td></tr>\r\n",
187                         ast_inet_ntoa(iabuf, sizeof(iabuf), oldsin.sin_addr));
188         ast_build_string(&c, &reslen, "<tr><td><i>Bind Port</i></td><td><b>%d</b></td></tr>\r\n",
189                         ntohs(oldsin.sin_port));
190         ast_build_string(&c, &reslen, "<tr><td colspan=\"2\"><hr></td></tr>\r\n");
191         v = vars;
192         while(v) {
193                 if (strncasecmp(v->name, "cookie_", 7))
194                         ast_build_string(&c, &reslen, "<tr><td><i>Submitted Variable '%s'</i></td><td>%s</td></tr>\r\n", v->name, v->value);
195                 v = v->next;
196         }
197         ast_build_string(&c, &reslen, "<tr><td colspan=\"2\"><hr></td></tr>\r\n");
198         v = vars;
199         while(v) {
200                 if (!strncasecmp(v->name, "cookie_", 7))
201                         ast_build_string(&c, &reslen, "<tr><td><i>Cookie '%s'</i></td><td>%s</td></tr>\r\n", v->name, v->value);
202                 v = v->next;
203         }
204         ast_build_string(&c, &reslen, "</table><center><font size=\"-1\"><i>Asterisk and Digium are registered trademarks of Digium, Inc.</i></font></center></body>\r\n");
205         return strdup(result);
206 }
207
208 static struct ast_http_uri statusuri = {
209         .callback = httpstatus_callback,
210         .description = "Asterisk HTTP General Status",
211         .uri = "httpstatus",
212         .has_subtree = 0,
213 };
214         
215 static struct ast_http_uri staticuri = {
216         .callback = static_callback,
217         .description = "Asterisk HTTP Static Delivery",
218         .uri = "static",
219         .has_subtree = 1,
220 };
221         
222 char *ast_http_error(int status, const char *title, const char *extra_header, const char *text)
223 {
224         char *c = NULL;
225         asprintf(&c,
226                 "Content-type: text/html\r\n"
227                 "%s"
228                 "\r\n"
229                 "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\r\n"
230                 "<html><head>\r\n"
231                 "<title>%d %s</title>\r\n"
232                 "</head><body>\r\n"
233                 "<h1>%s</h1>\r\n"
234                 "<p>%s</p>\r\n"
235                 "<hr />\r\n"
236                 "<address>Asterisk Server</address>\r\n"
237                 "</body></html>\r\n",
238                         (extra_header ? extra_header : ""), status, title, title, text);
239         return c;
240 }
241
242 int ast_http_uri_link(struct ast_http_uri *urih)
243 {
244         struct ast_http_uri *prev=uris;
245         if (!uris || strlen(uris->uri) <= strlen(urih->uri)) {
246                 urih->next = uris;
247                 uris = urih;
248         } else {
249                 while (prev->next && (strlen(prev->next->uri) > strlen(urih->uri)))
250                         prev = prev->next;
251                 /* Insert it here */
252                 urih->next = prev->next;
253                 prev->next = urih;
254         }
255         return 0;
256 }       
257
258 void ast_http_uri_unlink(struct ast_http_uri *urih)
259 {
260         struct ast_http_uri *prev = uris;
261         if (!uris)
262                 return;
263         if (uris == urih) {
264                 uris = uris->next;
265         }
266         while(prev->next) {
267                 if (prev->next == urih) {
268                         prev->next = urih->next;
269                         break;
270                 }
271                 prev = prev->next;
272         }
273 }
274
275 static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char **title, int *contentlength, struct ast_variable **cookies)
276 {
277         char *c;
278         char *turi;
279         char *params;
280         char *var;
281         char *val;
282         struct ast_http_uri *urih=NULL;
283         int len;
284         struct ast_variable *vars=NULL, *v, *prev = NULL;
285         
286         
287         params = strchr(uri, '?');
288         if (params) {
289                 *params = '\0';
290                 params++;
291                 while ((var = strsep(&params, "&"))) {
292                         val = strchr(var, '=');
293                         if (val) {
294                                 *val = '\0';
295                                 val++;
296                                 ast_uri_decode(val);
297                         } else 
298                                 val = "";
299                         ast_uri_decode(var);
300                         if ((v = ast_variable_new(var, val))) {
301                                 if (vars)
302                                         prev->next = v;
303                                 else
304                                         vars = v;
305                                 prev = v;
306                         }
307                 }
308         }
309         if (prev)
310                 prev->next = *cookies;
311         else
312                 vars = *cookies;
313         *cookies = NULL;
314         ast_uri_decode(uri);
315         if (!strncasecmp(uri, prefix, prefix_len)) {
316                 uri += prefix_len;
317                 if (!*uri || (*uri == '/')) {
318                         if (*uri == '/')
319                                 uri++;
320                         urih = uris;
321                         while(urih) {
322                                 len = strlen(urih->uri);
323                                 if (!strncasecmp(urih->uri, uri, len)) {
324                                         if (!uri[len] || uri[len] == '/') {
325                                                 turi = uri + len;
326                                                 if (*turi == '/')
327                                                         turi++;
328                                                 if (!*turi || urih->has_subtree) {
329                                                         uri = turi;
330                                                         break;
331                                                 }
332                                         }
333                                 }
334                                 urih = urih->next;
335                         }
336                 }
337         }
338         if (urih) {
339                 c = urih->callback(sin, uri, vars, status, title, contentlength);
340                 ast_variables_destroy(vars);
341         } else if (ast_strlen_zero(uri) && ast_strlen_zero(prefix)) {
342                 /* Special case: If no prefix, and no URI, send to /static/index.html */
343                 c = ast_http_error(302, "Moved Temporarily", "Location: /static/index.html\r\n", "This is not the page you are looking for...");
344                 *status = 302;
345                 *title = strdup("Moved Temporarily");
346         } else {
347                 c = ast_http_error(404, "Not Found", NULL, "The requested URL was not found on this serer.");
348                 *status = 404;
349                 *title = strdup("Not Found");
350         }
351         return c;
352 }
353
354 static void *ast_httpd_helper_thread(void *data)
355 {
356         char buf[4096];
357         char cookie[4096];
358         char timebuf[256];
359         struct ast_http_server_instance *ser = data;
360         struct ast_variable *var, *prev=NULL, *vars=NULL;
361         char *uri, *c, *title=NULL;
362         char *vname, *vval;
363         int status = 200, contentlength = 0;
364         time_t t;
365
366         if (fgets(buf, sizeof(buf), ser->f)) {
367                 /* Skip method */
368                 uri = buf;
369                 while(*uri && (*uri > 32))
370                         uri++;
371                 if (*uri) {
372                         *uri = '\0';
373                         uri++;
374                 }
375
376                 /* Skip white space */
377                 while (*uri && (*uri < 33))
378                         uri++;
379
380                 if (*uri) {
381                         c = uri;
382                         while (*c && (*c > 32))
383                                  c++;
384                         if (*c) {
385                                 *c = '\0';
386                         }
387                 }
388
389                 while (fgets(cookie, sizeof(cookie), ser->f)) {
390                         /* Trim trailing characters */
391                         while(!ast_strlen_zero(cookie) && (cookie[strlen(cookie) - 1] < 33)) {
392                                 cookie[strlen(cookie) - 1] = '\0';
393                         }
394                         if (ast_strlen_zero(cookie))
395                                 break;
396                         if (!strncasecmp(cookie, "Cookie: ", 8)) {
397                                 vname = cookie + 8;
398                                 vval = strchr(vname, '=');
399                                 if (vval) {
400                                         /* Ditch the = and the quotes */
401                                         *vval = '\0';
402                                         vval++;
403                                         if (*vval)
404                                                 vval++;
405                                         if (strlen(vval))
406                                                 vval[strlen(vval) - 1] = '\0';
407                                         var = ast_variable_new(vname, vval);
408                                         if (var) {
409                                                 if (prev)
410                                                         prev->next = var;
411                                                 else
412                                                         vars = var;
413                                                 prev = var;
414                                         }
415                                 }
416                         }
417                 }
418
419                 if (*uri) {
420                         if (!strcasecmp(buf, "get")) 
421                                 c = handle_uri(&ser->requestor, uri, &status, &title, &contentlength, &vars);
422                         else 
423                                 c = ast_http_error(501, "Not Implemented", NULL, "Attempt to use unimplemented / unsupported method");\
424                 } else 
425                         c = ast_http_error(400, "Bad Request", NULL, "Invalid Request");
426
427                 /* If they aren't mopped up already, clean up the cookies */
428                 if (vars)
429                         ast_variables_destroy(vars);
430
431                 if (!c)
432                         c = ast_http_error(500, "Internal Error", NULL, "Internal Server Error");
433                 if (c) {
434                         time(&t);
435                         strftime(timebuf, sizeof(timebuf), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&t));
436                         ast_cli(ser->fd, "HTTP/1.1 %d %s\r\n", status, title ? title : "OK");
437                         ast_cli(ser->fd, "Server: Asterisk\r\n");
438                         ast_cli(ser->fd, "Date: %s\r\n", timebuf);
439                         ast_cli(ser->fd, "Connection: close\r\n");
440                         if (contentlength) {
441                                 char *tmp;
442                                 tmp = strstr(c, "\r\n\r\n");
443                                 if (tmp) {
444                                         ast_cli(ser->fd, "Content-length: %d\r\n", contentlength);
445                                         write(ser->fd, c, (tmp + 4 - c));
446                                         write(ser->fd, tmp + 4, contentlength);
447                                 }
448                         } else
449                                 ast_cli(ser->fd, "%s", c);
450                         free(c);
451                 }
452                 if (title)
453                         free(title);
454         }
455         fclose(ser->f);
456         free(ser);
457         return NULL;
458 }
459
460 static void *http_root(void *data)
461 {
462         int fd;
463         struct sockaddr_in sin;
464         int sinlen;
465         struct ast_http_server_instance *ser;
466         pthread_t launched;
467         pthread_attr_t attr;
468         
469         for (;;) {
470                 ast_wait_for_input(httpfd, -1);
471                 sinlen = sizeof(sin);
472                 fd = accept(httpfd, (struct sockaddr *)&sin, &sinlen);
473                 if (fd < 0) {
474                         if ((errno != EAGAIN) && (errno != EINTR))
475                                 ast_log(LOG_WARNING, "Accept failed: %s\n", strerror(errno));
476                         continue;
477                 }
478                 ser = ast_calloc(1, sizeof(*ser));
479                 if (ser) {
480                         ser->fd = fd;
481                         memcpy(&ser->requestor, &sin, sizeof(ser->requestor));
482                         if ((ser->f = fdopen(ser->fd, "w+"))) {
483                                 pthread_attr_init(&attr);
484                                 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
485                                 
486                                 if (ast_pthread_create(&launched, &attr, ast_httpd_helper_thread, ser)) {
487                                         ast_log(LOG_WARNING, "Unable to launch helper thread: %s\n", strerror(errno));
488                                         fclose(ser->f);
489                                         free(ser);
490                                 }
491                         } else {
492                                 ast_log(LOG_WARNING, "fdopen failed!\n");
493                                 close(ser->fd);
494                                 free(ser);
495                         }
496                 }
497         }
498         return NULL;
499 }
500
501 char *ast_http_setcookie(const char *var, const char *val, int expires, char *buf, size_t buflen)
502 {
503         char *c;
504         c = buf;
505         ast_build_string(&c, &buflen, "Set-Cookie: %s=\"%s\"; Version=\"1\"", var, val);
506         if (expires)
507                 ast_build_string(&c, &buflen, "; Max-Age=%d", expires);
508         ast_build_string(&c, &buflen, "\r\n");
509         return buf;
510 }
511
512
513 static void http_server_start(struct sockaddr_in *sin)
514 {
515         char iabuf[INET_ADDRSTRLEN];
516         int flags;
517         int x = 1;
518         
519         /* Do nothing if nothing has changed */
520         if (!memcmp(&oldsin, sin, sizeof(oldsin))) {
521                 ast_log(LOG_DEBUG, "Nothing changed in http\n");
522                 return;
523         }
524         
525         memcpy(&oldsin, sin, sizeof(oldsin));
526         
527         /* Shutdown a running server if there is one */
528         if (master != AST_PTHREADT_NULL) {
529                 pthread_cancel(master);
530                 pthread_kill(master, SIGURG);
531                 pthread_join(master, NULL);
532         }
533         
534         if (httpfd != -1)
535                 close(httpfd);
536
537         /* If there's no new server, stop here */
538         if (!sin->sin_family)
539                 return;
540         
541         
542         httpfd = socket(AF_INET, SOCK_STREAM, 0);
543         if (httpfd < 0) {
544                 ast_log(LOG_WARNING, "Unable to allocate socket: %s\n", strerror(errno));
545                 return;
546         }
547         
548         setsockopt(httpfd, SOL_SOCKET, SO_REUSEADDR, &x, sizeof(x));
549         if (bind(httpfd, (struct sockaddr *)sin, sizeof(*sin))) {
550                 ast_log(LOG_NOTICE, "Unable to bind http server to %s:%d: %s\n",
551                         ast_inet_ntoa(iabuf, sizeof(iabuf), sin->sin_addr), ntohs(sin->sin_port),
552                         strerror(errno));
553                 close(httpfd);
554                 httpfd = -1;
555                 return;
556         }
557         if (listen(httpfd, 10)) {
558                 ast_log(LOG_NOTICE, "Unable to listen!\n");
559                 close(httpfd);
560                 httpfd = -1;
561                 return;
562         }
563         flags = fcntl(httpfd, F_GETFL);
564         fcntl(httpfd, F_SETFL, flags | O_NONBLOCK);
565         if (ast_pthread_create(&master, NULL, http_root, NULL)) {
566                 ast_log(LOG_NOTICE, "Unable to launch http server on %s:%d: %s\n",
567                                 ast_inet_ntoa(iabuf, sizeof(iabuf), sin->sin_addr), ntohs(sin->sin_port),
568                                 strerror(errno));
569                 close(httpfd);
570                 httpfd = -1;
571         }
572 }
573
574 static int __ast_http_load(int reload)
575 {
576         struct ast_config *cfg;
577         struct ast_variable *v;
578         int enabled=0;
579         int newenablestatic=0;
580         struct sockaddr_in sin;
581         struct hostent *hp;
582         struct ast_hostent ahp;
583         char newprefix[MAX_PREFIX];
584
585         memset(&sin, 0, sizeof(sin));
586         sin.sin_port = 8088;
587         strcpy(newprefix, DEFAULT_PREFIX);
588         cfg = ast_config_load("http.conf");
589         if (cfg) {
590                 v = ast_variable_browse(cfg, "general");
591                 while(v) {
592                         if (!strcasecmp(v->name, "enabled"))
593                                 enabled = ast_true(v->value);
594                         else if (!strcasecmp(v->name, "enablestatic"))
595                                 newenablestatic = ast_true(v->value);
596                         else if (!strcasecmp(v->name, "bindport"))
597                                 sin.sin_port = ntohs(atoi(v->value));
598                         else if (!strcasecmp(v->name, "bindaddr")) {
599                                 if ((hp = ast_gethostbyname(v->value, &ahp))) {
600                                         memcpy(&sin.sin_addr, hp->h_addr, sizeof(sin.sin_addr));
601                                 } else {
602                                         ast_log(LOG_WARNING, "Invalid bind address '%s'\n", v->value);
603                                 }
604                         } else if (!strcasecmp(v->name, "prefix")) {
605                                 if (!ast_strlen_zero(v->value)) {
606                                         newprefix[0] = '/';
607                                         ast_copy_string(newprefix + 1, v->value, sizeof(newprefix) - 1);
608                                 } else {
609                                         newprefix[0] = '\0';
610                                 }
611                                         
612                         }
613                         v = v->next;
614                 }
615                 ast_config_destroy(cfg);
616         }
617         if (enabled)
618                 sin.sin_family = AF_INET;
619         if (strcmp(prefix, newprefix)) {
620                 ast_copy_string(prefix, newprefix, sizeof(prefix));
621                 prefix_len = strlen(prefix);
622         }
623         enablestatic = newenablestatic;
624         http_server_start(&sin);
625         return 0;
626 }
627
628 static int handle_show_http(int fd, int argc, char *argv[])
629 {
630         char iabuf[INET_ADDRSTRLEN];
631         struct ast_http_uri *urih;
632         if (argc != 3)
633                 return RESULT_SHOWUSAGE;
634         ast_cli(fd, "HTTP Server Status:\n");
635         ast_cli(fd, "Prefix: %s\n", prefix);
636         if (oldsin.sin_family)
637                 ast_cli(fd, "Server Enabled and Bound to %s:%d\n\n",
638                         ast_inet_ntoa(iabuf, sizeof(iabuf), oldsin.sin_addr),
639                         ntohs(oldsin.sin_port));
640         else
641                 ast_cli(fd, "Server Disabled\n\n");
642         ast_cli(fd, "Enabled URI's:\n");
643         urih = uris;
644         while(urih){
645                 ast_cli(fd, "%s/%s%s => %s\n", prefix, urih->uri, (urih->has_subtree ? "/..." : "" ), urih->description);
646                 urih = urih->next;
647         }
648         if (!uris)
649                 ast_cli(fd, "None.\n");
650         return RESULT_SUCCESS;
651 }
652
653 int ast_http_reload(void)
654 {
655         return __ast_http_load(1);
656 }
657
658 static char show_http_help[] =
659 "Usage: http show status\n"
660 "       Shows status of internal HTTP engine\n";
661
662 static struct ast_cli_entry http_cli[] = {
663         { { "http", "show", "status", NULL }, handle_show_http,
664           "Display HTTP server status", show_http_help },
665 };
666
667 int ast_http_init(void)
668 {
669         ast_http_uri_link(&statusuri);
670         ast_http_uri_link(&staticuri);
671         ast_cli_register_multiple(http_cli, sizeof(http_cli) / sizeof(http_cli[0]));
672         return __ast_http_load(0);
673 }