Code

frontend: Add support for TCP connections.
[sysdb.git] / src / frontend / sock.c
1 /*
2  * SysDB - src/frontend/sock.c
3  * Copyright (C) 2013 Sebastian 'tokkee' Harl <sh@tokkee.org>
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
17  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
19  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
22  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
23  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
24  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
25  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26  */
28 #if HAVE_CONFIG_H
29 #       include "config.h"
30 #endif /* HAVE_CONFIG_H */
32 #include "sysdb.h"
33 #include "core/object.h"
34 #include "frontend/connection-private.h"
35 #include "frontend/sock.h"
37 #include "utils/channel.h"
38 #include "utils/error.h"
39 #include "utils/llist.h"
40 #include "utils/os.h"
41 #include "utils/strbuf.h"
43 #include <assert.h>
44 #include <errno.h>
46 #include <stdio.h>
47 #include <stdlib.h>
48 #include <string.h>
50 #include <unistd.h>
52 #include <sys/time.h>
53 #include <sys/types.h>
54 #include <sys/select.h>
55 #include <sys/socket.h>
56 #include <sys/param.h>
57 #include <sys/un.h>
59 #ifdef HAVE_UCRED_H
60 #       include <ucred.h>
61 #endif
62 #ifdef HAVE_SYS_UCRED_H
63 #       include <sys/ucred.h>
64 #endif
66 #include <pwd.h>
68 #include <netdb.h>
69 #include <libgen.h>
70 #include <pthread.h>
72 /*
73  * private data types
74  */
76 typedef struct {
77         char *address;
78         int   type;
80         int sock_fd;
81         int (*accept)(sdb_conn_t *);
82         int (*peer)(sdb_conn_t *);
83 } listener_t;
85 typedef struct {
86         int type;
87         const char *prefix;
89         int (*open)(listener_t *);
90         void (*close)(listener_t *);
91 } fe_listener_impl_t;
93 struct sdb_fe_socket {
94         listener_t *listeners;
95         size_t listeners_num;
97         sdb_llist_t *open_connections;
99         /* channel used for communication between main
100          * and connection handler threads */
101         sdb_channel_t *chan;
102 };
104 /*
105  * connection management functions
106  */
108 static int
109 unixsock_peer(sdb_conn_t *conn)
111         uid_t uid;
113         struct passwd pw_entry;
114         struct passwd *result = NULL;
115         char buf[1024];
117 #ifdef SO_PEERCRED
118         struct ucred cred;
119         socklen_t len = sizeof(cred);
121         if (getsockopt(conn->fd, SOL_SOCKET, SO_PEERCRED, &cred, &len)
122                         || (len != sizeof(cred))) {
123                 char errbuf[1024];
124                 sdb_log(SDB_LOG_ERR, "frontend: Failed to determine peer for "
125                                 "connection conn#%i: %s", conn->fd,
126                                 sdb_strerror(errno, errbuf, sizeof(errbuf)));
127                 return -1;
128         }
129         uid = cred.uid;
130 #else /* SO_PEERCRED */
131         sdb_log(SDB_LOG_ERR, "frontend: Failed to determine peer for "
132                         "connection conn#%i: operation not supported", conn->fd);
133         return -1;
134 #endif
136         memset(&pw_entry, 0, sizeof(pw_entry));
137         if (getpwuid_r(uid, &pw_entry, buf, sizeof(buf), &result) || (! result)
138                         || (! (conn->username = strdup(result->pw_name)))) {
139                 char errbuf[1024];
140                 sdb_log(SDB_LOG_ERR, "frontend: Failed to determine peer for "
141                                 "connection conn#%i: %s", conn->fd,
142                                 sdb_strerror(errno, errbuf, sizeof(errbuf)));
143                 return -1;
144         }
145         return 0;
146 } /* unixsock_peer */
148 static int
149 open_unixsock(listener_t *listener)
151         char *addr_copy;
152         char *base_dir;
153         struct sockaddr_un sa;
154         int status;
156         listener->sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
157         if (listener->sock_fd < 0) {
158                 char buf[1024];
159                 sdb_log(SDB_LOG_ERR, "frontend: Failed to open UNIX socket %s: %s",
160                                 listener->address, sdb_strerror(errno, buf, sizeof(buf)));
161                 return -1;
162         }
164         memset(&sa, 0, sizeof(sa));
165         sa.sun_family = AF_UNIX;
166         strncpy(sa.sun_path, listener->address, sizeof(sa.sun_path));
168         addr_copy = strdup(listener->address);
169         if (! addr_copy) {
170                 char errbuf[1024];
171                 sdb_log(SDB_LOG_ERR, "frontend: strdup failed: %s",
172                                 sdb_strerror(errno, errbuf, sizeof(errbuf)));
173                 return -1;
174         }
175         base_dir = dirname(addr_copy);
177         /* ensure that the directory exists */
178         if (sdb_mkdir_all(base_dir, 0777)) {
179                 char errbuf[1024];
180                 sdb_log(SDB_LOG_ERR, "frontend: Failed to create directory '%s': %s",
181                                 base_dir, sdb_strerror(errno, errbuf, sizeof(errbuf)));
182                 free(addr_copy);
183                 return -1;
184         }
185         free(addr_copy);
187         if (unlink(listener->address) && (errno != ENOENT)) {
188                 char errbuf[1024];
189                 sdb_log(SDB_LOG_WARNING, "frontend: Failed to remove stale UNIX "
190                                 "socket %s: %s", listener->address,
191                                 sdb_strerror(errno, errbuf, sizeof(errbuf)));
192         }
194         status = bind(listener->sock_fd, (struct sockaddr *)&sa, sizeof(sa));
195         if (status) {
196                 char buf[1024];
197                 sdb_log(SDB_LOG_ERR, "frontend: Failed to bind to UNIX socket %s: %s",
198                                 listener->address, sdb_strerror(errno, buf, sizeof(buf)));
199                 return -1;
200         }
202         listener->peer = unixsock_peer;
203         return 0;
204 } /* open_unixsock */
206 static void
207 close_unixsock(listener_t *listener)
209         assert(listener);
211         if (! listener->address)
212                 return;
214         if (listener->sock_fd >= 0)
215                 close(listener->sock_fd);
216         listener->sock_fd = -1;
218         unlink(listener->address);
219 } /* close_unixsock */
221 static int
222 open_tcp(listener_t *listener)
224         struct addrinfo *ai, *ai_list = NULL;
225         int status;
227         assert(listener);
229         if ((status = sdb_resolve(SDB_NET_TCP, listener->address, &ai_list))) {
230                 sdb_log(SDB_LOG_ERR, "frontend: Failed to resolve '%s': %s",
231                                 listener->address, gai_strerror(status));
232                 return -1;
233         }
235         for (ai = ai_list; ai != NULL; ai = ai->ai_next) {
236                 char errbuf[1024];
237                 int reuse = 1;
239                 listener->sock_fd = socket(ai->ai_family,
240                                 ai->ai_socktype, ai->ai_protocol);
241                 if (listener->sock_fd < 0) {
242                         sdb_log(SDB_LOG_ERR, "frontend: Failed to open socket for %s: %s",
243                                         listener->address,
244                                         sdb_strerror(errno, errbuf, sizeof(errbuf)));
245                         continue;
246                 }
248                 if (setsockopt(listener->sock_fd, SOL_SOCKET, SO_REUSEADDR,
249                                         &reuse, sizeof(reuse)) < 0) {
250                         sdb_log(SDB_LOG_ERR, "frontend: Failed to set socket option: %s",
251                                         sdb_strerror(errno, errbuf, sizeof(errbuf)));
252                         close(listener->sock_fd);
253                         listener->sock_fd = -1;
254                         continue;
255                 }
257                 if (bind(listener->sock_fd, ai->ai_addr, ai->ai_addrlen) < 0) {
258                         char host[1024], port[32];
259                         getnameinfo(ai->ai_addr, ai->ai_addrlen, host, sizeof(host),
260                                         port, sizeof(port), NI_NUMERICHOST | NI_NUMERICSERV);
261                         sdb_log(SDB_LOG_ERR, "frontend: Failed to bind to %s:%s: %s",
262                                         host, port, sdb_strerror(errno, errbuf, sizeof(errbuf)));
263                         close(listener->sock_fd);
264                         listener->sock_fd = -1;
265                         continue;
266                 }
267                 break;
268         }
269         freeaddrinfo(ai_list);
271         if (listener->sock_fd < 0)
272                 return -1;
273         return 0;
274 } /* open_tcp */
276 static void
277 close_tcp(listener_t *listener)
279         assert(listener);
281         if (listener->sock_fd >= 0)
282                 close(listener->sock_fd);
283         listener->sock_fd = -1;
284 } /* close_tcp */
286 /*
287  * private variables
288  */
290 /* the enum has to be sorted the same as the implementations array
291  * to ensure that the type may be used as index into the array */
292 enum {
293         LISTENER_TCP = 0, /* this is the default */
294         LISTENER_UNIXSOCK,
295 };
296 static fe_listener_impl_t listener_impls[] = {
297         { LISTENER_TCP,      "tcp",  open_tcp,      close_tcp },
298         { LISTENER_UNIXSOCK, "unix", open_unixsock, close_unixsock },
299 };
301 /*
302  * private helper functions
303  */
305 static int
306 listener_listen(listener_t *listener)
308         assert(listener);
310         /* try to reopen */
311         if (listener->sock_fd < 0)
312                 if (listener_impls[listener->type].open(listener))
313                         return -1;
314         assert(listener->sock_fd >= 0);
316         if (listen(listener->sock_fd, /* backlog = */ 32)) {
317                 char buf[1024];
318                 sdb_log(SDB_LOG_ERR, "frontend: Failed to listen on socket %s: %s",
319                                 listener->address, sdb_strerror(errno, buf, sizeof(buf)));
320                 return -1;
321         }
322         return 0;
323 } /* listener_listen */
325 static void
326 listener_close(listener_t *listener)
328         assert(listener);
330         if (listener_impls[listener->type].close)
331                 listener_impls[listener->type].close(listener);
333         if (listener->sock_fd >= 0)
334                 close(listener->sock_fd);
335         listener->sock_fd = -1;
336 } /* listener_close */
338 static int
339 get_type(const char *address)
341         char *sep;
342         size_t len;
343         size_t i;
345         if (*address == '/')
346                 return LISTENER_UNIXSOCK;
347         sep = strchr(address, (int)':');
348         if (! sep)
349                 return listener_impls[0].type;
351         assert(sep > address);
352         len = (size_t)(sep - address);
354         for (i = 0; i < SDB_STATIC_ARRAY_LEN(listener_impls); ++i) {
355                 fe_listener_impl_t *impl = listener_impls + i;
357                 if (!strncmp(address, impl->prefix, len)) {
358                         assert(impl->type == (int)i);
359                         return impl->type;
360                 }
361         }
362         return -1;
363 } /* get_type */
365 static void
366 listener_destroy(listener_t *listener)
368         if (! listener)
369                 return;
371         listener_close(listener);
373         if (listener->address)
374                 free(listener->address);
375         listener->address = NULL;
376 } /* listener_destroy */
378 static listener_t *
379 listener_create(sdb_fe_socket_t *sock, const char *address)
381         listener_t *listener;
382         size_t len;
383         int type;
385         type = get_type(address);
386         if (type < 0) {
387                 sdb_log(SDB_LOG_ERR, "frontend: Unsupported address type specified "
388                                 "in listen address '%s'", address);
389                 return NULL;
390         }
392         listener = realloc(sock->listeners,
393                         (sock->listeners_num + 1) * sizeof(*sock->listeners));
394         if (! listener) {
395                 char buf[1024];
396                 sdb_log(SDB_LOG_ERR, "frontend: Failed to allocate memory: %s",
397                                 sdb_strerror(errno, buf, sizeof(buf)));
398                 return NULL;
399         }
401         sock->listeners = listener;
402         listener = sock->listeners + sock->listeners_num;
404         len = strlen(listener_impls[type].prefix);
405         if ((! strncmp(address, listener_impls[type].prefix, len))
406                         && (address[len] == ':'))
407                 address += strlen(listener_impls[type].prefix) + 1;
409         listener->sock_fd = -1;
410         listener->address = strdup(address);
411         if (! listener->address) {
412                 char buf[1024];
413                 sdb_log(SDB_LOG_ERR, "frontend: Failed to allocate memory: %s",
414                                 sdb_strerror(errno, buf, sizeof(buf)));
415                 listener_destroy(listener);
416                 return NULL;
417         }
418         listener->type = type;
419         listener->accept = NULL;
420         listener->peer = NULL;
422         if (listener_impls[type].open(listener)) {
423                 /* prints error */
424                 listener_destroy(listener);
425                 return NULL;
426         }
428         ++sock->listeners_num;
429         return listener;
430 } /* listener_create */
432 static void
433 socket_clear(sdb_fe_socket_t *sock)
435         size_t i;
437         assert(sock);
438         for (i = 0; i < sock->listeners_num; ++i)
439                 listener_destroy(sock->listeners + i);
440         if (sock->listeners)
441                 free(sock->listeners);
442         sock->listeners = NULL;
443         sock->listeners_num = 0;
444 } /* socket_clear */
446 static void
447 socket_close(sdb_fe_socket_t *sock)
449         size_t i;
451         assert(sock);
452         for (i = 0; i < sock->listeners_num; ++i)
453                 listener_close(sock->listeners + i);
454 } /* socket_close */
456 /*
457  * connection handler functions
458  */
460 static void *
461 connection_handler(void *data)
463         sdb_fe_socket_t *sock = data;
465         assert(sock);
467         while (42) {
468                 struct timespec timeout = { 0, 500000000 }; /* .5 seconds */
469                 sdb_conn_t *conn;
470                 int status;
472                 errno = 0;
473                 status = sdb_channel_select(sock->chan, /* read */ NULL, &conn,
474                                 /* write */ NULL, NULL, &timeout);
475                 if (status) {
476                         char buf[1024];
478                         if (errno == ETIMEDOUT)
479                                 continue;
480                         if (errno == EBADF) /* channel shut down */
481                                 break;
483                         sdb_log(SDB_LOG_ERR, "frontend: Failed to read from channel: %s",
484                                         sdb_strerror(errno, buf, sizeof(buf)));
485                         continue;
486                 }
488                 status = (int)sdb_connection_handle(conn);
489                 if (status <= 0) {
490                         /* error or EOF -> close connection */
491                         sdb_object_deref(SDB_OBJ(conn));
492                         continue;
493                 }
495                 /* return the connection to the main loop */
496                 if (sdb_llist_append(sock->open_connections, SDB_OBJ(conn))) {
497                         sdb_log(SDB_LOG_ERR, "frontend: Failed to re-append "
498                                         "connection %s to list of open connections",
499                                         SDB_OBJ(conn)->name);
500                 }
502                 /* pass ownership back to list; or destroy in case of an error */
503                 sdb_object_deref(SDB_OBJ(conn));
504         }
505         return NULL;
506 } /* connection_handler */
508 static int
509 connection_accept(sdb_fe_socket_t *sock, listener_t *listener)
511         sdb_object_t *obj;
512         int status;
514         obj = SDB_OBJ(sdb_connection_accept(listener->sock_fd));
515         if (! obj)
516                 return -1;
518         if (listener->accept && listener->accept(CONN(obj))) {
519                 /* accept() is expected to log an error */
520                 sdb_object_deref(obj);
521                 return -1;
522         }
523         if (listener->peer && listener->peer(CONN(obj))) {
524                 /* peer() is expected to log an error */
525                 sdb_object_deref(obj);
526                 return -1;
527         }
529         status = sdb_llist_append(sock->open_connections, obj);
530         if (status)
531                 sdb_log(SDB_LOG_ERR, "frontend: Failed to append "
532                                 "connection %s to list of open connections",
533                                 obj->name);
535         /* hand ownership over to the list; or destroy in case of an error */
536         sdb_object_deref(obj);
537         return status;
538 } /* connection_accept */
540 static int
541 socket_handle_incoming(sdb_fe_socket_t *sock,
542                 fd_set *ready, fd_set *exceptions)
544         sdb_llist_iter_t *iter;
545         size_t i;
547         for (i = 0; i < sock->listeners_num; ++i) {
548                 listener_t *listener = sock->listeners + i;
549                 if (FD_ISSET(listener->sock_fd, ready))
550                         if (connection_accept(sock, listener))
551                                 continue;
552         }
554         iter = sdb_llist_get_iter(sock->open_connections);
555         if (! iter) {
556                 sdb_log(SDB_LOG_ERR, "frontend: Failed to acquire iterator "
557                                 "for open connections");
558                 return -1;
559         }
561         while (sdb_llist_iter_has_next(iter)) {
562                 sdb_object_t *obj = sdb_llist_iter_get_next(iter);
564                 if (FD_ISSET(CONN(obj)->fd, exceptions)) {
565                         sdb_log(SDB_LOG_INFO, "Exception on fd %d",
566                                         CONN(obj)->fd);
567                         /* close the connection */
568                         sdb_llist_iter_remove_current(iter);
569                         sdb_object_deref(obj);
570                         continue;
571                 }
573                 if (FD_ISSET(CONN(obj)->fd, ready)) {
574                         sdb_llist_iter_remove_current(iter);
575                         sdb_channel_write(sock->chan, &obj);
576                 }
577         }
578         sdb_llist_iter_destroy(iter);
579         return 0;
580 } /* socket_handle_incoming */
582 /*
583  * public API
584  */
586 sdb_fe_socket_t *
587 sdb_fe_sock_create(void)
589         sdb_fe_socket_t *sock;
591         sock = calloc(1, sizeof(*sock));
592         if (! sock)
593                 return NULL;
595         sock->open_connections = sdb_llist_create();
596         if (! sock->open_connections) {
597                 sdb_fe_sock_destroy(sock);
598                 return NULL;
599         }
600         return sock;
601 } /* sdb_fe_sock_create */
603 void
604 sdb_fe_sock_destroy(sdb_fe_socket_t *sock)
606         if (! sock)
607                 return;
609         socket_clear(sock);
611         sdb_llist_destroy(sock->open_connections);
612         sock->open_connections = NULL;
613         free(sock);
614 } /* sdb_fe_sock_destroy */
616 int
617 sdb_fe_sock_add_listener(sdb_fe_socket_t *sock, const char *address)
619         listener_t *listener;
621         if ((! sock) || (! address))
622                 return -1;
624         listener = listener_create(sock, address);
625         if (! listener)
626                 return -1;
627         return 0;
628 } /* sdb_fe_sock_add_listener */
630 void
631 sdb_fe_sock_clear_listeners(sdb_fe_socket_t *sock)
633         if (! sock)
634                 return;
636         socket_clear(sock);
637 } /* sdb_fe_sock_clear_listeners */
639 int
640 sdb_fe_sock_listen_and_serve(sdb_fe_socket_t *sock, sdb_fe_loop_t *loop)
642         fd_set sockets;
643         int max_listen_fd = 0;
644         size_t i;
646         pthread_t handler_threads[loop->num_threads];
647         size_t num_threads;
649         if ((! sock) || (! sock->listeners_num) || sock->chan
650                         || (! loop) || (loop->num_threads <= 0))
651                 return -1;
653         if (! loop->do_loop)
654                 return 0;
656         FD_ZERO(&sockets);
657         for (i = 0; i < sock->listeners_num; ++i) {
658                 listener_t *listener = sock->listeners + i;
660                 if (listener_listen(listener)) {
661                         socket_close(sock);
662                         return -1;
663                 }
665                 FD_SET(listener->sock_fd, &sockets);
666                 if (listener->sock_fd > max_listen_fd)
667                         max_listen_fd = listener->sock_fd;
668         }
670         sock->chan = sdb_channel_create(1024, sizeof(sdb_conn_t *));
671         if (! sock->chan) {
672                 socket_close(sock);
673                 return -1;
674         }
676         sdb_log(SDB_LOG_INFO, "frontend: Starting %zu connection "
677                         "handler thread%s managing %zu listener%s",
678                         loop->num_threads, loop->num_threads == 1 ? "" : "s",
679                         sock->listeners_num, sock->listeners_num == 1 ? "" : "s");
681         num_threads = loop->num_threads;
682         memset(&handler_threads, 0, sizeof(handler_threads));
683         for (i = 0; i < num_threads; ++i) {
684                 errno = 0;
685                 if (pthread_create(&handler_threads[i], /* attr = */ NULL,
686                                         connection_handler, /* arg = */ sock)) {
687                         char errbuf[1024];
688                         sdb_log(SDB_LOG_ERR, "frontend: Failed to create "
689                                         "connection handler thread: %s",
690                                         sdb_strerror(errno, errbuf, sizeof(errbuf)));
691                         num_threads = i;
692                         break;
693                 }
694         }
696         while (loop->do_loop && num_threads) {
697                 struct timeval timeout = { 1, 0 }; /* one second */
698                 sdb_llist_iter_t *iter;
700                 int max_fd = max_listen_fd;
701                 fd_set ready;
702                 fd_set exceptions;
703                 int n;
705                 FD_ZERO(&ready);
706                 FD_ZERO(&exceptions);
708                 ready = sockets;
710                 iter = sdb_llist_get_iter(sock->open_connections);
711                 if (! iter) {
712                         sdb_log(SDB_LOG_ERR, "frontend: Failed to acquire iterator "
713                                         "for open connections");
714                         break;
715                 }
717                 while (sdb_llist_iter_has_next(iter)) {
718                         sdb_object_t *obj = sdb_llist_iter_get_next(iter);
720                         if (CONN(obj)->fd < 0) {
721                                 sdb_llist_iter_remove_current(iter);
722                                 sdb_object_deref(obj);
723                                 continue;
724                         }
726                         FD_SET(CONN(obj)->fd, &ready);
727                         FD_SET(CONN(obj)->fd, &exceptions);
729                         if (CONN(obj)->fd > max_fd)
730                                 max_fd = CONN(obj)->fd;
731                 }
732                 sdb_llist_iter_destroy(iter);
734                 errno = 0;
735                 n = select(max_fd + 1, &ready, NULL, &exceptions, &timeout);
736                 if (n < 0) {
737                         char buf[1024];
739                         if (errno == EINTR)
740                                 continue;
742                         sdb_log(SDB_LOG_ERR, "frontend: Failed to monitor sockets: %s",
743                                         sdb_strerror(errno, buf, sizeof(buf)));
744                         break;
745                 }
746                 else if (! n)
747                         continue;
749                 /* handle new and open connections */
750                 if (socket_handle_incoming(sock, &ready, &exceptions))
751                         break;
752         }
754         socket_close(sock);
756         sdb_log(SDB_LOG_INFO, "frontend: Waiting for connection handler threads "
757                         "to terminate");
758         if (! sdb_channel_shutdown(sock->chan))
759                 for (i = 0; i < num_threads; ++i)
760                         pthread_join(handler_threads[i], NULL);
761         /* else: we tried our best; let the operating system clean up */
763         sdb_channel_destroy(sock->chan);
764         sock->chan = NULL;
766         if (! num_threads)
767                 return -1;
768         return 0;
769 } /* sdb_fe_sock_listen_and_server */
771 /* vim: set tw=78 sw=4 ts=4 noexpandtab : */