]> git.tuebingen.mpg.de Git - paraslash.git/commitdiff
para_server/para_audiod: new option --group
authorAndre <maan@p133.(none)>
Sun, 9 Apr 2006 17:30:21 +0000 (19:30 +0200)
committerAndre <maan@p133.(none)>
Sun, 9 Apr 2006 17:30:21 +0000 (19:30 +0200)
Used to set the GID at startup. This patch also reformats
server.ggo and audiod.ggo to make them more readable.

audiod.c
audiod.ggo
daemon.c
daemon.h
server.c
server.ggo

index 72da56d14583444f55e2d55175e4249cccc623a1..7029ea9116f9c115a5c4d5d41599fcd414ea930e 100644 (file)
--- a/audiod.c
+++ b/audiod.c
@@ -268,17 +268,7 @@ void para_log(int ll, const char* fmt,...)
 
        if (ll < conf.loglevel_arg)
                return;
-       if (!logfile && conf.logfile_given)
-               logfile = open_log(conf.logfile_arg);
-       if (!logfile && conf.daemon_given)
-               return;
-       if (!logfile) {
-               if (ll < WARNING)
-                       outfd = stdout;
-               else
-                       outfd = stderr;
-       } else
-               outfd = logfile;
+       outfd = logfile? logfile : stderr;
        time(&t1);
        tm = localtime(&t1);
        strftime(str, MAXLINE, "%b %d %H:%M:%S", tm);
@@ -1656,19 +1646,20 @@ int __noreturn main(int argc, char *argv[])
        valid_fd_012();
        hostname = para_hostname();
        cmdline_parser(argc, argv, &conf);
-       para_drop_privileges(conf.user_arg);
+       para_drop_privileges(conf.user_arg, conf.group_arg);
        cf = configfile_exists();
        if (cf) {
                if (cmdline_parser_configfile(cf, &conf, 0, 0, 0)) {
-                       fprintf(stderr, "parse error in config file\n");
+                       PARA_EMERG_LOG("%s", "parse error in config file\n");
                        exit(EXIT_FAILURE);
                }
        }
+       if (conf.logfile_given)
+               logfile = open_log(conf.logfile_arg);
        log_welcome("para_audiod", conf.loglevel_arg);
        i = init_stream_io();
        if (i < 0) {
-               fprintf(stderr, "init stream io error: %s\n",
-                       PARA_STRERROR(-i));
+               PARA_EMERG_LOG("init stream io error: %s\n", PARA_STRERROR(-i));
                exit(EXIT_FAILURE);
        }
        server_uptime(UPTIME_SET);
index 8b0249c11a2c6a8d49562b01e0d9e4f056c518c2..5e89e6a40aafd839966157a37431f508ac6d5526 100644 (file)
 section "general options"
-option "user" u "run as user 'name'. Read the output of 'para_server -h' for a detailed information on this option." string typestr="name" optional
-option "loglevel" l "set loglevel (0-6)" int typestr="level" default="4" optional
-option "daemon" d "run as background daemon" flag off
-option "force" F "force startup even if socket exits" flag off
-option "logfile" L "(default=stdout/stderr)" string typestr="filename" optional
-option "mode" m "mode to use on startup (on/off/sb)" string typestr="mode" default="on" optional
-option "socket" s "well-known socket to listen on (default=/var/paraslash/audiod_sock.<host_name>)" string typestr="filename" optional
+#~~~~~~~~~~~~~~~~~~~~~~~~
+
+option "user" u
+#~~~~~~~~~~~~~~
+
+"run as user 'name'. Read the output of
+'para_server -h' for more information on this
+option."
+
+       string typestr="name"
+       optional
+
+option "group" g
+#~~~~~~~~~~~~~~~
+
+"set group id to 'group'. Read the output of
+'para_server -h' for more information on this
+option."
+
+       string typestr="group"
+       optional
+
+option "loglevel" l
+#~~~~~~~~~~~~~~~~~~
+
+"set loglevel (0-6)"
+
+       int typestr="level"
+       default="4"
+       optional
+
+option "daemon" d
+#~~~~~~~~~~~~~~~~
+
+"run as background daemon"
+
+       flag off
+
+
+option "force" F
+#~~~~~~~~~~~~~~~
+
+"force startup even if
+socket exists"
+
+       flag off
+
+option "logfile" L
+#~~~~~~~~~~~~~~~~~
+
+"(default=stdout/stderr)"
+
+       string typestr="filename"
+       optional
+
+option "mode" m
+#~~~~~~~~~~~~~~
+
+"mode to use on startup (on/off/sb)"
+
+       string typestr="mode"
+       default="on"
+       optional
+
+option "socket" s
+
+"well-known socket to listen on
+(default=/var/paraslash/audiod_sock.<host_name>)"
+
+       string typestr="filename"
+       optional
 
 option "user_allow" -
