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

# many of these tests use as_root
if {! [installtest_p]} { return }
if {! [nss_p]} { 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} {
	    set failed 1
	}
	timeout { 
	    kill -INT -[exp_pid] 2
	    set failed 1
	}
    }
    catch {close}; catch {wait}

    if {$failed != 0} {
	fail "$TEST_NAME"
    } else {
	pass "$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
if {[installtest_p]} then {
    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 revokation by host name" trusted
array unset current_trusted_servers
array set current_trusted_servers [array get servers]

set test "No longer trusted after revokation 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"
}

# Trust our server as a module signer. This must be done as root. 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
}

# Even though this action must be performed as root, Do not perform it if we
# are already root, since it alters the state of the install tree. See PR 11442.
# The related tests should then be expected failures.
set effective_pid [exec /usr/bin/id -u]
if {$effective_pid != 0} {
    set cmd [concat [exec which stap] --trust-servers=signer,no-prompt --use-server=$cert_info]
    eval as_root { $cmd }
}

# Our server should now be trusted as a signer, separate from the previously
# discovered trusted signing servers.
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 {$effective_pid == 0} {
    setup_xfail *-*-*
}
if {[array size new_signing_servers] > 0} {
    pass "$test"
} else {
    fail "$test"
}

# The new_signing_servers array should now match the our_servers array, since
# the we specified our server by certificate serial number so that we don't
# accidentally trust another server on the same host.
set test "Number of new signing servers matches"
if {$effective_pid == 0} {
    setup_xfail *-*-*
}
if {[array size new_signing_servers] == [array size our_servers]} {
    pass "$test"
    set test "New signing servers matches"
    if {$effective_pid == 0} {
	setup_xfail *-*-*
    }
    if {[array_equal new_signing_servers our_servers]} {
	pass "$test"
    } else {
	fail "$test"
    }
} else {
    fail "$test"
    set test "New signing servers matches"
    if {$effective_pid == 0} {
	setup_xfail *-*-*
    }
    fail "$test"
}

set test "Server has port number"
if {[regexp {^.*port=([^ ]*).*} $our_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.
    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.
    if {$no_module == 0} {
	set expect_failure 1
	# If we are root, we can load anything.
	if {$effective_pid == 0} {
	    set expect_failure 0
	} else {
	    set id_info [exec /usr/bin/id]
	    # Not root, so we must be a member of stapusr.
	    if {[regexp {.*\(stapusr\).*} "$id_info"]} {
		# Members of stapdev can load an unsigned module.
		if {[regexp {.*\(stapdev\).*} "$id_info"]} {
		    set expect_failure 0
		} else {
		    # Otherwise, the module must be signed.
		    if {$no_signature == 0} {
			# We must have the necessary credentials. We already know that we are
			# stapusr. See if we need to be stapsys.
			if {$privilege == "--privilege=stapsys"} {
			    if {[regexp {.*\(stapsys\).*} "$id_info"]} {
				set expect_failure 0
			    }
			} else {
			    # stapusr is good enough.
			    set expect_failure 0
			}
		    }
		}
	    }    
	}
	if {$expect_failure} {
	    setup_xfail *-*-*
	}
    }
    set test "Load and run signed module when trusted with $privilege"
    if {$no_module == 1 || ! [installtest_p]} {
	untested "$test"
    } else {
	set failed 1
	set cmd [concat staprun $module_name]
	send_log "executing: $cmd\n"
	eval spawn $cmd
	expect {
	    -timeout 150
	    -re {^Hello From Server\r\n} {
		set failed 0
		exp_continue
	    }
	    timeout { 
		kill -INT -[exp_pid] 2
		set failed 1
	    }
	}
	catch {close}; catch {wait}
	if {$failed == 0} {
	    pass "$test"
	} else {
	    fail "$test"
	}
    }
}

# Revoke trust in our server as a module signer. This must be done as root.
# Specify the server by certificate serial number so that we don't accidentally
# revoke trust in a previously trusted server.
# Even though this action must be performed as root, Do not perform it if we
# are already root, since it alters the state of the install tree. See PR 11442.
if {$effective_pid != 0} {
    set cmd [concat [exec which stap] --trust-servers=revoke,signer,no-prompt --use-server=$cert_info]
    eval as_root { $cmd }
}

# Our server should no longer be trusted as a signer
list_servers "List current signing servers after revokation" signer
array unset current_signing_servers
array set current_signing_servers [array get servers]

set test "No longer trusted as a signer after revokation"
if {[array_in current_signing_servers 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 unless we
# are root or a member of the groups stapdev and stapusr.
if {$no_module == 0} {
    set expect_failure 1
    if {[exec /usr/bin/id -u] == 0} {
	set expect_failure 0
    } else {
	if {[regexp {.*\(stapdev\).*} "$id_info"]} {
	    if {[regexp {.*\(stapusr\).*} "$id_info"]} {
		set expect_failure 0
	    }
	}
    }
    if {$expect_failure} {
	setup_xfail *-*-*
    }
}
set test "Load and run signed module when not trusted"
if {$no_module == 1 || ! [installtest_p]} {
    untested "$test"
} else {
    set failed 1
    set cmd [concat staprun $module_name]
    send_log "executing: $cmd\n"
    eval spawn $cmd
    expect {
	-timeout 150
	-re {^Hello From Server\r\n} {
	    set failed 0
	    exp_continue;
	}
	timeout { 
	    kill -INT -[exp_pid] 2
	    set failed 1
	}
    }
    catch {close}; catch {wait}
    if {$failed == 0} {
	pass "$test"
    } else {
	fail "$test"
    }
}

# Shutdown the server we started
shutdown_server
