# In this test, we are using arrays as sets. The key itself does not matter,
# only the value

if {! [installtest_p]} { untested "client"; return }
if {! [nss_p]} { untested "client (no nss)"; return }

# Note we have to be root (to get the namespace to work correctly).
set effective_uid [exec /usr/bin/id -u]
if {$effective_uid != 0} { untested "client (must be root)"; return }

# Let's start with a clean slate in terms of trust.
exec rm -fr $env(SYSTEMTAP_DIR)/ssl

# arr given as list (e.g. [array_has [array get myarr] elem])
proc array_has {arr elem} {

    array set foo $arr
    if {[array size foo] == 0} {
        return 0
    }

    foreach {key val} [array get foo] {
        if {[string equal $val $elem]} {
            return 1
        }
    }

    return 0
}

# Check if array1 is a subset of array2. Returns 1 if yes, otherwise 0.
# (Modified from http://wiki.tcl.tk/1032.)
proc array_in {array1 array2} {
    upvar 1 $array1 sub $array2 super

    if {![array exists sub]} {
        return -code error "$array1 is not an array"
    }
    if {![array exists super]} {
        return -code error "$array2 is not an array"
    }
    if {[array size sub] > [array size super]} {
        return 0
    }
    if {[array size sub] == 0} {
        return 1
    }

    foreach key [array names sub] {
        if {![array_has [array get super] $sub($key)]} {
            return 0
        }
    }
    return 1
}

# Check if array1 and array2 have all the same elements
proc array_equal {array1 array2} {
    upvar 1 $array1 foo $array2 bar
    return [expr [array_in foo bar] && [array_in bar foo]]
}

# Returns the union of the three arrays
proc array_union {array1 array2 array3} {
    upvar 1 $array1 foo $array2 bar $array3 baz
    array unset ::union

    if {![array exists foo]} {
        return -code error "$array1 is not an array"
    }
    if {![array exists bar]} {
        return -code error "$array2 is not an array"
    }
    if {![array exists baz]} {
        return -code error "$array3 is not an array"
    }

    set n 0
    foreach key [array names foo] {
        set ::union($n) $foo($key)
        incr n
    }
    foreach key [array names bar] {
        if {![array_has [array get ::union] $bar($key)]} {
            set ::union($n) $bar($key)
            incr n
        }
    }
    foreach key [array names baz] {
        if {![array_has [array get ::union] $baz($key)]} {
            set ::union($n) $baz($key)
            incr n
        }
    }
}

# Returns all the elements in array_new not in array_old
proc array_diff {array_new array_old} {
    upvar 1 $array_new anew $array_old aold
    array unset ::diff

    if {![array exists anew]} {
        return -code error "$array_new is not an array"
    }
    if {![array exists aold]} {
        return -code error "$array_old is not an array"
    }

    set n 0
    foreach key [array names anew] {
        if {![array_has [array get aold] $anew($key)]} {
            set ::diff($n) $anew($key)
            incr n
        }
    }
}

# Test the --list-servers option and return an array of the servers found.
proc list_servers { TEST_NAME SERVER_SPEC args } {
    set failed 0
    set n 0
    array unset ::servers
    set cmd [concat stap --list-servers=$SERVER_SPEC $args]
    send_log "executing: $cmd\n"
    eval spawn $cmd
    expect {
	-timeout 150
	-re "^Systemtap Compile Server Status for '${SERVER_SPEC}'\r\n" {
	    exp_continue
	}
	-re {^No servers found\r\n} {
	}
	-re {^ host=[^\r]*\r\n} {
	    set ::servers($n) "$expect_out(0,string)"
	    incr n
	    exp_continue
	}
	-re {^No certificate found in database [^\r]*\r\n} {
	    exp_continue
	}
	-re {^[^\r]*\r\n} {
	    verbose -log "unexpected output: $expect_out(0,string)"
	    set failed 1
	}
	timeout { 
	    verbose -log "timeout"
	    kill -INT -[exp_pid] 2
	    set failed 1
	}
    }
    catch {close}; catch {wait}

    if {$failed != 0} {
	fail "$TEST_NAME"
    } else {
	pass "$TEST_NAME"
    }
}