+#~~~~~~~~~~~~~~~~~~~~
 
 "allow this user to connect to para_audiod.
 May be specified multiple times. If not
-specified at all, allow all users to
+specified at all, all users are allowed to
 connect."
 
-int typestr="uid" default="-1" optional multiple
+       int typestr="uid"
+       default="-1"
+       optional
+       multiple
 
-section "stream i/o options."
 
-#################
 
-option "receiver" r "Select receiver.
 
-May be given multiple times, once for each
-supported audio format. receiver_spec
-consists of an audio format and the receiver
-name, separated by a colon, and any options
-for that receiver, seperated by whitespace.
-If any receiver options are present, the
-whole receiver argument must be quoted.
+section "stream i/o options."
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+option "receiver" r
+#~~~~~~~~~~~~~~~~~~
+
+"Select receiver.  May be given multiple
+times, once for each supported audio format.
+'receiver_spec' consists of an audio format and
+the receiver name, separated by a colon, and
+any options for that receiver, seperated by
+whitespace.  If any receiver options are
+present, the whole receiver argument must be
+quoted.
 
 Example:
 
 -r 'mp3:http -i www.paraslash.org -p 8009'
 "
 
-string typestr="receiver_spec" default="http" optional multiple
+       string typestr="receiver_spec"
+       default="http"
+       optional
+       multiple
+
 
-#################
-option "no_default_filters" D "Configure filters manually.
+option "no_default_filters" D
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-If (and only if) this option is set, the
---filter options take effect.  Otherwise, the
-compiled-in default filters mp3dec (oggdec)
-and wav are activated for mp3 (ogg) streams."
+"Configure filters manually. If (and only
+if) this option is set, the --filter options
+take effect.  Otherwise, the compiled-in
+default filters mp3dec (oggdec) and wav are
+activated for mp3 (ogg) streams."
 
-flag off
-#################
+       flag off
 
-option "filter" f "Select filter(s) manually.
 
-May be given multiple times. filter_spec
-consists of an audio format, the name of the
-filter, and any options for that filter.
+option "filter" f
+#~~~~~~~~~~~~~~~~
+
+"Select filter(s) manually.  May be given
+multiple times. filter_spec consists of an
+audio format, the name of the filter, and any
+options for that filter.
 
 Examples:
-       -f mp3:mp3dec
+       -f 'mp3:mp3dec'
        -f 'mp3:compress --inertia 5 --damp 2'
 
 Note that these options are ignored by default,
@@ -63,9 +142,9 @@ see --no_default_filters."
 
 string typestr="filter_spec" optional multiple
 
-#################
 
 option "stream_write_cmd" w
+#~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 "Specify stream writer.
 
@@ -77,11 +156,13 @@ Each occurence of START_TIME() gets replaced
 at runtime by the stream start time announced
 by para_server, plus any offsets."
 
-string typestr="format:command" optional multiple
+       string typestr="format:command"
+       optional
+       multiple
 
-#################
 
 option "stream_delay" -
+#~~~~~~~~~~~~~~~~~~~~~~
 
 "Time to add to para_server's start_time.
 
@@ -90,12 +171,16 @@ stream start time for stream_write_cmd if
 START_TIME() was given. Useful for
 syncronizing the audio output of clients."
 
