flesh out check for reasonable-looking service names
[monkeysphere.git] / src / share / common
index ea872ba17abe39ec1a8a721876b6bf74d42ab069..0a7fe87c862585928b7cb6b066b482eb044300aa 100644 (file)
@@ -147,8 +147,8 @@ lock() {
     local action="$1"
     local file="$2"
 
-    if ! ( which lockfile-create >/dev/null 2>/dev/null ) ; then
-       if ! ( which lockfile >/dev/null ); then
+    if ! ( type lockfile-create &>/dev/null ) ; then
+       if ! ( type lockfile &>/dev/null ); then
            failure "Neither lockfile-create nor lockfile are in the path!"
        fi
        use_lockfileprogs=
@@ -197,7 +197,7 @@ advance_date() {
     local shortunits
 
     # try things the GNU way first 
-    if date -d "$number $longunits" "$format" >/dev/null 2>&1; then
+    if date -d "$number $longunits" "$format" &>/dev/null; then
        date -d "$number $longunits" "$format"
     else
        # otherwise, convert to (a limited version of) BSD date syntax:
@@ -252,7 +252,13 @@ check_capability() {
 
 # hash of a file
 file_hash() {
-    md5sum "$1" 2> /dev/null
+    if type md5sum &>/dev/null ; then
+       md5sum "$1"
+    elif type md5 &>/dev/null ; then
+       md5 "$1"
+    else
+       failure "Neither md5sum nor md5 are in the path!"
+    fi
 }
 
 # convert escaped characters in pipeline from gpg output back into
@@ -275,7 +281,7 @@ get_gpg_expiration() {
 
     keyExpire="$1"
 
-    if [ -z "$keyExpire" -a "$PROMPT" = 'true' ]; then
+    if [ -z "$keyExpire" -a "$PROMPT" != 'false' ]; then
        cat >&2 <<EOF
 Please specify how long the key should be valid.
          0 = key does not expire
@@ -285,7 +291,8 @@ Please specify how long the key should be valid.
       <n>y = key expires in n years
 EOF
        while [ -z "$keyExpire" ] ; do
-           read -p "Key is valid for? (0) " keyExpire
+           printf "Key is valid for? (0) " >&2
+           read keyExpire
            if ! test_gpg_expire ${keyExpire:=0} ; then
                echo "invalid value" >&2
                unset keyExpire
@@ -303,7 +310,9 @@ passphrase_prompt() {
     local fifo="$2"
     local PASS
 
-    if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
+    if [ "$DISPLAY" ] && type "${SSH_ASKPASS:-ssh-askpass}" >/dev/null 2>/dev/null; then
+       printf 'Launching "%s"\n' "${SSH_ASKPASS:-ssh-askpass}" | log info
+       printf '(with prompt "%s")\n' "$prompt" | log debug
        "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
     else
        read -s -p "$prompt" PASS
@@ -331,7 +340,7 @@ remove_line() {
     fi
 
     # if the string is in the file...
-    if grep -q -F "$string" "$file" 2> /dev/null ; then
+    if grep -q -F "$string" "$file" 2>/dev/null ; then
        tempfile=$(mktemp "${file}.XXXXXXX") || \
            failure "Unable to make temp file '${file}.XXXXXXX'"
        
@@ -382,7 +391,7 @@ translate_ssh_variables() {
     path="$2"
 
     # get the user's home directory
-    userHome=$(getent passwd "$uname" | cut -d: -f6)
+    userHome=$(get_homedir "$uname")
 
     # translate '%u' to user name
     path=${path/\%u/"$uname"}
@@ -402,52 +411,63 @@ test_gpg_expire() {
 check_key_file_permissions() {
     local uname
     local path
-    local stat
-    local access
-    local gAccess
-    local oAccess
-
-    # function to check that the given permission corresponds to writability
-    is_write() {
-       [ "$1" = "w" ]
-    }
 
     uname="$1"
     path="$2"
 
+    if [ "$STRICT_MODES" = 'false' ] ; then
+       log debug "skipping path permission check for '$path' because STRICT_MODES is false..."
+       return 0
+    fi
     log debug "checking path permission '$path'..."
+    "${SYSSHAREDIR}/checkperms" "$uname" "$path"
+}
 
-    # return 255 if cannot stat file
-    if ! stat=$(ls -ld "$path" 2>/dev/null) ; then
-        log error "could not stat path '$path'."
-       return 255
+# return a list of all users on the system
+list_users() {
+    if type getent &>/dev/null ; then
+       # for linux and FreeBSD systems
+       getent passwd | cut -d: -f1
+    elif type dscl &>/dev/null ; then
+       # for Darwin systems
+       dscl localhost -list /Search/Users
+    else
+       failure "Neither getent or dscl is in the path!  Could not determine list of users."
     fi
+}
 
-    owner=$(echo "$stat" | awk '{ print $3 }')
-    gAccess=$(echo "$stat" | cut -c6)
-    oAccess=$(echo "$stat" | cut -c9)
-
-    # return 1 if path has invalid owner
-    if [ "$owner" != "$uname" -a "$owner" != 'root' ] ; then
-       log error "improper ownership on path '$path':"
-       log error " $owner != ($uname|root)"
-       return 1
+# take one argument, a service name.  in response, print a series of
+# lines, each with a unique numeric port number that might be
+# associated with that service name.  (e.g. in: "https", out: "443")
+# if nothing is found, print nothing, and return 0.
+# 
+# return 1 if there was an error in the search somehow
+get_port_for_service() {
+
+    [[ "$1" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] || \
+        failure $(printf "This is not a valid service name: '%s'" "$1")
+    if type getent &>/dev/null ; then
+        # for linux and FreeBSD systems (getent returns 2 if not found, 0 on success, 1 or 3 on various failures)
+        (getent services "$service" || if [ "$?" -eq 2 ] ; then true ; else false; fi) | awk '{ print $2 }' | cut -f1 -d/ | sort -u
+    elif [ -r /etc/services ] ; then
+        # fall back to /etc/services for systems that don't have getent (MacOS?)
+        # FIXME: doesn't handle aliases like "null" (or "http"?), which don't show up at the beginning of the line.
+        awk $(printf '/^%s[[:space:]]/{ print $2 }' "$1") /etc/services | cut -f1 -d/ | sort -u
+    else
+        return 1
     fi
+}
 
-    # return 2 if path has group or other writability
-    if is_write "$gAccess" || is_write "$oAccess" ; then
-       log error "improper group or other writability on path '$path':"
-       log error " group: $gAccess, other: $oAcess"
-       return 2
-    fi
+# return the path to the home directory of a user
+get_homedir() {
+    local uname=${1:-`whoami`}
+    eval "echo ~${uname}"
+}
 
-    # return zero if all clear, or go to next path
-    if [ "$path" = '/' ] ; then
-       log debug "path ok."
-       return 0
-    else
-       check_key_file_permissions "$uname" $(dirname "$path")
-    fi
+# return the primary group of a user
+get_primary_group() {
+    local uname=${1:-`whoami`}
+    groups "$uname" | sed 's/^..* : //' | awk '{ print $1 }'
 }
 
 ### CONVERSION UTILITIES
@@ -458,20 +478,29 @@ gpg2ssh() {
     
     keyID="$1"
 
-    gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
+    gpg --export "$keyID" | openpgp2ssh "$keyID" 2>/dev/null
 }
 
 # output known_hosts line from ssh key
 ssh2known_hosts() {
     local host
+    local port
     local key
 
-    host="$1"
+    # FIXME this does not properly deal with IPv6 hosts using the
+    # standard port (because it's unclear whether their final
+    # colon-delimited address section is a port number or an address
+    # string)
+    host=${1%:*}
+    port=${1##*:}
     key="$2"
 
-    echo -n "$host "
-    echo -n "$key" | tr -d '\n'
-    echo " MonkeySphere${DATE}"
+    # specify the host and port properly for new ssh known_hosts
+    # format
+    if [ "$port" != "$host" ] ; then
+       host="[${host}]:${port}"
+    fi
+    printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
 }
 
 # output authorized_keys line from ssh key
@@ -482,45 +511,56 @@ ssh2authorized_keys() {
     userID="$1"
     key="$2"
 
-    echo -n "$key" | tr -d '\n'
-    echo " MonkeySphere${DATE} ${userID}"
+    printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
 }
 
 # convert key from gpg to ssh known_hosts format
 gpg2known_hosts() {
     local host
     local keyID
+    local key
 
     host="$1"
     keyID="$2"
 
+    key=$(gpg2ssh "$keyID")
+
     # NOTE: it seems that ssh-keygen -R removes all comment fields from
     # all lines in the known_hosts file.  why?
     # NOTE: just in case, the COMMENT can be matched with the
     # following regexp:
     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
-    echo -n "$host "
-    gpg2ssh "$keyID" | tr -d '\n'
-    echo " MonkeySphere${DATE}"
+    printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
 }
 
 # convert key from gpg to ssh authorized_keys format
 gpg2authorized_keys() {
     local userID
     local keyID
+    local key
 
     userID="$1"
     keyID="$2"
 
+    key=$(gpg2ssh "$keyID")
+
     # NOTE: just in case, the COMMENT can be matched with the
     # following regexp:
     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
-    gpg2ssh "$keyID" | tr -d '\n'
-    echo " MonkeySphere${DATE} ${userID}"
+    printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
 }
 
 ### GPG UTILITIES
 
+# script to determine if gpg version is equal to or greater than specified version
+is_gpg_version_greater_equal() {
+    local gpgVersion=$(gpg --version | head -1 | awk '{ print $3 }')
+    local latest=$(printf '%s\n%s\n' "$1" "$gpgVersion" \
+       | tr '.' ' ' | sort -g -k1 -k2 -k3 \
+       | tail -1 | tr ' ' '.')
+    [[ "$gpgVersion" == "$latest" ]]
+}
+
 # retrieve all keys with given user id from keyserver
 # FIXME: need to figure out how to retrieve all matching keys
 # (not just first N (5 in this case))
@@ -538,7 +578,7 @@ gpg_fetch_userid() {
     echo 1,2,3,4,5 | \
        gpg --quiet --batch --with-colons \
        --command-fd 0 --keyserver "$KEYSERVER" \
-       --search ="$userID" > /dev/null 2>&1
+       --search ="$userID" &>/dev/null
     returnCode="$?"
 
     return "$returnCode"
@@ -550,7 +590,7 @@ gpg_fetch_userid() {
 # userid and key policy checking
 # the following checks policy on the returned keys
 # - checks that full key has appropriate valididy (u|f)
-# - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
+# - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
 # - checks that requested user ID has appropriate validity
 # (see /usr/share/doc/gnupg/DETAILS.gz)
 # output is one line for every found key, in the following format:
@@ -562,8 +602,6 @@ gpg_fetch_userid() {
 #
 # all log output must go to stderr, as stdout is used to pass the
 # flag:sshKey to the calling function.
-#
-# expects global variable: "MODE"
 process_user_id() {
     local returnCode=0
     local userID
@@ -584,11 +622,7 @@ process_user_id() {
     userID="$1"
 
     # set the required key capability based on the mode
-    if [ "$MODE" = 'known_hosts' ] ; then
-       requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
-    elif [ "$MODE" = 'authorized_keys' ] ; then
-       requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
-    fi
+    requiredCapability=${REQUIRED_KEY_CAPABILITY:="a"}
     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
 
     # fetch the user ID if necessary/requested
@@ -749,6 +783,59 @@ process_user_id() {
     # being processed in the key files over "bad" keys (key flag '1')
 }
 
+# output all valid keys for specified user ID literal
+keys_for_userid() {
+    local userID
+    local noKey=
+    local nKeys
+    local nKeysOK
+    local ok
+    local sshKey
+    local tmpfile
+
+    userID="$1"
+
+    log verbose "processing: $userID"
+
+    nKeys=0
+    nKeysOK=0
+
+    IFS=$'\n'
+    for line in $(process_user_id "${userID}") ; do
+       # note that key was found
+       nKeys=$((nKeys+1))
+
+       ok=$(echo "$line" | cut -d: -f1)
+       sshKey=$(echo "$line" | cut -d: -f2)
+
+        if [ -z "$sshKey" ] ; then
+            continue
+        fi
+
+       # if key OK, output key to stdout
+       if [ "$ok" -eq '0' ] ; then
+           # note that key was found ok
+           nKeysOK=$((nKeysOK+1))
+
+           printf '%s\n' "$sshKey"
+       fi
+    done
+
+    # if at least one key was found...
+    if [ "$nKeys" -gt 0 ] ; then
+       # if ok keys were found, return 0
+       if [ "$nKeysOK" -gt 0 ] ; then
+           return 0
+       # else return 2
+       else
+           return 2
+       fi
+    # if no keys were found, return 1
+    else
+       return 1
+    fi
+}
+
 # process a single host in the known_host file
 process_host_known_hosts() {
     local host
@@ -761,7 +848,7 @@ process_host_known_hosts() {
     local tmpfile
 
     # set the key processing mode
-    export MODE='known_hosts'
+    export REQUIRED_KEY_CAPABILITY="$REQUIRED_HOST_KEY_CAPABILITY"
 
     host="$1"
     userID="ssh://${host}"
@@ -798,7 +885,7 @@ process_host_known_hosts() {
                # hash from stdin to stdout
                tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
                ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
-               ssh-keygen -H -f "$tmpfile" 2> /dev/null
+               ssh-keygen -H -f "$tmpfile" 2>/dev/null
                cat "$tmpfile" >> "$KNOWN_HOSTS"
                rm -f "$tmpfile" "${tmpfile}.old"
            else
@@ -836,6 +923,7 @@ update_known_hosts() {
     local nHostsBAD
     local fileCheck
     local host
+    local newUmask
 
     # the number of hosts specified on command line
     nHosts="$#"
@@ -845,10 +933,20 @@ update_known_hosts() {
 
     # touch the known_hosts file so that the file permission check
     # below won't fail upon not finding the file
-    (umask 0022 && touch "$KNOWN_HOSTS")
+    if [ ! -f "$KNOWN_HOSTS" ]; then
+       # make sure to create any files or directories with the appropriate write bits turned off:
+       newUmask=$(printf "%04o" $(( 0$(umask) | 0022 )) )
+       [ -d $(dirname "$KNOWN_HOSTS") ] \
+           || (umask "$newUmask" && mkdir -p -m 0700 $(dirname "$KNOWN_HOSTS") ) \
+           || failure "Could not create path to known_hosts file '$KNOWN_HOSTS'"
+       # make sure to create this file with the appropriate bits turned off:
+       (umask "$newUmask" && touch "$KNOWN_HOSTS") \
+           || failure "Unable to create known_hosts file '$KNOWN_HOSTS'"
+    fi
 
     # check permissions on the known_hosts file path
-    check_key_file_permissions $(whoami) "$KNOWN_HOSTS" || failure
+    check_key_file_permissions $(whoami) "$KNOWN_HOSTS" \
+       || failure "Bad permissions governing known_hosts file '$KNOWN_HOSTS'"
 
     # create a lockfile on known_hosts:
     lock create "$KNOWN_HOSTS"
@@ -856,7 +954,7 @@ update_known_hosts() {
     trap "lock remove $KNOWN_HOSTS" EXIT
 
     # note pre update file checksum
-    fileCheck="$(file_hash "$KNOWN_HOSTS")"
+    fileCheck=$(file_hash "$KNOWN_HOSTS")
 
     for host ; do
        # process the host
@@ -934,7 +1032,7 @@ process_uid_authorized_keys() {
     local sshKey
 
     # set the key processing mode
-    export MODE='authorized_keys'
+    export REQUIRED_KEY_CAPABILITY="$REQUIRED_USER_KEY_CAPABILITY"
 
     userID="$1"
 
@@ -1080,7 +1178,7 @@ process_authorized_user_ids() {
     # check permissions on the authorized_user_ids file path
     check_key_file_permissions $(whoami) "$authorizedUserIDs" || failure
 
-    if ! meat "$authorizedUserIDs" > /dev/null ; then
+    if ! meat "$authorizedUserIDs" >/dev/null ; then
        log debug " no user IDs to process."
        return
     fi
@@ -1101,9 +1199,11 @@ process_authorized_user_ids() {
 # fingerprints, one per line:
 list_primary_fingerprints() {
     local fake=$(msmktempdir)
+    trap "rm -rf $fake" EXIT
     GNUPGHOME="$fake" gpg --no-tty --quiet --import
     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
        awk -F: '/^fpr:/{ print $10 }'
+    trap - EXIT
     rm -rf "$fake"
 }