# Test the --list-servers option and return an array of the servers
# found in the custom namespace.
proc ns_list_servers { TEST_NAME SERVER_SPEC args } {
    set failed 0
    set n 0
    array unset ::servers
    set cmd "stap --list-servers=$SERVER_SPEC $args"
    send_log "executing: $cmd\n"
    lassign [server_ns_as_root $cmd] rc output
    if {$rc == 0} {
	# The command succeeded. Search the output for the servers.
	foreach line [split $output \n] {
	    if {[regexp {^ host=(.*)$} $line match server]} {
		set ::servers($n) $server
		incr n
	    } elseif {[regexp "^Systemtap Compile Server Status for '${SERVER_SPEC}'$" $line]
		       || [regexp {^No servers found$} $line]
		       || [regexp {^No certificate found in database .*$} $line]} {
		# Ignore this output
	    } else {
		verbose -log "unexpected output: $line"
	    }
	}
	pass "$TEST_NAME"
    } else {
	fail "$TEST_NAME"
    }
}

# There may be other servers running. Let's keep track of them.
#
# Sometimes, we'll see a server running from the last test, if it
# hasn't quite died yet. So, make sure we get the same result twice.
list_servers "List existing online servers" online
array set existing_online_servers [array get servers]
set i 0
while {1} {
    list_servers "List existing online servers" online
    array unset eos2
    array set eos2 [array get servers]
    set i [expr $i + 1]
    if {$i > 10} {
	fail "List existing online servers: never got stable"
	return
    }

    verbose -log "verify existing online servers - attempt $i: [array size existing_online_servers] [array size eos2]"
    if {[array_equal existing_online_servers eos2]} {
	# Arrays are equal, we're done
	break
    }
    array unset existing_online_servers
    array set existing_online_servers [array get eos2]
}

# There may be existing trusted servers. Keep track of them.
list_servers "List existing trusted servers" trusted
array unset existing_trusted_servers
array set existing_trusted_servers [array get servers]

# There may be existing trusted signers. Keep track of them.
list_servers "List existing signing servers" signer
array unset existing_signing_servers
array set existing_signing_servers [array get servers]

# If we query all known servers, it should contain exactly the union of the
# above three queries.
list_servers "List all existing servers" all
array unset all_existing_servers
array set all_existing_servers [array get servers]

# First we create a union
array_union existing_online_servers \
            existing_trusted_servers \
            existing_signing_servers
array set existing_unioned_servers [array get union]

# Now we can compare
set test "Verify existing server list"
if {[array_equal existing_unioned_servers all_existing_servers]} {
    pass "$test"
} else {
    fail "$test"
}

list_servers "List existing online servers (before start)" online
array unset existing_online_servers
array set existing_online_servers [array get servers]

# Now start our own server and make sure we can work with it.
if {! [setup_server] || $avahi_ok_p != 1} {
    untested "Compile-server client tests against a server"
    return
}

# Our server should now appear online, separate from the previously
# discovered online servers. Note that our server could generate
# serveral listings because it could appear at more than one ip
# address,
list_servers "List current online servers" online
array unset current_online_servers
array set current_online_servers [array get servers]

# array_diff will give us all the servers in current not in existing
array_diff current_online_servers existing_online_servers
array unset new_online_servers
array set new_online_servers [array get diff]

set test "New online servers"
if {[array size new_online_servers] > 0} {
    pass "$test"
} else {
    fail "$test"
}

# Our server should now be trusted, separate from the previously
# discovered trusted servers.
list_servers "List current trusted servers" online,trusted
array unset current_trusted_servers
array set current_trusted_servers [array get servers]

# array_diff will give us all the servers in current not in existing
array_diff current_trusted_servers existing_trusted_servers
array unset new_trusted_servers
array set new_trusted_servers [array get diff]

set test "New trusted servers"
if {[array size new_trusted_servers] > 0} {
    pass "$test"
} else {
    fail "$test"
}

# The new servers should automatically be trusted, so the
# new_trusted_servers array should be a subset of the
# new_online_servers array, but not necessarily vice-versa, since new
# servers may have come online independently of our testing.
set test "Verify new trusted server list"
if {[array_in new_trusted_servers new_online_servers]} {
    pass "$test"
} else {
    fail "$test"
}

# The newly trusted servers represent the server we just started.
array unset our_servers
array set our_servers [array get new_trusted_servers]

# The new servers should not be trusted as signers so there should be
# no new signing servers.
list_servers "List current signing servers" signer
array unset current_signing_servers
array set current_signing_servers [array get servers]

set test "No new signing servers"
if {[array_equal current_signing_servers existing_signing_servers]} {
    pass "$test"
} else {
    fail "$test"
}

# Revoke trust in our server. Specify the server by host name.
set test "Server has host name"
if {[regexp {^ host=([^ ]*).*} $our_servers(0) match host_name]} {
    pass $test
} else {
    fail $test
}

# Compile and run a simple hello test, selecting the server automatically
set test "Hello from server"
set rc [stap_run_batch $srcdir/systemtap.server/hello.stp $use_server]
if {$rc == 0} { pass $test } else { fail $test }