-int typestr="milliseconds" default="200" no
-
-#################
+       int typestr="milliseconds"
+       default="200"
+       optional
 
 option "stream_timeout" -
+#~~~~~~~~~~~~~~~~~~~~~~~~
 
-"Deactivate slot if idle for that many seconds"
+"Deactivate slot if idle for that many
+seconds"
 
-int typestr="seconds" default="30" optional
+       int typestr="seconds"
+       default="30"
+       optional
index 616fd2c2f9adf7f9afb927ce840a00a0ae01aa97..d3a43b9a2de32fe50f2a42009cbcfb7830972985 100644 (file)
--- a/daemon.c
+++ b/daemon.c
 #include "para.h"
 #include "daemon.h"
 #include <pwd.h>
+
+/* getgrnam() */
+#include <sys/types.h>
+#include <grp.h>
+
 #include "string.h"
 
 /**
@@ -61,15 +66,17 @@ void daemon_init(void)
  *
  * Either calls exit() or returns a valid file handle.
  */
-/* might be called from para_log, so we must not use para_log */
 FILE *open_log(const char *logfile_name)
 {
        FILE *logfile;
 
        if (!logfile_name)
                return NULL;
-       if (!(logfile = fopen(logfile_name, "a")))
+       if (!(logfile = fopen(logfile_name, "a"))) {
+               PARA_EMERG_LOG("can not open %s, uid: %d\n", logfile_name,
+                       getuid());
                exit(EXIT_FAILURE);
+       }
        setlinebuf(logfile);
        return logfile;
 }
@@ -101,20 +108,33 @@ void log_welcome(const char *whoami, int loglevel)
 /**
  * give up superuser privileges
  *
- * This function returns immediately if not invoked with EUID zero.  Otherwise,
- * it tries to obtain the UID for the user specified by \a username and exits
- * if this user was not found. On success, effective and real UID and the saved
- * set-user-ID are all set to the UID of \a username.
+ * This function returns immediately if not invoked with EUID zero. Otherwise,
+ * it tries to obtain the GID of \a groupname and the UID of \a username.  On
+ * success, effective and real GID/UID and the saved set-group-ID/set-user-ID
+ * are all set accordingly. On errors, an appropriate message is logged and exit()
+ * is called to terminate the process.
  *
- * \sa getpwnam(3), getuid(2), setuid(2)
+ * \sa getpwnam(3), getuid(2), setuid(2), getgrnam(2), setgid(2)
  */
-void para_drop_privileges(const char *username)
+void para_drop_privileges(const char *username, const char *groupname)
 {
        struct passwd *p;
        char *tmp;
 
        if (geteuid())
                return;
+       if (groupname) {
+               struct group *g = getgrnam(groupname);
+               if (!g) {
+                       PARA_EMERG_LOG("failed to get group %s\n", groupname);
+                       exit(EXIT_FAILURE);
+               }
+               if (setgid(g->gr_gid) < 0) {
+                       PARA_EMERG_LOG("failed to set group id %d (%s)\n",
+                               g->gr_gid, strerror(errno));
+                       exit(EXIT_FAILURE);
+               }
+       }
        if (!username) {
                PARA_EMERG_LOG("%s", "root privileges, but no user option given\n");
                exit(EXIT_FAILURE);
@@ -126,7 +146,7 @@ void para_drop_privileges(const char *username)
                PARA_EMERG_LOG("%s", "no such user\n");
                exit(EXIT_FAILURE);
        }
-       PARA_NOTICE_LOG("%s", "dropping root privileges\n");
+       PARA_INFO_LOG("%s", "dropping root privileges\n");
        setuid(p->pw_uid);
        PARA_DEBUG_LOG("uid: %d, euid: %d\n", getuid(), geteuid());
        setuid(p->pw_uid);
@@ -170,4 +190,3 @@ __malloc char *uptime_str(void)
        return make_message("%li:%02li:%02li", t / 86400,
                (t / 3600) % 24, (t / 60) % 60);
 }
-
index 11035b50b9dc2636b7a60d717907c2e91be7c560..e1b8149aaba8b83af3476a6ba988bcd4722a42d5 100644 (file)
--- a/daemon.h
+++ b/daemon.h
@@ -5,7 +5,7 @@ void daemon_init(void);
 FILE *open_log(const char *logfile_name);
 void close_log(FILE* logfile);
 void log_welcome(const char *whoami, int loglevel);
-void para_drop_privileges(const char *username);
+void para_drop_privileges(const char *username, const char *groupname);
 /** used for server_uptime() */
 enum uptime {UPTIME_SET, UPTIME_GET};
 time_t server_uptime(enum uptime set_or_get);
index 8f0500965ca1461dbe4d661ff4086ed95793cad9..bcb43a20e610d275294447b4fc4728f8caaece78 100644 (file)
--- a/server.c
+++ b/server.c
@@ -135,15 +135,7 @@ void para_log(int ll, const char* fmt,...)
 
        if (ll < conf.loglevel_arg)
                return;
-       if (!logfile) {
-               if (ll < WARNING)
-                       outfd = stdout;
-               else
-                       outfd = stderr;
-       } else
-               outfd = logfile;
-       if (conf.daemon_given && !logfile)
-               return;
+       outfd = logfile? logfile : stderr;
        time(&t1);
        tm = localtime(&t1);
        strftime(str, MAXLINE, "%b %d %H:%M:%S", tm);
@@ -348,7 +340,7 @@ static unsigned do_inits(int argc, char **argv)
        init_random_seed();
        /* parse command line options */
        cmdline_parser(argc, argv, &conf);
-       para_drop_privileges(conf.user_arg);
+       para_drop_privileges(conf.user_arg, conf.group_arg);
        /* parse config file, open log and set defaults */
        parse_config(0);
        log_welcome("para_server", conf.loglevel_arg);
index dd4e91ac476714581ad47f2df47f276dd8501d1e..c524009f937d2ab09ec7369b3d8e5524a4577896 100644 (file)
 section "General options"
-option "loglevel" l "set loglevel (0-6)" int typestr="level" default="4" optional
-option "port" p "port to listen on" int typestr="portnumber" default="2990" optional
-option "daemon" d "run as background daemon" flag off
-option "user" u "run as user 'name'. para_server does not need any special privileges. If started as root (EUID == 0) this option must be given at the command line (not in the configuration file) so that para_server can drop the root privileges right after parsing the command line options, but before parsing the configuration file. In this case, real/effective/saved UID are all set to the UID of 'username'. As the configuration file is read afterwards, those options that have a default value depending on the UID (e.g. the home directory for the configuration file) are computed by using the uid of 'username'. This option has no effect if para_server ist started as a non-root user (i.e.  EUID != 0)" string typestr="name" optional
+#~~~~~~~~~~~~~~~~~~~~~~~~
+
+option "loglevel" l
+#~~~~~~~~~~~~~~~~~~
+
+"set loglevel (0-6)"
+
+       int typestr="level"
+       default="4"
+       optional
+
+option "port" p
+#~~~~~~~~~~~~~~
+
+"listening port"
+
+       int typestr="portnumber"
+       default="2990"
+       optional
+
+option "daemon" d
+#~~~~~~~~~~~~~~~~
+
+"run as background daemon"
+
+       flag off
+
+option "user" u
+#~~~~~~~~~~~~~~
+
+"run as user 'name'. para_server does not
+need any special privileges. If started as
+root (EUID == 0) this option must be given at
+the command line (not in the configuration
+file) so that para_server can drop the root
+privileges right after parsing the command
+line options, but before parsing the
+configuration file. In this case,
+real/effective/saved UID are all set to the
+UID of 'username'. As the configuration file
+is read afterwards, those options that have a
+default value depending on the UID (e.g. the
+directory for the configuration file) are
+computed by using the uid of 'username'.
+This option has no effect if para_server is
+started as a non-root user (i.e.  EUID != 0)"
+
+       string typestr="name"
+       optional
+
+
+option "group" g
+#~~~~~~~~~~~~~~~
+
+"set group id to according to 'group'. This
+option is silently ignored if EUID != 0.
+Otherwise, real/effective GID and the saved
+set-group ID are all set to the GID given by
+'group'. Must not be given in the config file."
+
+       string typestr="groupname"
+       optional
+
+
 
 section "Configuration files"