set cmd [concat stap --trust-servers=ssl,revoke,no-prompt --use-server=$host_name]
send_log "executing: $cmd\n"
eval spawn $cmd
expect {
    -timeout 150
    -re {^.*\r\n} { exp_continue }
    timeout { 
	kill -INT -[exp_pid] 2
	set failed 1
    }
}
catch {close}; catch {wait}

# Our server should no longer be trusted.
list_servers "List current trusted servers after revocation by host name" trusted
array unset current_trusted_servers
array set current_trusted_servers [array get servers]

set test "No longer trusted after revocation by host name"
if {[array_equal current_trusted_servers existing_trusted_servers]} {
    pass "$test"
} else {
    fail "$test"
}

# Reinstate trust in our server. Specify the server by ip address.
# The default for --trusted servers is 'ssl'.
set test "Server has ip address"
if {[regexp {^.*address=([^ ]*).*} $our_servers(0) match ip_address]} {
    pass $test
} else {
    fail $test
}
set cmd [concat stap --trust-servers=no-prompt --use-server=$ip_address]
send_log "executing: $cmd\n"
eval spawn $cmd
expect {
    -timeout 150
    -re {^.*\r\n} { exp_continue }
    timeout { 
	kill -INT -[exp_pid] 2
	set failed 1
    }
}
catch {close}; catch {wait}

# Our server should be trusted again, separate from the previously discovered
# trusted servers.
list_servers "List current trusted servers after reinstatement by ip address" online,trusted
array unset current_trusted_servers
array set current_trusted_servers [array get servers]

array_diff current_trusted_servers existing_trusted_servers
array unset new_trusted_servers
array set new_trusted_servers [array get diff]

set test "New trusted servers after reinstatement by ip address"
if {[array size new_trusted_servers] > 0} {
    pass "$test"
} else {
    fail "$test"
}

# The new_trusted_servers array should now match the our_servers
# array, since the our_servers array is a copy of the original
# new_trusted_servers array.
set test "New trusted servers matches after reinstatement by ip address"
if {[array_equal new_trusted_servers our_servers]} {
    pass "$test"
} else {
    fail "$test"
    verbose -log "new_trusted_servers:"
	foreach {index value} [array get new_trusted_servers] {
	    verbose -log "  index: $index, value: $value" 
	}
	verbose -log "our_servers:"
	foreach {index value} [array get our_servers] {
	    verbose -log "  index: $index, value: $value" 
	}
}

# Trust our server as a module signer. This must be done as root and
# in the namespace. Specify the server by certificate serial number.
set test "Server has certificate serial number"
if {[regexp {^.*certinfo="([^ ]*)".*} $our_servers(0) match cert_info]} {
    pass $test
} else {
    fail $test
}

# Grab the existing trusted signers in the namespace. There should be
# none (since we're starting with a clean slate).
set test "Namespace existing signer servers"
ns_list_servers "List namespace existing signing servers" signer
array unset ns_existing_signing_servers
array set ns_existing_signing_servers [array get servers]
if {[array size ns_existing_signing_servers] == 0} {
    pass $test
} else {
    fail $test
}

# We only want to change the system's trusted signer list in our mount
# namespace, not in the real system.
set test "Adding trusted signer (in a namespace)"
set cmd "stap --trust-servers=signer,no-prompt --use-server=$cert_info"
lassign [server_ns_as_root $cmd] rc output
if {$rc == 0} {
    pass "$test"
} else {
    fail "$test"
}

# Our server should now be trusted as a signer (but only in our namespace).
ns_list_servers "List namespace current online signing servers" online,signer
array unset ns_current_signing_servers
array set ns_current_signing_servers [array get servers]

# The current list of signing servers in the namespace should have
# changed.
array_diff ns_current_signing_servers ns_existing_signing_servers
array unset ns_new_signing_servers
array set ns_new_signing_servers [array get diff]
set test "New namespace signing servers"
if {[array size ns_new_signing_servers] > 0} {
    pass "$test"
} else {
    fail "$test"
}

# The current list of signing servers shouldn't have changed in the
# real system (since the list of servers in the real system and in the
# namespace should be separate).
list_servers "List current online signing servers" online,signer
array unset current_signing_servers
array set current_signing_servers [array get servers]

array_diff current_signing_servers existing_signing_servers
array unset new_signing_servers
array set new_signing_servers [array get diff]
set test "New signing servers"
if {[array size new_signing_servers] == 0} {
    pass "$test"
} else {
    fail "$test"
}

set test "Server has port number"
if {[regexp {^.*port=([^ ]*).*} $ns_new_signing_servers(0) match port_num]} {
    pass $test
} else {
    fail $test
}