-option "logfile" L "(default=stdout/stderr)" string typestr="filename" optional
-option "config_file" c "(default='~/.paraslash/server.conf'" string typestr="filename" optional
-option "user_list" - "(default='~/.paraslash/server.users')" string typestr="filename" optional
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+option "logfile" L
+#~~~~~~~~~~~~~~~~~
+
+"(default=stdout/stderr)"
+
+       string typestr="filename"
+       optional
+
+option "config_file" c
+#~~~~~~~~~~~~~~~~~~~~~
+
+"(default='~/.paraslash/server.conf'"
+
+       string typestr="filename"
+       optional
+
+option "user_list" -
+#~~~~~~~~~~~~~~~~~~~
+
+"(default='~/.paraslash/server.users')"
+
+       string typestr="filename"
+       optional
+
+
 
 section "audio file sender"
-option "autoplay" a "start playing on startup" flag off
-option "announce_time" A "Delay betweeen announcing the stream and sending data" int typestr="milliseconds" default="300" optional
-option "selector" S "(default=random)" string typestr="name" optional
+#~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+option "autoplay" a
+#~~~~~~~~~~~~~~~~~~
+
+"start playing on startup"
+
+       flag off
+
+option "announce_time" A
+#~~~~~~~~~~~~~~~~~~~~~~~
+
+"Delay betweeen announcing the stream and sending data"
+
+       int typestr="milliseconds"
+       default="300"
+       optional
+
+
+option "selector" S
+#~~~~~~~~~~~~~~~~~~
+
+"(default=random)"
+
+       string typestr="name"
+       optional
+
+
 
 section "mysql selector:"
-option "mysql_host" - "mysql server" string default="localhost" optional
-option "mysql_port" - "where mysql is listening" int default="3306" optional
-option "mysql_user" - "default value: username from /etc/passwd" string optional
-option "mysql_passwd" - "(required)" string optional
-option "mysql_database" - "name of mysql database" string default="paraslash" optional
-option "mysql_audio_file_dir"  - "dir to search for audio files (required)" string optional
-option "mysql_default_score" - "scoring rule to use if stream definition does not contain explicit score definition" string default="(LASTPLAYED() / 1440 - 1000 / (LASTPLAYED() + 1) - sqrt(NUMPLAYED()))" optional
+#~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+option "mysql_host" -
+#~~~~~~~~~~~~~~~~~~~~
+
+"mysql server"
+
+       string typestr="ip or hostname"
+       default="localhost"
+       optional
+
+option "mysql_port" -
+#~~~~~~~~~~~~~~~~~~~~
+
+"where mysql is listening"
+
+       int typestr="portnumber"
+       default="3306"
+       optional
+
+
+option "mysql_user" -
+#~~~~~~~~~~~~~~~~~~~~
+
+"default value: username from /etc/passwd"
+
+       string typestr="username"
+       optional
+
+option "mysql_passwd" -
+#~~~~~~~~~~~~~~~~~~~~~~
+
+"(required)"
+
+       string
+       optional
+
+option "mysql_database" -
+#~~~~~~~~~~~~~~~~~~~~~~~~
+
+"name of mysql database"
+
+       string
+       default="paraslash"
+       optional
+
+option "mysql_audio_file_dir"  -
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+"dir to search for audio files (required)"
+
+       string typestr="dirname"
+       optional
+
+option "mysql_default_score" -
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+"scoring rule to use if stream definition
+does not contain explicit score definition"
+
+       string
+       default="(LASTPLAYED() / 1440 - 1000 / (LASTPLAYED() + 1) - sqrt(NUMPLAYED()))"
+       optional
+
+
+
 
 section "random selector"
-option "random_dir" - "dir to search for audio files" string default="/home/music" optional
+#~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+option "random_dir" -
+#~~~~~~~~~~~~~~~~~~~~
+
+"dir to search for audio files"
+       string typestr="dirname"
+       default="/home/music"
+       optional
+
+
+
 
 section "http sender"
-option "http_port" - "tcp port for http streaming" int typestr="portnumber" default="8000" optional
-option "http_default_deny" - "deny connections from hosts which are not explicitly allowed" flag off
-option "http_access" - "Add given host/network to access control list (whitelist if http_default_deny was given, blacklist otherwise) before opening the tcp port. This option can be given multiple times. Example: '192.168.0.0/24' whitelists/blacklists the 256 hosts 192.168.0.x" string typestr="a.b.c.d/n" optional multiple
-option "http_no_autostart" - "do not open tcp port on server startup" flag off
-option "http_max_clients" - "maximal simultaneous connections, non-positive value means unlimited" int typestr="number" default="-1" optional
+#~~~~~~~~~~~~~~~~~~~~
+
+
+option "http_port" -
+#~~~~~~~~~~~~~~~~~~~
+
+"tcp port for http streaming"
+
+       int typestr="portnumber"
+       default="8000"
+       optional
+
+option "http_default_deny" -
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+"deny connections from hosts which are not
+explicitly allowed"
+
+       flag off
+
+option "http_access" -
+#~~~~~~~~~~~~~~~~~~~~~
+
+"Add given host/network to access control
+list (whitelist if http_default_deny was
+given, blacklist otherwise) before opening
+the tcp port. This option can be given
+multiple times. Example: '192.168.0.0/24'
+whitelists/blacklists the 256 hosts
+192.168.0.x"
+
+       string typestr="a.b.c.d/n"
+       optional
+       multiple
+
+option "http_no_autostart" -
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+"do not open tcp port on server startup"
+
+       flag off
+
+option "http_max_clients" -
+#~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+"maximal simultaneous connections,
+non-positive value means unlimited"
+
+       int typestr="number"
+       default="-1"
+       optional
+
+
+
 
 section "dccp sender"
-option "dccp_port" - "port for dccp streaming" int typestr="portnumber" default="5001" optional
+#~~~~~~~~~~~~~~~~~~~~
+
+
+option "dccp_port" -
+#~~~~~~~~~~~~~~~~~~~
+
+"port for dccp streaming"
+
+       int typestr="portnumber"
+       default="5001"
+       optional
+
+
+
 
 section "ortp sender"
-option "ortp_target" - "Add given host/port to the list of targets. This option can be given multiple times. Example: '224.0.1.38:1500' instructs the ortp sender to send to udp port 1500 on host 224.0.1.38 (unassigned ip in the Local Network Control Block 224.0.0/24). This is useful for LAN-streaming." string typestr="a.b.c.d:p" optional multiple
-option "ortp_no_autostart" - "do not start to send automatically" flag off
-option "ortp_default_port" - "default udp port if not specified" int typestr="portnumber" default="1500" optional
-option "ortp_header_interval" H "time between extra header sends" int typestr="milliseconds" default="2000" optional
-option "ortp_jitter_compensation" j "non-zero values enable ortp's adaptive jitter compensation" int typestr="milliseconds" default="400" optional
+#~~~~~~~~~~~~~~~~~~~~
+
+option "ortp_target" -
+#~~~~~~~~~~~~~~~~~~~~~
+
+"Add given host/port to the list of targets.
+This option can be given multiple times.
+Example: '224.0.1.38:1500' instructs the ortp
+sender to send to udp port 1500 on host
+224.0.1.38 (unassigned ip in the Local
+Network Control Block 224.0.0/24). This is
+useful for LAN-streaming."
+
+       string typestr="a.b.c.d:p"
+       optional
+       multiple
+
+option "ortp_no_autostart" -
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+"do not start to send automatically"
+
+       flag off
+
+option "ortp_default_port" -
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+"default udp port if not specified"
+
+       int typestr="portnumber"
+       default="1500"
+       optional
+
+option "ortp_header_interval" H
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+"time between extra header sends"
+
+       int typestr="milliseconds"
+       default="2000"
+       optional
+
+option "ortp_jitter_compensation" j
+#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+"non-zero values enable ortp's adaptive
+jitter compensation"
+
+       int typestr="milliseconds"
+       default="400"
+       optional