foreach privilege {"--unprivileged" "--privilege=stapusr" "--privilege=stapsys"} {
    # Compile a simple test using an unprivileged setting. This will
    # ask the server to check and sign the module. Specify the server
    # using host name and port.
    #
    # Notice we can use the server directly, and don't need to be in
    # the namespace. We'll need to be in the namespace when trying to
    # run the compiled module.
    set test "Compile module using server with $privilege"
    set failed 1
    set module_name ""
    set cmd [concat stap -p4 $privilege $srcdir/systemtap.server/hello.stp --use-server=$host_name:$port_num]
    send_log "executing: $cmd\n"
    eval spawn $cmd
    expect {
	-timeout 150
	-re {^stap_[^ \t\r\n]*\.ko\r\n} {
	    set module_name [string trim "$expect_out(0,string)" \r\n]
	    set failed 0
	    exp_continue
	}
	-re {^.*\r\n} { exp_continue }
	timeout { 
	    kill -INT -[exp_pid] 2
	}
    }
    catch {close}; catch {wait}
    if {$failed == 0} {
	pass "$test"
    }
    send_log "'$module_name'\n"

    # Make sure that the module was returned
    set no_module 0
    set test "Module was created with $privilege"
    catch {exec /bin/ls $module_name $module_name.sgn} result
    send_log "$result\n"
    if {[file exists $module_name]} {
	pass "$test"
    } else {
	fail "$test"
	set no_module 1
    }

    # Make sure that the module was signed
    set no_signature 0
    set test "Module was signed with $privilege"
    if {[file exists $module_name.sgn]} {
	pass "$test"
    } else {
	fail "$test"
	set no_signature 1
    }

    # Make sure we can load the module. This will verify that the
    # signature is correct and trusted.  There must be a module to
    # load.
    #
    # Note that we must do this in the namespace, since only there can
    # the signature be verified.
    #
    # We're going to use the optional 'stapusr' and 'stapsys' users,
    # if they exist.
    set test "Load and run signed module when trusted with $privilege"
    set user ""
    if {$privilege == "--privilege=stapsys"} {
	if {$systemtap_stapsys_user_exists} {
	    set user "stapsys"
	} elseif {$systemtap_stapusr_user_exists} {
	    set user "stapusr"
	    setup_xfail *-*-*
	}
    } else {
	if {$systemtap_stapusr_user_exists} {
	    set user "stapusr"
	}
    }

    if {$no_module == 1 || $no_signature == 1 || [string length $user] == 0} {
	untested "$test"
    } else {
	set failed 1
	set module_path [exec pwd]/$module_name
	set cmd [concat staprun $module_path]
	lassign [server_ns_as_user $user $cmd] rc output
	if {$rc == 0} {
	    foreach line [split $output \n] {
		if {[regexp {^Hello From Server$} $line]} {
		    set failed 0
		}
	    }
	}
	if {$failed == 0} {
	    pass "$test"
	} else {
	    fail "$test"
	}
    }
}

# Revoke trust in our server as a module signer in the namespace.
# Specify the server by certificate serial number to ensure we're
# revoking trust in the correct server.
set test "Revoking trusted signer (in a namespace)"
set cmd [concat stap --trust-servers=revoke,signer,no-prompt --use-server=$cert_info]
lassign [server_ns_as_root $cmd] rc output
if {$rc == 0} {
    pass "$test"
} else {
    fail "$test"
}

# Our server should no longer be trusted as a signer in the namespace.
ns_list_servers "List namespace current signing servers after revocation " signer
array unset ns_current_signing_servers
array set ns_current_signing_servers [array get servers]

set test "No longer trusted as a signer after revocation"
if {[array_in ns_current_signing_servers ns_existing_signing_servers]} {
    pass "$test"
} else {
    fail "$test"
}

# Since our server is no longer a trusted signer, attempting to load
# and run the module now should fail (when run as 'stapsys' in the
# namespace). We have to use the 'stapsys' user since the last module
# compiled used "--privilege=stapsys".
set test "Load and run signed module when not trusted"
if {$no_module == 1 || !$systemtap_stapsys_user_exists} {
    untested "$test"
} else {
    setup_xfail *-*-*
    set failed 1
    set module_path [exec pwd]/$module_name
    set cmd [concat staprun $module_path]
    lassign [server_ns_as_user "stapsys" $cmd] rc output
    if {$rc == 0} {
	foreach line [split $output \n] {
	    if {[regexp {^Hello From Server$} $line]} {
		set failed 0
	    }
	}
    }
    if {$failed == 0} {
	pass "$test"
    } else {
	fail "$test"
    }
}

# Shutdown the server we started
shutdown_server
